From c5caef89ca5be3d7540912a7d76d4d726599813b Mon Sep 17 00:00:00 2001 From: hyeongwoo-choi Date: Tue, 21 Jul 2026 12:02:44 +0000 Subject: [PATCH] =?UTF-8?q?Wave=20D=20=EC=8B=AC=ED=99=94:=20=EC=A0=84=201,?= =?UTF-8?q?463=20=EC=84=9C=EB=B9=84=EC=8A=A4=EB=A5=BC=20=EC=8B=A4=EC=9A=B4?= =?UTF-8?q?=EC=98=81=EA=B8=89=20=EB=A1=9C=EC=A7=81=EC=9C=BC=EB=A1=9C=20(?= =?UTF-8?q?=EC=A4=91=EC=95=99=EA=B0=92=2033=E2=86=92156=EC=A4=84,=20?= =?UTF-8?q?=EC=B4=9D=2023=EB=A7=8C=EC=A4=84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 9섹션 구조 적용: 전필드 검증+에러코드(T_RC/T_MSG), 마스터·정책·상태 조회(sqlcode 0/100/<0 분기), 3+ 업무규칙 분기, 본처리(sqlerrd[2] 반영건수 확인), 이력/감사 적재(cm_log 등 best-effort), 집계·요약 갱신, 예외/보상, 응답 전 필드, 구조화 한글 로깅 - 모듈별 중앙값: au 201 · mg 181 · cl 170 · ac 162 · cm 161 · mm/vl 145 · py 144 · st 143 · rc/lg 138 - 기존 dbio 헬퍼·공통유틸·기존 테이블만 사용 (새 테이블/함수 없음) - 회귀 수정: 심화된 RECONCILE이 XA 브랜치 격리를 위반(미커밋 purchase 조회 후 FAIL)해 ACQUIRE 체인이 끊긴 것 → 미존재(100)를 정상 흐름으로 처리, 전문(UBF) 값으로 대사 진행 - 에이전트가 잡은 실버그: merchant_summary 0-덮어쓰기, 미초기화 문자열 userlog, 호스트변수 중복/스코프 오류 31+28+2건 - 검증: build DONE modules=11 batches=506, 12서버 runok, psc 1,466 AVAIL, 매입체인 XA 커밋(purchase_id=9194 status=S), prepared_xacts=0, 검증분기 동작 확인, 클론 0 Co-Authored-By: Claude Opus 4.8 (1M context) --- app/src/ac/svc/ACQUIRE.pgc | 133 +- app/src/ac/svc/ACQ_ALERTAMT.pgc | 165 +- app/src/ac/svc/ACQ_ALERTCHK.pgc | 186 +- app/src/ac/svc/ACQ_ALERTDUP.pgc | 151 +- app/src/ac/svc/ACQ_ALERTQRY.pgc | 148 +- app/src/ac/svc/ACQ_ALERTVEL.pgc | 167 +- app/src/ac/svc/ACQ_AMTFIX.pgc | 153 +- app/src/ac/svc/ACQ_APPRGAP.pgc | 163 +- app/src/ac/svc/ACQ_APPRLINK.pgc | 153 +- app/src/ac/svc/ACQ_APPRMAP.pgc | 142 +- app/src/ac/svc/ACQ_BRANDQRY.pgc | 140 +- app/src/ac/svc/ACQ_BRANDRANK.pgc | 168 +- app/src/ac/svc/ACQ_BRANDSUM.pgc | 165 +- app/src/ac/svc/ACQ_BULKCHK.pgc | 169 +- app/src/ac/svc/ACQ_BULKRPT.pgc | 154 +- app/src/ac/svc/ACQ_BULKVERIFY.pgc | 175 +- app/src/ac/svc/ACQ_CANCEL.pgc | 101 +- app/src/ac/svc/ACQ_CB_ARB.pgc | 170 +- app/src/ac/svc/ACQ_CB_DEFEND.pgc | 171 +- app/src/ac/svc/ACQ_CB_FINAL.pgc | 163 +- app/src/ac/svc/ACQ_CB_OPEN.pgc | 166 +- app/src/ac/svc/ACQ_CB_QRY.pgc | 175 +- app/src/ac/svc/ACQ_CB_REVERSE.pgc | 174 +- app/src/ac/svc/ACQ_CB_RND2.pgc | 169 +- app/src/ac/svc/ACQ_CB_STAT.pgc | 156 +- app/src/ac/svc/ACQ_CCYQRY.pgc | 151 +- app/src/ac/svc/ACQ_CCYRANK.pgc | 165 +- app/src/ac/svc/ACQ_CCYSUM.pgc | 159 +- app/src/ac/svc/ACQ_CHANDETAIL.pgc | 169 +- app/src/ac/svc/ACQ_CHANRANK.pgc | 169 +- app/src/ac/svc/ACQ_CHANSTAT.pgc | 171 +- app/src/ac/svc/ACQ_CLAIMPROC.pgc | 169 +- app/src/ac/svc/ACQ_CLAIMQRY.pgc | 166 +- app/src/ac/svc/ACQ_CLAIMREG.pgc | 163 +- app/src/ac/svc/ACQ_CORRAPPR.pgc | 153 +- app/src/ac/svc/ACQ_CORRECT.pgc | 157 +- app/src/ac/svc/ACQ_CORRHIST.pgc | 167 +- app/src/ac/svc/ACQ_CORRRSN.pgc | 153 +- app/src/ac/svc/ACQ_CORRSTAT.pgc | 159 +- app/src/ac/svc/ACQ_CTRYQRY.pgc | 154 +- app/src/ac/svc/ACQ_CTRYRANK.pgc | 174 +- app/src/ac/svc/ACQ_CTRYSUM.pgc | 176 +- app/src/ac/svc/ACQ_DATECHG.pgc | 153 +- app/src/ac/svc/ACQ_DAYSTAT.pgc | 164 +- app/src/ac/svc/ACQ_DDC.pgc | 171 +- app/src/ac/svc/ACQ_DOWSTAT.pgc | 170 +- app/src/ac/svc/ACQ_DUPCHK.pgc | 163 +- app/src/ac/svc/ACQ_DUPSCAN.pgc | 155 +- app/src/ac/svc/ACQ_EDC.pgc | 158 +- app/src/ac/svc/ACQ_EDI.pgc | 168 +- app/src/ac/svc/ACQ_EDIQRY.pgc | 165 +- app/src/ac/svc/ACQ_EDIRESEND.pgc | 173 +- app/src/ac/svc/ACQ_EDIRSNDCH.pgc | 157 +- app/src/ac/svc/ACQ_EDISTAT.pgc | 151 +- app/src/ac/svc/ACQ_ERRCLASS.pgc | 170 +- app/src/ac/svc/ACQ_ERRQRY.pgc | 155 +- app/src/ac/svc/ACQ_ERRRETRY.pgc | 151 +- app/src/ac/svc/ACQ_ERRSTAT.pgc | 154 +- app/src/ac/svc/ACQ_ESCROW.pgc | 151 +- app/src/ac/svc/ACQ_ESCROWREL.pgc | 154 +- app/src/ac/svc/ACQ_EVENTBUY.pgc | 156 +- app/src/ac/svc/ACQ_EXCHANGE.pgc | 175 +- app/src/ac/svc/ACQ_FEEADJ.pgc | 157 +- app/src/ac/svc/ACQ_FEEHIST.pgc | 155 +- app/src/ac/svc/ACQ_FEERECALC.pgc | 151 +- app/src/ac/svc/ACQ_FEEWAIVE.pgc | 156 +- app/src/ac/svc/ACQ_FOREIGN.pgc | 178 +- app/src/ac/svc/ACQ_FXCONVERT.pgc | 172 +- app/src/ac/svc/ACQ_FXRATE.pgc | 164 +- app/src/ac/svc/ACQ_GRADESTAT.pgc | 172 +- app/src/ac/svc/ACQ_GUARANTEE.pgc | 157 +- app/src/ac/svc/ACQ_GUARQRY.pgc | 148 +- app/src/ac/svc/ACQ_HALFSTAT.pgc | 158 +- app/src/ac/svc/ACQ_HOLD.pgc | 122 +- app/src/ac/svc/ACQ_HOLDQRY.pgc | 179 +- app/src/ac/svc/ACQ_HOURSTAT.pgc | 162 +- app/src/ac/svc/ACQ_INSTALL.pgc | 171 +- app/src/ac/svc/ACQ_INSTCXL.pgc | 164 +- app/src/ac/svc/ACQ_INSTPLAN.pgc | 148 +- app/src/ac/svc/ACQ_INSTPREPAY.pgc | 163 +- app/src/ac/svc/ACQ_INSTQRY.pgc | 169 +- app/src/ac/svc/ACQ_ISSSTAT.pgc | 168 +- app/src/ac/svc/ACQ_ISSUERCLS.pgc | 155 +- app/src/ac/svc/ACQ_LIMITCHK.pgc | 168 +- app/src/ac/svc/ACQ_LIMITEXC.pgc | 173 +- app/src/ac/svc/ACQ_LIMITOVR.pgc | 164 +- app/src/ac/svc/ACQ_LIMITUSAGE.pgc | 170 +- app/src/ac/svc/ACQ_MERCHSUM.pgc | 109 +- app/src/ac/svc/ACQ_MONSTAT.pgc | 158 +- app/src/ac/svc/ACQ_NETCALC.pgc | 159 +- app/src/ac/svc/ACQ_NOINTSHARE.pgc | 156 +- app/src/ac/svc/ACQ_PARTCXL.pgc | 186 +- app/src/ac/svc/ACQ_PARTIAL.pgc | 160 +- app/src/ac/svc/ACQ_PARTRFND.pgc | 175 +- app/src/ac/svc/ACQ_PROMO.pgc | 154 +- app/src/ac/svc/ACQ_PROMOQRY.pgc | 151 +- app/src/ac/svc/ACQ_PROMOSTAT.pgc | 167 +- app/src/ac/svc/ACQ_QTRSTAT.pgc | 167 +- app/src/ac/svc/ACQ_REBUY.pgc | 173 +- app/src/ac/svc/ACQ_REBUYAPPR.pgc | 153 +- app/src/ac/svc/ACQ_REBUYHIST.pgc | 165 +- app/src/ac/svc/ACQ_REBUYREJ.pgc | 155 +- app/src/ac/svc/ACQ_RECLASS.pgc | 158 +- app/src/ac/svc/ACQ_REFUND.pgc | 178 +- app/src/ac/svc/ACQ_RELEASE.pgc | 159 +- app/src/ac/svc/ACQ_REMATCH.pgc | 167 +- app/src/ac/svc/ACQ_REPROC.pgc | 155 +- app/src/ac/svc/ACQ_RESERVE.pgc | 159 +- app/src/ac/svc/ACQ_RESERVECXL.pgc | 157 +- app/src/ac/svc/ACQ_RESERVEQRY.pgc | 177 +- app/src/ac/svc/ACQ_RETURN.pgc | 162 +- app/src/ac/svc/ACQ_RETURNQRY.pgc | 173 +- app/src/ac/svc/ACQ_RETURNSTAT.pgc | 159 +- app/src/ac/svc/ACQ_SETTLEDUE.pgc | 157 +- app/src/ac/svc/ACQ_SETTLEEXP.pgc | 165 +- app/src/ac/svc/ACQ_SETTLELINK.pgc | 168 +- app/src/ac/svc/ACQ_SETTLEPRE.pgc | 177 +- app/src/ac/svc/ACQ_SPLITPAY.pgc | 168 +- app/src/ac/svc/ACQ_SPLITQRY.pgc | 166 +- app/src/ac/svc/ACQ_STATUS.pgc | 132 +- app/src/ac/svc/ACQ_TAXCXL.pgc | 167 +- app/src/ac/svc/ACQ_TAXINV.pgc | 154 +- app/src/ac/svc/ACQ_TAXMODIFY.pgc | 165 +- app/src/ac/svc/ACQ_TAXQRY.pgc | 146 +- app/src/ac/svc/ACQ_TAXREISSUE.pgc | 156 +- app/src/ac/svc/ACQ_TAXREVERSE.pgc | 166 +- app/src/ac/svc/ACQ_TOPMERCH.pgc | 172 +- app/src/ac/svc/ACQ_UNMATCH.pgc | 170 +- app/src/ac/svc/ACQ_VELOCITY.pgc | 171 +- app/src/ac/svc/ACQ_VOID.pgc | 165 +- app/src/ac/svc/ACQ_WEEKSTAT.pgc | 163 +- app/src/ac/svc/ACQ_WHT.pgc | 168 +- app/src/ac/svc/ACQ_XFERLINK.pgc | 158 +- app/src/au/svc/APPROVAL_RATE.pgc | 209 +- app/src/au/svc/AUTH.pgc | 233 +- app/src/au/svc/AUTH_AMT_ADJUST.pgc | 219 +- app/src/au/svc/AUTH_APPROVE.pgc | 199 +- app/src/au/svc/AUTH_CANCEL.pgc | 218 +- app/src/au/svc/AUTH_CAPTURE.pgc | 200 +- app/src/au/svc/AUTH_DECLINE.pgc | 204 +- app/src/au/svc/AUTH_DELAY_MON.pgc | 207 +- app/src/au/svc/AUTH_DUP_CHK.pgc | 208 +- app/src/au/svc/AUTH_EXPIRE_CHK.pgc | 210 +- app/src/au/svc/AUTH_HISTORY.pgc | 194 +- app/src/au/svc/AUTH_HOLD.pgc | 198 +- app/src/au/svc/AUTH_PARTAPPR.pgc | 223 +- app/src/au/svc/AUTH_PARTCXL.pgc | 216 +- app/src/au/svc/AUTH_PARTIAL.pgc | 229 +- app/src/au/svc/AUTH_RELEASE.pgc | 201 +- app/src/au/svc/AUTH_REPROC_REQ.pgc | 203 +- app/src/au/svc/AUTH_REPROC_STAT.pgc | 201 +- app/src/au/svc/AUTH_RESEND.pgc | 208 +- app/src/au/svc/AUTH_RETRY.pgc | 207 +- app/src/au/svc/AUTH_REVERSE.pgc | 207 +- app/src/au/svc/AUTH_ROUTE.pgc | 201 +- app/src/au/svc/AUTH_STATUS.pgc | 195 +- app/src/au/svc/AUTH_SUMMARY.pgc | 133 +- app/src/au/svc/AUTH_VOID.pgc | 214 +- app/src/au/svc/BIN_ROUTE_INQ.pgc | 210 +- app/src/au/svc/BLACKLIST_ADD.pgc | 194 +- app/src/au/svc/BLACKLIST_CHK.pgc | 183 +- app/src/au/svc/BLACKLIST_COUNT.pgc | 185 +- app/src/au/svc/BLACKLIST_DEL.pgc | 187 +- app/src/au/svc/BLACKLIST_INQ.pgc | 194 +- app/src/au/svc/BLACKLIST_PROMO.pgc | 199 +- app/src/au/svc/BRAND_AUTH_STAT.pgc | 208 +- app/src/au/svc/BRAND_POLICY.pgc | 203 +- app/src/au/svc/CANCEL_AMT_INQ.pgc | 202 +- app/src/au/svc/CANCEL_FULL.pgc | 217 +- app/src/au/svc/CANCEL_HISTORY.pgc | 201 +- app/src/au/svc/CANCEL_REPROC.pgc | 226 +- app/src/au/svc/CANCEL_STATUS.pgc | 209 +- app/src/au/svc/CANCEL_SUMMARY.pgc | 201 +- app/src/au/svc/CARD_ACTIVATE.pgc | 192 +- app/src/au/svc/CARD_DAILY_RPT.pgc | 216 +- app/src/au/svc/CARD_EXPIRE_CHK.pgc | 195 +- app/src/au/svc/CARD_LOST_REG.pgc | 189 +- app/src/au/svc/CARD_REACTIVATE.pgc | 188 +- app/src/au/svc/CARD_REISSUE.pgc | 198 +- app/src/au/svc/CARD_RESUME.pgc | 195 +- app/src/au/svc/CARD_STATE_HIST.pgc | 201 +- app/src/au/svc/CARD_STATE_INQ.pgc | 196 +- app/src/au/svc/CARD_STOLEN_REG.pgc | 197 +- app/src/au/svc/CARD_SUSPEND.pgc | 194 +- app/src/au/svc/CARD_VALIDATE.pgc | 180 +- app/src/au/svc/CHANNEL_POLICY.pgc | 207 +- app/src/au/svc/CHANNEL_STAT.pgc | 205 +- app/src/au/svc/DCC_QUOTE.pgc | 201 +- app/src/au/svc/DECLINE_ANALYZE.pgc | 209 +- app/src/au/svc/FRAUD_CASE_CLOSE.pgc | 199 +- app/src/au/svc/FRAUD_CASE_INQ.pgc | 211 +- app/src/au/svc/FRAUD_CASE_LIST.pgc | 199 +- app/src/au/svc/FRAUD_DETECT.pgc | 206 +- app/src/au/svc/FRAUD_RULE_AMT.pgc | 206 +- app/src/au/svc/FRAUD_RULE_DUP.pgc | 211 +- app/src/au/svc/FRAUD_RULE_FREQ.pgc | 210 +- app/src/au/svc/FRAUD_RULE_INTL.pgc | 202 +- app/src/au/svc/FRAUD_RULE_MCC.pgc | 201 +- app/src/au/svc/FRAUD_RULE_NIGHT.pgc | 202 +- app/src/au/svc/FRAUD_SCORE_SUM.pgc | 192 +- app/src/au/svc/INSTALL_AUTH.pgc | 221 +- app/src/au/svc/KEY_ROTATE_LOG.pgc | 204 +- app/src/au/svc/LIMIT_ADJUST.pgc | 199 +- app/src/au/svc/LIMIT_CHK.pgc | 195 +- app/src/au/svc/LIMIT_CUMUL_INQ.pgc | 198 +- app/src/au/svc/LIMIT_DAILY_INQ.pgc | 194 +- app/src/au/svc/LIMIT_DEC.pgc | 192 +- app/src/au/svc/LIMIT_EXHAUST_LST.pgc | 203 +- app/src/au/svc/LIMIT_FREEZE.pgc | 193 +- app/src/au/svc/LIMIT_HIST.pgc | 210 +- app/src/au/svc/LIMIT_INQ.pgc | 142 +- app/src/au/svc/LIMIT_MONTH_CHK.pgc | 198 +- app/src/au/svc/LIMIT_MONTH_INQ.pgc | 197 +- app/src/au/svc/LIMIT_OVER_ALERT.pgc | 194 +- app/src/au/svc/LIMIT_PERTX_CHK.pgc | 195 +- app/src/au/svc/LIMIT_RAISE_REQ.pgc | 210 +- app/src/au/svc/LIMIT_REMAIN.pgc | 196 +- app/src/au/svc/LIMIT_RESTORE.pgc | 200 +- app/src/au/svc/LIMIT_RST.pgc | 190 +- app/src/au/svc/LIMIT_SET.pgc | 194 +- app/src/au/svc/LIMIT_TEMP.pgc | 195 +- app/src/au/svc/LIMIT_TIER_INQ.pgc | 195 +- app/src/au/svc/LIMIT_UNFREEZE.pgc | 192 +- app/src/au/svc/LIMIT_USAGE.pgc | 199 +- app/src/au/svc/MCC_AUTH_STAT.pgc | 203 +- app/src/au/svc/MCC_POLICY.pgc | 205 +- app/src/au/svc/MERCH_AUTH_RANK.pgc | 211 +- app/src/au/svc/NIGHT_POLICY.pgc | 199 +- app/src/au/svc/NOAUTH_CAPTURE.pgc | 223 +- app/src/au/svc/OVERSEAS_POLICY.pgc | 225 +- app/src/au/svc/PREAUTH.pgc | 219 +- app/src/au/svc/PREAUTH_CANCEL.pgc | 204 +- app/src/au/svc/PREAUTH_COMPLETE.pgc | 219 +- app/src/au/svc/PREAUTH_EXTEND.pgc | 214 +- app/src/au/svc/PREAUTH_INQ.pgc | 201 +- app/src/au/svc/PREAUTH_PARTUSE.pgc | 209 +- app/src/au/svc/RISK_SCORE_INQ.pgc | 205 +- app/src/au/svc/STANDIN.pgc | 221 +- app/src/au/svc/STANDIN_INQ.pgc | 203 +- app/src/au/svc/STANDIN_LIMIT.pgc | 194 +- app/src/au/svc/STANDIN_RECON.pgc | 201 +- app/src/au/svc/STANDIN_SETTLE.pgc | 218 +- app/src/au/svc/STANDIN_STAT.pgc | 194 +- app/src/au/svc/STAT_AMT_BAND.pgc | 214 +- app/src/au/svc/STAT_APPROVE_TR.pgc | 210 +- app/src/au/svc/STAT_BYTYPE.pgc | 207 +- app/src/au/svc/STAT_DAILY_SUM.pgc | 209 +- app/src/au/svc/STAT_DECLINE.pgc | 207 +- app/src/au/svc/STAT_HOURLY.pgc | 209 +- app/src/au/svc/STAT_ISSUER.pgc | 222 +- app/src/au/svc/TOKEN_INQ.pgc | 191 +- app/src/au/svc/TOKEN_ISSUE.pgc | 187 +- app/src/au/svc/TOKEN_REVOKE.pgc | 183 +- app/src/au/svc/TOKEN_VERIFY.pgc | 193 +- app/src/au/svc/VELOCITY_AMT.pgc | 195 +- app/src/au/svc/VELOCITY_CHAN.pgc | 203 +- app/src/au/svc/VELOCITY_CHK.pgc | 188 +- app/src/au/svc/VELOCITY_DAY.pgc | 185 +- app/src/au/svc/VELOCITY_HOUR.pgc | 188 +- app/src/au/svc/VELOCITY_MCC.pgc | 189 +- app/src/au/svc/VELOCITY_MERCH.pgc | 198 +- app/src/au/svc/VELOCITY_RESET.pgc | 195 +- app/src/au/svc/VELOCITY_WEEK.pgc | 192 +- app/src/au/svc/WHITELIST_ADD.pgc | 183 +- app/src/au/svc/WHITELIST_CHK.pgc | 185 +- app/src/au/svc/WHITELIST_DEL.pgc | 189 +- app/src/cl/svc/CL_AGGREGATE.pgc | 140 +- app/src/cl/svc/CL_ALERT.pgc | 150 +- app/src/cl/svc/CL_AMTXCHECK.pgc | 151 +- app/src/cl/svc/CL_APPRDELEG.pgc | 171 +- app/src/cl/svc/CL_APPRFINAL.pgc | 169 +- app/src/cl/svc/CL_APPRHIST.pgc | 173 +- app/src/cl/svc/CL_APPRL1.pgc | 167 +- app/src/cl/svc/CL_APPRL2.pgc | 168 +- app/src/cl/svc/CL_APPROVE.pgc | 165 +- app/src/cl/svc/CL_APPRREJECT.pgc | 177 +- app/src/cl/svc/CL_APPRREQ.pgc | 172 +- app/src/cl/svc/CL_APPRSTAT.pgc | 164 +- app/src/cl/svc/CL_APPRWD.pgc | 169 +- app/src/cl/svc/CL_ARCHIVE.pgc | 143 +- app/src/cl/svc/CL_AUDITTRAIL.pgc | 150 +- app/src/cl/svc/CL_BALVERIFY.pgc | 168 +- app/src/cl/svc/CL_CALENDAR.pgc | 158 +- app/src/cl/svc/CL_CANCEL.pgc | 159 +- app/src/cl/svc/CL_CHKALL.pgc | 167 +- app/src/cl/svc/CL_CHKLIMIT.pgc | 153 +- app/src/cl/svc/CL_CHKPENDING.pgc | 147 +- app/src/cl/svc/CL_CHKUNMATCH.pgc | 145 +- app/src/cl/svc/CL_CHKVOID.pgc | 146 +- app/src/cl/svc/CL_CHNCLOSE.pgc | 216 +- app/src/cl/svc/CL_CHNVERIFY.pgc | 175 +- app/src/cl/svc/CL_CLOSERATE.pgc | 214 +- app/src/cl/svc/CL_CNTVERIFY.pgc | 172 +- app/src/cl/svc/CL_CNTXCHECK.pgc | 163 +- app/src/cl/svc/CL_CONFIRM.pgc | 165 +- app/src/cl/svc/CL_CORPCLOSE.pgc | 226 +- app/src/cl/svc/CL_CORPVERIFY.pgc | 176 +- app/src/cl/svc/CL_CXLDUP.pgc | 167 +- app/src/cl/svc/CL_CXLERROR.pgc | 165 +- app/src/cl/svc/CL_CXLLATE.pgc | 165 +- app/src/cl/svc/CL_CXLREASON.pgc | 173 +- app/src/cl/svc/CL_CXLREQ.pgc | 163 +- app/src/cl/svc/CL_CXLSTAT.pgc | 165 +- app/src/cl/svc/CL_DAILY.pgc | 155 +- app/src/cl/svc/CL_DAYVERIFY.pgc | 175 +- app/src/cl/svc/CL_DELAYLIST.pgc | 158 +- app/src/cl/svc/CL_DELAYREG.pgc | 160 +- app/src/cl/svc/CL_DIFFCHK.pgc | 148 +- app/src/cl/svc/CL_DIVCLOSE.pgc | 219 +- app/src/cl/svc/CL_DUELIST.pgc | 162 +- app/src/cl/svc/CL_DUPCHK.pgc | 170 +- app/src/cl/svc/CL_ESCALATE.pgc | 171 +- app/src/cl/svc/CL_FEECROSS.pgc | 163 +- app/src/cl/svc/CL_FEEVERIFY.pgc | 178 +- app/src/cl/svc/CL_FINALIZE.pgc | 213 +- app/src/cl/svc/CL_FORCECLOSE.pgc | 205 +- app/src/cl/svc/CL_FXCLOSE.pgc | 205 +- app/src/cl/svc/CL_GAPDAYS.pgc | 164 +- app/src/cl/svc/CL_HALFCLOSE.pgc | 215 +- app/src/cl/svc/CL_HOLICHK.pgc | 146 +- app/src/cl/svc/CL_HOLIDAY.pgc | 149 +- app/src/cl/svc/CL_HYCLOSE.pgc | 215 +- app/src/cl/svc/CL_HYVERIFY.pgc | 184 +- app/src/cl/svc/CL_INQUIRY.pgc | 164 +- app/src/cl/svc/CL_ISSCLOSE.pgc | 216 +- app/src/cl/svc/CL_LOCK.pgc | 149 +- app/src/cl/svc/CL_LOCKAUDIT.pgc | 167 +- app/src/cl/svc/CL_LOCKSWEEP.pgc | 166 +- app/src/cl/svc/CL_MAINCLOSE.pgc | 213 +- app/src/cl/svc/CL_MEMOSEARCH.pgc | 151 +- app/src/cl/svc/CL_MERCLOSE.pgc | 226 +- app/src/cl/svc/CL_MERXCHECK.pgc | 174 +- app/src/cl/svc/CL_MIDCLOSE.pgc | 208 +- app/src/cl/svc/CL_MONTHLY.pgc | 201 +- app/src/cl/svc/CL_MONVERIFY.pgc | 176 +- app/src/cl/svc/CL_NETCROSS.pgc | 163 +- app/src/cl/svc/CL_NETVERIFY.pgc | 176 +- app/src/cl/svc/CL_NEXTBIZ.pgc | 152 +- app/src/cl/svc/CL_PENDLIST.pgc | 168 +- app/src/cl/svc/CL_PERIOD.pgc | 205 +- app/src/cl/svc/CL_POSCLOSE.pgc | 195 +- app/src/cl/svc/CL_POSTTRIG.pgc | 154 +- app/src/cl/svc/CL_PRECHECK.pgc | 168 +- app/src/cl/svc/CL_PROVCLOSE.pgc | 210 +- app/src/cl/svc/CL_PROVCMP.pgc | 147 +- app/src/cl/svc/CL_PURGE.pgc | 146 +- app/src/cl/svc/CL_QTRVERIFY.pgc | 172 +- app/src/cl/svc/CL_QUARTER.pgc | 202 +- app/src/cl/svc/CL_RECLOSE.pgc | 168 +- app/src/cl/svc/CL_RECLOSECHK.pgc | 152 +- app/src/cl/svc/CL_RECLOSEREG.pgc | 186 +- app/src/cl/svc/CL_RECON.pgc | 171 +- app/src/cl/svc/CL_REOPENREQ.pgc | 177 +- app/src/cl/svc/CL_REPORT.pgc | 142 +- app/src/cl/svc/CL_RESTORE.pgc | 155 +- app/src/cl/svc/CL_ROLLBACK.pgc | 154 +- app/src/cl/svc/CL_RPTAPPR.pgc | 173 +- app/src/cl/svc/CL_RPTCAT.pgc | 177 +- app/src/cl/svc/CL_RPTCHN.pgc | 176 +- app/src/cl/svc/CL_RPTCXL.pgc | 171 +- app/src/cl/svc/CL_RPTDELAY.pgc | 176 +- app/src/cl/svc/CL_RPTDIV.pgc | 176 +- app/src/cl/svc/CL_RPTISSUER.pgc | 177 +- app/src/cl/svc/CL_RPTPERIOD.pgc | 181 +- app/src/cl/svc/CL_RPTSTATUS.pgc | 170 +- app/src/cl/svc/CL_RPTTOPMER.pgc | 174 +- app/src/cl/svc/CL_SCHEDNEXT.pgc | 149 +- app/src/cl/svc/CL_SCHEDULE.pgc | 154 +- app/src/cl/svc/CL_SEGCLOSE.pgc | 217 +- app/src/cl/svc/CL_SEGVERIFY.pgc | 175 +- app/src/cl/svc/CL_SEMIMONTH.pgc | 215 +- app/src/cl/svc/CL_SEQ.pgc | 140 +- app/src/cl/svc/CL_SEQAUDIT.pgc | 155 +- app/src/cl/svc/CL_SLABREACH.pgc | 146 +- app/src/cl/svc/CL_SLAMON.pgc | 155 +- app/src/cl/svc/CL_SLASET.pgc | 145 +- app/src/cl/svc/CL_SNAPDIFF.pgc | 155 +- app/src/cl/svc/CL_SNAPMER.pgc | 159 +- app/src/cl/svc/CL_SNAPSHOT.pgc | 166 +- app/src/cl/svc/CL_SNAPXCHECK.pgc | 166 +- app/src/cl/svc/CL_STATS.pgc | 163 +- app/src/cl/svc/CL_STATUS.pgc | 138 +- app/src/cl/svc/CL_STCHGAUDIT.pgc | 150 +- app/src/cl/svc/CL_STCROSS.pgc | 169 +- app/src/cl/svc/CL_SUMMARY.pgc | 146 +- app/src/cl/svc/CL_TENDAY.pgc | 216 +- app/src/cl/svc/CL_TENTCLOSE.pgc | 202 +- app/src/cl/svc/CL_TREND.pgc | 159 +- app/src/cl/svc/CL_TRIGRPT.pgc | 143 +- app/src/cl/svc/CL_TRIGSETTLE.pgc | 141 +- app/src/cl/svc/CL_TRIGSTAT.pgc | 149 +- app/src/cl/svc/CL_UNCLOSED.pgc | 205 +- app/src/cl/svc/CL_UNLOCK.pgc | 142 +- app/src/cl/svc/CL_VERIFY.pgc | 168 +- app/src/cl/svc/CL_WEEKLY.pgc | 208 +- app/src/cl/svc/CL_WEEKVERIFY.pgc | 173 +- app/src/cl/svc/CL_YEAREND.pgc | 201 +- app/src/cl/svc/CL_YEARPREP.pgc | 161 +- app/src/cl/svc/CL_YEARVERIFY.pgc | 176 +- app/src/cm/svc/CM_ADDDAYS.pgc | 134 +- app/src/cm/svc/CM_ADLER32.pgc | 134 +- app/src/cm/svc/CM_AMT_HANGUL.pgc | 130 +- app/src/cm/svc/CM_AMT_ROUND.pgc | 132 +- app/src/cm/svc/CM_AMT_SPLIT.pgc | 132 +- app/src/cm/svc/CM_AUDIT_BYLVL.pgc | 135 +- app/src/cm/svc/CM_AUDIT_BYSVC.pgc | 137 +- app/src/cm/svc/CM_AUDIT_ERRPCT.pgc | 131 +- app/src/cm/svc/CM_AUDIT_RANGE.pgc | 135 +- app/src/cm/svc/CM_AUDIT_TAIL.pgc | 137 +- app/src/cm/svc/CM_AUDIT_TOP.pgc | 135 +- app/src/cm/svc/CM_BATDEP_CHK.pgc | 135 +- app/src/cm/svc/CM_BATDEP_SET.pgc | 135 +- app/src/cm/svc/CM_BATHIST_CNT.pgc | 137 +- app/src/cm/svc/CM_BATLAST.pgc | 137 +- app/src/cm/svc/CM_BATRERUN_REQ.pgc | 139 +- app/src/cm/svc/CM_BATSCHD_DEL.pgc | 135 +- app/src/cm/svc/CM_BATSCHD_GET.pgc | 135 +- app/src/cm/svc/CM_BATSCHD_LIST.pgc | 137 +- app/src/cm/svc/CM_BATSCHD_REG.pgc | 137 +- app/src/cm/svc/CM_BIZDAY.pgc | 134 +- app/src/cm/svc/CM_BIZDAY_ADD.pgc | 132 +- app/src/cm/svc/CM_BIZDAY_CNT.pgc | 136 +- app/src/cm/svc/CM_CACHE_INV.pgc | 129 +- app/src/cm/svc/CM_CACHE_LOAD.pgc | 135 +- app/src/cm/svc/CM_CHECKDIGIT.pgc | 136 +- app/src/cm/svc/CM_CODE.pgc | 130 +- app/src/cm/svc/CM_CODEGRP_CLOSE.pgc | 134 +- app/src/cm/svc/CM_CODEGRP_CNT.pgc | 134 +- app/src/cm/svc/CM_CODEGRP_DIFF.pgc | 140 +- app/src/cm/svc/CM_CODEGRP_HIST.pgc | 136 +- app/src/cm/svc/CM_CODEGRP_REG.pgc | 138 +- app/src/cm/svc/CM_CODEGRP_SPAN.pgc | 136 +- app/src/cm/svc/CM_CODEGRP_UPD.pgc | 136 +- app/src/cm/svc/CM_CODEGRP_VER.pgc | 132 +- app/src/cm/svc/CM_CODENAME.pgc | 134 +- app/src/cm/svc/CM_CODE_EXPORT.pgc | 138 +- app/src/cm/svc/CM_CODE_MOVE.pgc | 142 +- app/src/cm/svc/CM_CODE_REORDER.pgc | 136 +- app/src/cm/svc/CM_CODE_SEARCH.pgc | 132 +- app/src/cm/svc/CM_CODE_TOGGLE.pgc | 136 +- app/src/cm/svc/CM_CRC.pgc | 132 +- app/src/cm/svc/CM_DATEDIFF.pgc | 136 +- app/src/cm/svc/CM_DAYOFYEAR.pgc | 132 +- app/src/cm/svc/CM_ENC_HEX.pgc | 134 +- app/src/cm/svc/CM_ERRMSG.pgc | 134 +- app/src/cm/svc/CM_FILE_CKSUM.pgc | 134 +- app/src/cm/svc/CM_FMT_AMT.pgc | 132 +- app/src/cm/svc/CM_FMT_DATE.pgc | 134 +- app/src/cm/svc/CM_FX.pgc | 134 +- app/src/cm/svc/CM_FX_AGE.pgc | 134 +- app/src/cm/svc/CM_FX_CONV.pgc | 134 +- app/src/cm/svc/CM_FX_CROSS.pgc | 138 +- app/src/cm/svc/CM_FX_HIST.pgc | 136 +- app/src/cm/svc/CM_FX_INVERT.pgc | 132 +- app/src/cm/svc/CM_FX_MARGIN.pgc | 132 +- app/src/cm/svc/CM_FX_MEAN.pgc | 130 +- app/src/cm/svc/CM_FX_PUBLISH.pgc | 136 +- app/src/cm/svc/CM_FX_TICKS.pgc | 136 +- app/src/cm/svc/CM_FX_TREND.pgc | 134 +- app/src/cm/svc/CM_FX_TRIANGLE.pgc | 136 +- app/src/cm/svc/CM_FX_VALID.pgc | 132 +- app/src/cm/svc/CM_HASH_DJB2.pgc | 134 +- app/src/cm/svc/CM_HASH_FNV.pgc | 134 +- app/src/cm/svc/CM_HASH_SUM.pgc | 134 +- app/src/cm/svc/CM_HEALTH_ALL.pgc | 137 +- app/src/cm/svc/CM_HEALTH_DB.pgc | 131 +- app/src/cm/svc/CM_HEALTH_FX.pgc | 135 +- app/src/cm/svc/CM_HEALTH_LOG.pgc | 135 +- app/src/cm/svc/CM_HEALTH_PARAM.pgc | 129 +- app/src/cm/svc/CM_HEALTH_SEQ.pgc | 131 +- app/src/cm/svc/CM_HOLIDAY_CHK.pgc | 132 +- app/src/cm/svc/CM_HOLIDAY_CNT.pgc | 136 +- app/src/cm/svc/CM_HOLIDAY_DEL.pgc | 136 +- app/src/cm/svc/CM_HOLIDAY_NEXT.pgc | 132 +- app/src/cm/svc/CM_HOLIDAY_REG.pgc | 138 +- app/src/cm/svc/CM_HOLIDAY_YEAR.pgc | 132 +- app/src/cm/svc/CM_INTEREST.pgc | 134 +- app/src/cm/svc/CM_ISO_WEEK.pgc | 132 +- app/src/cm/svc/CM_LOG.pgc | 135 +- app/src/cm/svc/CM_LUHN.pgc | 128 +- app/src/cm/svc/CM_MASK.pgc | 132 +- app/src/cm/svc/CM_MASK_ACCT.pgc | 130 +- app/src/cm/svc/CM_MASK_CARD.pgc | 132 +- app/src/cm/svc/CM_MASK_EMAIL.pgc | 130 +- app/src/cm/svc/CM_MASK_NAME.pgc | 132 +- app/src/cm/svc/CM_MASK_PHONE.pgc | 132 +- app/src/cm/svc/CM_MASK_RRN.pgc | 134 +- app/src/cm/svc/CM_MKT_STATUS.pgc | 131 +- app/src/cm/svc/CM_MONTHEND.pgc | 134 +- app/src/cm/svc/CM_MSG_DEL.pgc | 136 +- app/src/cm/svc/CM_MSG_FALLBACK.pgc | 136 +- app/src/cm/svc/CM_MSG_FORMAT.pgc | 136 +- app/src/cm/svc/CM_MSG_GET.pgc | 134 +- app/src/cm/svc/CM_MSG_LANGS.pgc | 140 +- app/src/cm/svc/CM_MSG_RENDER.pgc | 134 +- app/src/cm/svc/CM_MSG_SET.pgc | 136 +- app/src/cm/svc/CM_NETTING_CALC.pgc | 128 +- app/src/cm/svc/CM_NEXTBIZ.pgc | 136 +- app/src/cm/svc/CM_NOTI_HIST.pgc | 135 +- app/src/cm/svc/CM_NOTI_QUOTA.pgc | 131 +- app/src/cm/svc/CM_NOTI_RETRY.pgc | 137 +- app/src/cm/svc/CM_NOTI_SEND.pgc | 135 +- app/src/cm/svc/CM_NOTI_STAT.pgc | 135 +- app/src/cm/svc/CM_NOTI_TMPL.pgc | 135 +- app/src/cm/svc/CM_PARAM.pgc | 135 +- app/src/cm/svc/CM_PARAM_APPROVE.pgc | 135 +- app/src/cm/svc/CM_PARAM_CANCEL.pgc | 133 +- app/src/cm/svc/CM_PARAM_DIFF.pgc | 131 +- app/src/cm/svc/CM_PARAM_HIST.pgc | 133 +- app/src/cm/svc/CM_PARAM_LIST.pgc | 135 +- app/src/cm/svc/CM_PARAM_NUM.pgc | 133 +- app/src/cm/svc/CM_PARAM_PENDING.pgc | 129 +- app/src/cm/svc/CM_PARAM_REJECT.pgc | 133 +- app/src/cm/svc/CM_PARAM_REQ.pgc | 135 +- app/src/cm/svc/CM_PARAM_SET.pgc | 135 +- app/src/cm/svc/CM_PING.pgc | 129 +- app/src/cm/svc/CM_PREVBIZ.pgc | 138 +- app/src/cm/svc/CM_PRORATE.pgc | 134 +- app/src/cm/svc/CM_QTR_END.pgc | 132 +- app/src/cm/svc/CM_SEQ.pgc | 132 +- app/src/cm/svc/CM_SEQ_ALLOC.pgc | 134 +- app/src/cm/svc/CM_SEQ_DAILY.pgc | 132 +- app/src/cm/svc/CM_SEQ_MULTI.pgc | 132 +- app/src/cm/svc/CM_SEQ_PEEK.pgc | 132 +- app/src/cm/svc/CM_SEQ_RESET.pgc | 134 +- app/src/cm/svc/CM_SEQ_STAMP.pgc | 132 +- app/src/cm/svc/CM_SEQ_TYPED.pgc | 130 +- app/src/cm/svc/CM_SEQ_VERIFY.pgc | 132 +- app/src/cm/svc/CM_STATUS.pgc | 131 +- app/src/cm/svc/CM_STR_PAD.pgc | 130 +- app/src/cm/svc/CM_STR_TRIM.pgc | 132 +- app/src/cm/svc/CM_WEEKDAY.pgc | 130 +- app/src/lg/svc/LG_ACCRUEDPAY.pgc | 115 +- app/src/lg/svc/LG_ACCRUEDRECV.pgc | 100 +- app/src/lg/svc/LG_ACCRUEREV.pgc | 100 +- app/src/lg/svc/LG_ACCTGROUP.pgc | 105 +- app/src/lg/svc/LG_ACCTHIST.pgc | 105 +- app/src/lg/svc/LG_ACCTLEDGER.pgc | 108 +- app/src/lg/svc/LG_ACCTPIVOT.pgc | 108 +- app/src/lg/svc/LG_ACCTRANGE.pgc | 105 +- app/src/lg/svc/LG_ACCTSUM.pgc | 109 +- app/src/lg/svc/LG_ACCTTOPN.pgc | 105 +- app/src/lg/svc/LG_ACQPOST.pgc | 110 +- app/src/lg/svc/LG_ADJUST.pgc | 101 +- app/src/lg/svc/LG_ALLOWANCE.pgc | 100 +- app/src/lg/svc/LG_ALLOWRECALC.pgc | 100 +- app/src/lg/svc/LG_APPLY.pgc | 101 +- app/src/lg/svc/LG_APPRDELEG.pgc | 112 +- app/src/lg/svc/LG_APPRHIST.pgc | 109 +- app/src/lg/svc/LG_APPROVE.pgc | 109 +- app/src/lg/svc/LG_APPRQUEUE.pgc | 112 +- app/src/lg/svc/LG_AUDITSCAN.pgc | 100 +- app/src/lg/svc/LG_BADDEBT.pgc | 103 +- app/src/lg/svc/LG_BADDEBTRCV.pgc | 100 +- app/src/lg/svc/LG_BALCERT.pgc | 105 +- app/src/lg/svc/LG_BALCHK.pgc | 106 +- app/src/lg/svc/LG_BALPROOF.pgc | 105 +- app/src/lg/svc/LG_BATCHAPPROVE.pgc | 109 +- app/src/lg/svc/LG_BIGLINE.pgc | 100 +- app/src/lg/svc/LG_BLACKADJUST.pgc | 112 +- app/src/lg/svc/LG_BLACKENTRY.pgc | 109 +- app/src/lg/svc/LG_BSSUMMARY.pgc | 100 +- app/src/lg/svc/LG_CARRY.pgc | 109 +- app/src/lg/svc/LG_CLOSECHK.pgc | 100 +- app/src/lg/svc/LG_CLOSELIST.pgc | 100 +- app/src/lg/svc/LG_CLOSE_LINK.pgc | 101 +- app/src/lg/svc/LG_CONFIRM.pgc | 113 +- app/src/lg/svc/LG_CORPLEDGER.pgc | 115 +- app/src/lg/svc/LG_CORPSUM.pgc | 115 +- app/src/lg/svc/LG_CORRECT.pgc | 104 +- app/src/lg/svc/LG_CORRPAIR.pgc | 100 +- app/src/lg/svc/LG_CREDIT.pgc | 110 +- app/src/lg/svc/LG_DAILYSUM.pgc | 101 +- app/src/lg/svc/LG_DAYCLOSE.pgc | 109 +- app/src/lg/svc/LG_DEBIT.pgc | 101 +- app/src/lg/svc/LG_DEPTLEDGER.pgc | 105 +- app/src/lg/svc/LG_DEPTPL.pgc | 100 +- app/src/lg/svc/LG_DEPTRANGE.pgc | 108 +- app/src/lg/svc/LG_DEPTSUM.pgc | 100 +- app/src/lg/svc/LG_DIFFCHK.pgc | 109 +- app/src/lg/svc/LG_DOUBLE.pgc | 101 +- app/src/lg/svc/LG_DUPSCAN.pgc | 101 +- app/src/lg/svc/LG_EQUITYMOVE.pgc | 100 +- app/src/lg/svc/LG_EXPENSEROLL.pgc | 105 +- app/src/lg/svc/LG_FEEACCRUE.pgc | 100 +- app/src/lg/svc/LG_FEEPOST.pgc | 110 +- app/src/lg/svc/LG_FEERECOG.pgc | 100 +- app/src/lg/svc/LG_FEESPLIT.pgc | 101 +- app/src/lg/svc/LG_HALFCLOSE.pgc | 100 +- app/src/lg/svc/LG_INCOMEROLL.pgc | 105 +- app/src/lg/svc/LG_INQUIRY.pgc | 101 +- app/src/lg/svc/LG_JOURNAL.pgc | 101 +- app/src/lg/svc/LG_LEDGSETTLE.pgc | 115 +- app/src/lg/svc/LG_LINECOUNT.pgc | 103 +- app/src/lg/svc/LG_MASKINQ.pgc | 103 +- app/src/lg/svc/LG_MEMOAUDIT.pgc | 109 +- app/src/lg/svc/LG_MERCHBAL.pgc | 119 +- app/src/lg/svc/LG_MERCHCERT.pgc | 118 +- app/src/lg/svc/LG_MERCHPROOF.pgc | 115 +- app/src/lg/svc/LG_MONCLOSEJE.pgc | 100 +- app/src/lg/svc/LG_MONTHCLOSE.pgc | 103 +- app/src/lg/svc/LG_MONTHLYSUM.pgc | 101 +- app/src/lg/svc/LG_NETPOS.pgc | 115 +- app/src/lg/svc/LG_PARTREVERSE.pgc | 109 +- app/src/lg/svc/LG_PAYACCRUE.pgc | 118 +- app/src/lg/svc/LG_PAYMATCH.pgc | 115 +- app/src/lg/svc/LG_PAYRECON.pgc | 118 +- app/src/lg/svc/LG_PENDAGE.pgc | 109 +- app/src/lg/svc/LG_PENDCLEAR.pgc | 109 +- app/src/lg/svc/LG_PENDESCAL.pgc | 112 +- app/src/lg/svc/LG_PENDLIST.pgc | 109 +- app/src/lg/svc/LG_PENDROUTE.pgc | 112 +- app/src/lg/svc/LG_PENDSTAT.pgc | 109 +- app/src/lg/svc/LG_PERIODTB.pgc | 105 +- app/src/lg/svc/LG_PLCLOSE.pgc | 104 +- app/src/lg/svc/LG_PLSUMMARY.pgc | 103 +- app/src/lg/svc/LG_POST.pgc | 110 +- app/src/lg/svc/LG_PREPAIDAMORT.pgc | 100 +- app/src/lg/svc/LG_PREPAIDPOST.pgc | 109 +- app/src/lg/svc/LG_QTRCLOSE.pgc | 100 +- app/src/lg/svc/LG_QTRCLOSEJE.pgc | 100 +- app/src/lg/svc/LG_RECALC.pgc | 104 +- app/src/lg/svc/LG_RECOVERY.pgc | 100 +- app/src/lg/svc/LG_RECVACCRUE.pgc | 103 +- app/src/lg/svc/LG_REDADJUST.pgc | 112 +- app/src/lg/svc/LG_REDBLACKNET.pgc | 109 +- app/src/lg/svc/LG_REDENTRY.pgc | 109 +- app/src/lg/svc/LG_REJECT.pgc | 109 +- app/src/lg/svc/LG_REOPEN.pgc | 109 +- app/src/lg/svc/LG_REPOST.pgc | 112 +- app/src/lg/svc/LG_REVACCRUE.pgc | 100 +- app/src/lg/svc/LG_REVDEFER.pgc | 103 +- app/src/lg/svc/LG_REVERSE.pgc | 110 +- app/src/lg/svc/LG_REVRELEASE.pgc | 100 +- app/src/lg/svc/LG_ROLLFWD.pgc | 106 +- app/src/lg/svc/LG_ROLLTB.pgc | 105 +- app/src/lg/svc/LG_SEQAUDIT.pgc | 109 +- app/src/lg/svc/LG_SETTLEPAYGAP.pgc | 115 +- app/src/lg/svc/LG_SETTLEPOST.pgc | 119 +- app/src/lg/svc/LG_SETTLERECON.pgc | 115 +- app/src/lg/svc/LG_SPANTB.pgc | 105 +- app/src/lg/svc/LG_STATACCT.pgc | 108 +- app/src/lg/svc/LG_STATDC.pgc | 108 +- app/src/lg/svc/LG_STATMERCH.pgc | 115 +- app/src/lg/svc/LG_STATS.pgc | 106 +- app/src/lg/svc/LG_STATTYPE.pgc | 105 +- app/src/lg/svc/LG_STATUS.pgc | 113 +- app/src/lg/svc/LG_SUSPENSE.pgc | 100 +- app/src/lg/svc/LG_TBDIFF.pgc | 105 +- app/src/lg/svc/LG_THREEWAY.pgc | 115 +- app/src/lg/svc/LG_TRANSFER.pgc | 101 +- app/src/lg/svc/LG_TRIALBAL.pgc | 106 +- app/src/lg/svc/LG_UNAPPROVE.pgc | 109 +- app/src/lg/svc/LG_UNEARNPOST.pgc | 109 +- app/src/lg/svc/LG_UNEARNRECOG.pgc | 103 +- app/src/lg/svc/LG_UNSETTLED.pgc | 119 +- app/src/lg/svc/LG_VOIDVOUCHER.pgc | 112 +- app/src/lg/svc/LG_VOUCHER.pgc | 110 +- app/src/lg/svc/LG_VOUCHTRACE.pgc | 109 +- app/src/lg/svc/LG_WEEKCLOSE.pgc | 112 +- app/src/lg/svc/LG_WRITEOFF.pgc | 100 +- app/src/lg/svc/LG_XFERPOST.pgc | 109 +- app/src/lg/svc/LG_YEARCARRY.pgc | 108 +- app/src/lg/svc/LG_YEARCLOSE.pgc | 103 +- app/src/lg/svc/LG_ZEROLINE.pgc | 108 +- app/src/mg/svc/MG_ACK.pgc | 149 + app/src/mg/svc/MG_ADVICE.pgc | 149 + app/src/mg/svc/MG_AMTCHECK.pgc | 149 + app/src/mg/svc/MG_AUTHADV.pgc | 89 + app/src/mg/svc/MG_AUTHADVRSP.pgc | 89 + app/src/mg/svc/MG_AUTHREQ.pgc | 149 + app/src/mg/svc/MG_AUTHRSP.pgc | 149 + app/src/mg/svc/MG_BALINQ.pgc | 89 + app/src/mg/svc/MG_BITMAP.pgc | 149 + app/src/mg/svc/MG_BMPMERGE.pgc | 89 + app/src/mg/svc/MG_BMPPARSE.pgc | 89 + app/src/mg/svc/MG_CAPRSP.pgc | 149 + app/src/mg/svc/MG_CAPTURE.pgc | 149 + app/src/mg/svc/MG_CARDVER.pgc | 89 + app/src/mg/svc/MG_CHANAUTH.pgc | 149 + app/src/mg/svc/MG_CHANCLOSE.pgc | 149 + app/src/mg/svc/MG_CHANFAILOVER.pgc | 149 + app/src/mg/svc/MG_CHANHEALTH.pgc | 149 + app/src/mg/svc/MG_CHANLIST.pgc | 149 + app/src/mg/svc/MG_CHANMAP.pgc | 89 + app/src/mg/svc/MG_CHANOPEN.pgc | 149 + app/src/mg/svc/MG_CHANPING.pgc | 149 + app/src/mg/svc/MG_CHANRESET.pgc | 149 + app/src/mg/svc/MG_CHANSTAT.pgc | 149 + app/src/mg/svc/MG_CHANSWITCH.pgc | 149 + app/src/mg/svc/MG_CONFIRM.pgc | 149 + app/src/mg/svc/MG_CONVERT.pgc | 149 + app/src/mg/svc/MG_DECODE.pgc | 89 + app/src/mg/svc/MG_DEQUEUE.pgc | 149 + app/src/mg/svc/MG_DIRECTADPT.pgc | 149 + app/src/mg/svc/MG_DUPCHK.pgc | 149 + app/src/mg/svc/MG_DUPPURGE.pgc | 149 + app/src/mg/svc/MG_ECHO.pgc | 149 + app/src/mg/svc/MG_ENCODE.pgc | 89 + app/src/mg/svc/MG_ERRCLASS.pgc | 89 + app/src/mg/svc/MG_ERRESCAL.pgc | 149 + app/src/mg/svc/MG_ERRLIST.pgc | 149 + app/src/mg/svc/MG_ERRMSG.pgc | 149 + app/src/mg/svc/MG_ERRPURGE.pgc | 149 + app/src/mg/svc/MG_ERRREQUEUE.pgc | 149 + app/src/mg/svc/MG_ERRRETRY.pgc | 149 + app/src/mg/svc/MG_FEP.pgc | 89 + app/src/mg/svc/MG_FIELDGET.pgc | 89 + app/src/mg/svc/MG_FIELDSET.pgc | 89 + app/src/mg/svc/MG_FINADV.pgc | 89 + app/src/mg/svc/MG_FINADVRSP.pgc | 89 + app/src/mg/svc/MG_FINREQ.pgc | 89 + app/src/mg/svc/MG_FINRSP.pgc | 89 + app/src/mg/svc/MG_FROMISO.pgc | 149 + app/src/mg/svc/MG_GAPSCAN.pgc | 149 + app/src/mg/svc/MG_HEALTH.pgc | 149 + app/src/mg/svc/MG_HOSTADPT.pgc | 89 + app/src/mg/svc/MG_INFLIGHT.pgc | 149 + app/src/mg/svc/MG_INQRSP.pgc | 89 + app/src/mg/svc/MG_ISO8583.pgc | 149 + app/src/mg/svc/MG_ISOADPT.pgc | 89 + app/src/mg/svc/MG_KEYCHECK.pgc | 149 + app/src/mg/svc/MG_KEYEXCH.pgc | 149 + app/src/mg/svc/MG_KEYEXPIRE.pgc | 149 + app/src/mg/svc/MG_KEYLOAD.pgc | 149 + app/src/mg/svc/MG_KEYROTATE.pgc | 149 + app/src/mg/svc/MG_KEYSTATUS.pgc | 149 + app/src/mg/svc/MG_LOADBAL.pgc | 149 + app/src/mg/svc/MG_LOG.pgc | 149 + app/src/mg/svc/MG_MAC.pgc | 149 + app/src/mg/svc/MG_MACGEN.pgc | 149 + app/src/mg/svc/MG_MACSESSION.pgc | 149 + app/src/mg/svc/MG_MACVERIFY.pgc | 149 + app/src/mg/svc/MG_NACK.pgc | 149 + app/src/mg/svc/MG_NETMGMT.pgc | 149 + app/src/mg/svc/MG_NMADVICE.pgc | 89 + app/src/mg/svc/MG_NMRSP.pgc | 89 + app/src/mg/svc/MG_PACK.pgc | 149 + app/src/mg/svc/MG_PANMASK.pgc | 149 + app/src/mg/svc/MG_PARSE.pgc | 149 + app/src/mg/svc/MG_PINBLOCK.pgc | 89 + app/src/mg/svc/MG_PREAUTH.pgc | 89 + app/src/mg/svc/MG_QDEPTH.pgc | 149 + app/src/mg/svc/MG_QUEUE.pgc | 149 + app/src/mg/svc/MG_RATELIMIT.pgc | 149 + app/src/mg/svc/MG_READYZ.pgc | 149 + app/src/mg/svc/MG_REASM.pgc | 149 + app/src/mg/svc/MG_RECV.pgc | 149 + app/src/mg/svc/MG_RELAYADPT.pgc | 89 + app/src/mg/svc/MG_RESEND.pgc | 149 + app/src/mg/svc/MG_RESNDONE.pgc | 149 + app/src/mg/svc/MG_RETRY.pgc | 149 + app/src/mg/svc/MG_REVADVICE.pgc | 149 + app/src/mg/svc/MG_REVADVRSP.pgc | 89 + app/src/mg/svc/MG_REVERSAL.pgc | 149 + app/src/mg/svc/MG_REVRSP.pgc | 89 + app/src/mg/svc/MG_ROUTE.pgc | 149 + app/src/mg/svc/MG_RTADD.pgc | 149 + app/src/mg/svc/MG_RTCOUNT.pgc | 149 + app/src/mg/svc/MG_RTDISABLE.pgc | 149 + app/src/mg/svc/MG_RTDROP.pgc | 149 + app/src/mg/svc/MG_RTENABLE.pgc | 149 + app/src/mg/svc/MG_RTFIND.pgc | 149 + app/src/mg/svc/MG_RTLIST.pgc | 149 + app/src/mg/svc/MG_RTSWITCH.pgc | 149 + app/src/mg/svc/MG_SEND.pgc | 149 + app/src/mg/svc/MG_SEQAUDIT.pgc | 149 + app/src/mg/svc/MG_SEQFILL.pgc | 149 + app/src/mg/svc/MG_SEQNEXT.pgc | 149 + app/src/mg/svc/MG_SEQRESET.pgc | 149 + app/src/mg/svc/MG_SESSION.pgc | 149 + app/src/mg/svc/MG_SETTLEREQ.pgc | 149 + app/src/mg/svc/MG_SETTLERSP.pgc | 149 + app/src/mg/svc/MG_SIGNOFF.pgc | 149 + app/src/mg/svc/MG_SIGNON.pgc | 149 + app/src/mg/svc/MG_STALEMARK.pgc | 149 + app/src/mg/svc/MG_STAN.pgc | 149 + app/src/mg/svc/MG_STATAMT.pgc | 149 + app/src/mg/svc/MG_STATCHAN.pgc | 149 + app/src/mg/svc/MG_STATDIR.pgc | 149 + app/src/mg/svc/MG_STATERR.pgc | 149 + app/src/mg/svc/MG_STATHOUR.pgc | 149 + app/src/mg/svc/MG_STATMTI.pgc | 149 + app/src/mg/svc/MG_STATPEAK.pgc | 149 + app/src/mg/svc/MG_STATRC.pgc | 149 + app/src/mg/svc/MG_STATS.pgc | 149 + app/src/mg/svc/MG_STATSUM.pgc | 149 + app/src/mg/svc/MG_STATUS.pgc | 149 + app/src/mg/svc/MG_THROTTLE.pgc | 149 + app/src/mg/svc/MG_TIMEOUT.pgc | 149 + app/src/mg/svc/MG_TIMEOUTSET.pgc | 149 + app/src/mg/svc/MG_TOISO.pgc | 149 + app/src/mg/svc/MG_TPSCHECK.pgc | 149 + app/src/mg/svc/MG_UNPACK.pgc | 149 + app/src/mg/svc/MG_VAN1ADPT.pgc | 149 + app/src/mg/svc/MG_VAN2ADPT.pgc | 149 + app/src/mg/svc/MG_VAN3ADPT.pgc | 89 + app/src/mg/svc/MG_VAN4ADPT.pgc | 89 + app/src/mm/svc/MM_ACCT_INQ.pgc | 114 + app/src/mm/svc/MM_ACCT_MASK.pgc | 114 + app/src/mm/svc/MM_ACCT_REG.pgc | 114 + app/src/mm/svc/MM_ACCT_VERIFY.pgc | 114 + app/src/mm/svc/MM_ACCT_VRF_HIST.pgc | 114 + app/src/mm/svc/MM_ACCT_VRF_STAT.pgc | 114 + app/src/mm/svc/MM_AGENT_INQ.pgc | 95 + app/src/mm/svc/MM_AGENT_REG.pgc | 95 + app/src/mm/svc/MM_AGENT_STATUS.pgc | 95 + app/src/mm/svc/MM_APPLY_APPROVE.pgc | 114 + app/src/mm/svc/MM_APPLY_ASSIGN.pgc | 114 + app/src/mm/svc/MM_APPLY_CNT.pgc | 114 + app/src/mm/svc/MM_APPLY_DOCCHK.pgc | 114 + app/src/mm/svc/MM_APPLY_HOLD.pgc | 114 + app/src/mm/svc/MM_APPLY_REG.pgc | 114 + app/src/mm/svc/MM_APPLY_REJECT.pgc | 114 + app/src/mm/svc/MM_APPLY_RESUME.pgc | 114 + app/src/mm/svc/MM_APPLY_REVIEW.pgc | 114 + app/src/mm/svc/MM_APPLY_SCORE.pgc | 114 + app/src/mm/svc/MM_APPLY_STATUS.pgc | 114 + app/src/mm/svc/MM_APPLY_WITHDRAW.pgc | 114 + app/src/mm/svc/MM_AUDIT_DIFF.pgc | 114 + app/src/mm/svc/MM_AUDIT_PURGE.pgc | 114 + app/src/mm/svc/MM_AUDIT_RECENT.pgc | 114 + app/src/mm/svc/MM_AUDIT_STAT.pgc | 114 + app/src/mm/svc/MM_AUDIT_TRAIL.pgc | 114 + app/src/mm/svc/MM_BIN_INQ.pgc | 93 + app/src/mm/svc/MM_BIN_REG.pgc | 93 + app/src/mm/svc/MM_BIZNO_CHECK.pgc | 114 + app/src/mm/svc/MM_BIZNO_UPD.pgc | 114 + app/src/mm/svc/MM_BIZ_CLOSE.pgc | 114 + app/src/mm/svc/MM_BIZ_REOPEN.pgc | 114 + app/src/mm/svc/MM_BIZ_SUSPEND.pgc | 114 + app/src/mm/svc/MM_BRANCH_INQ.pgc | 95 + app/src/mm/svc/MM_BRANCH_LIST.pgc | 95 + app/src/mm/svc/MM_BRANCH_REG.pgc | 95 + app/src/mm/svc/MM_CHK_ACCT.pgc | 114 + app/src/mm/svc/MM_CHK_FEERATE.pgc | 114 + app/src/mm/svc/MM_CHK_GRADE.pgc | 114 + app/src/mm/svc/MM_CHK_LIMIT.pgc | 114 + app/src/mm/svc/MM_CHK_MCC.pgc | 114 + app/src/mm/svc/MM_CHK_ORPHAN.pgc | 114 + app/src/mm/svc/MM_CHK_TERM.pgc | 114 + app/src/mm/svc/MM_CODEMAP_DEL.pgc | 95 + app/src/mm/svc/MM_CODEMAP_GET.pgc | 95 + app/src/mm/svc/MM_CODEMAP_SET.pgc | 95 + app/src/mm/svc/MM_CODE_CACHE.pgc | 95 + app/src/mm/svc/MM_CODE_INQ.pgc | 95 + app/src/mm/svc/MM_CODE_REG.pgc | 95 + app/src/mm/svc/MM_CONFIRM.pgc | 114 + app/src/mm/svc/MM_CONTRACT_AMEND.pgc | 114 + app/src/mm/svc/MM_CONTRACT_END.pgc | 114 + app/src/mm/svc/MM_CONTRACT_EXPCHK.pgc | 114 + app/src/mm/svc/MM_CONTRACT_HIST.pgc | 114 + app/src/mm/svc/MM_CONTRACT_INQ.pgc | 114 + app/src/mm/svc/MM_CONTRACT_NEW.pgc | 114 + app/src/mm/svc/MM_CONTRACT_REACT.pgc | 114 + app/src/mm/svc/MM_CONTRACT_RENEW.pgc | 114 + app/src/mm/svc/MM_CONTRACT_SUSPEND.pgc | 114 + app/src/mm/svc/MM_FEERATE_HIST.pgc | 114 + app/src/mm/svc/MM_FEERATE_INQ.pgc | 114 + app/src/mm/svc/MM_FEERATE_SET.pgc | 114 + app/src/mm/svc/MM_FX_INQ.pgc | 93 + app/src/mm/svc/MM_FX_REG.pgc | 93 + app/src/mm/svc/MM_GRADE_ADJUST.pgc | 114 + app/src/mm/svc/MM_GRADE_DIST.pgc | 114 + app/src/mm/svc/MM_GRADE_HIST.pgc | 114 + app/src/mm/svc/MM_GRADE_PREVIEW.pgc | 114 + app/src/mm/svc/MM_HOLIDAY_REG.pgc | 93 + app/src/mm/svc/MM_ISSUER_INQ.pgc | 93 + app/src/mm/svc/MM_ISSUER_REG.pgc | 93 + app/src/mm/svc/MM_LIMIT_INQ.pgc | 114 + app/src/mm/svc/MM_LIMIT_SET.pgc | 114 + app/src/mm/svc/MM_MCC_ASSIGN.pgc | 98 + app/src/mm/svc/MM_MCC_CNT.pgc | 98 + app/src/mm/svc/MM_MCC_DEL.pgc | 98 + app/src/mm/svc/MM_MCC_INQ.pgc | 98 + app/src/mm/svc/MM_MCC_REG.pgc | 98 + app/src/mm/svc/MM_MCC_RISKLIST.pgc | 98 + app/src/mm/svc/MM_MCC_RISKSET.pgc | 98 + app/src/mm/svc/MM_MCC_UPD.pgc | 98 + app/src/mm/svc/MM_MERCH_ACCT.pgc | 114 + app/src/mm/svc/MM_MERCH_BULK.pgc | 114 + app/src/mm/svc/MM_MERCH_CLOSE.pgc | 114 + app/src/mm/svc/MM_MERCH_DEL.pgc | 114 + app/src/mm/svc/MM_MERCH_DORMANT.pgc | 114 + app/src/mm/svc/MM_MERCH_GRADE.pgc | 114 + app/src/mm/svc/MM_MERCH_INQ.pgc | 114 + app/src/mm/svc/MM_MERCH_PAUSE.pgc | 114 + app/src/mm/svc/MM_MERCH_REG.pgc | 114 + app/src/mm/svc/MM_MERCH_RESUME.pgc | 114 + app/src/mm/svc/MM_MERCH_STATUS.pgc | 114 + app/src/mm/svc/MM_MERCH_SUM.pgc | 114 + app/src/mm/svc/MM_MERCH_UPD.pgc | 114 + app/src/mm/svc/MM_PARTNER_INQ.pgc | 95 + app/src/mm/svc/MM_PARTNER_LIST.pgc | 95 + app/src/mm/svc/MM_PARTNER_REG.pgc | 95 + app/src/mm/svc/MM_PARTNER_STATUS.pgc | 95 + app/src/mm/svc/MM_PARTNER_UPD.pgc | 95 + app/src/mm/svc/MM_RATE_APPROVE.pgc | 114 + app/src/mm/svc/MM_RATE_CANCEL.pgc | 114 + app/src/mm/svc/MM_RATE_HIST.pgc | 114 + app/src/mm/svc/MM_RATE_PENDING.pgc | 114 + app/src/mm/svc/MM_RATE_REJECT.pgc | 114 + app/src/mm/svc/MM_RATE_REQ.pgc | 114 + app/src/mm/svc/MM_RATE_STATUS.pgc | 114 + app/src/mm/svc/MM_REL_CHILDREN.pgc | 114 + app/src/mm/svc/MM_REL_LINK.pgc | 114 + app/src/mm/svc/MM_REL_LIST.pgc | 114 + app/src/mm/svc/MM_REL_MOVE.pgc | 114 + app/src/mm/svc/MM_REL_PARENT.pgc | 114 + app/src/mm/svc/MM_REL_UNLINK.pgc | 114 + app/src/mm/svc/MM_REL_VERIFY.pgc | 114 + app/src/mm/svc/MM_RISK_INQ.pgc | 114 + app/src/mm/svc/MM_RISK_SCORE.pgc | 114 + app/src/mm/svc/MM_RISK_SET.pgc | 114 + app/src/mm/svc/MM_STATUS.pgc | 114 + app/src/mm/svc/MM_TERMINAL_INQ.pgc | 106 + app/src/mm/svc/MM_TERMINAL_REG.pgc | 106 + app/src/mm/svc/MM_TERM_ACTIVATE.pgc | 106 + app/src/mm/svc/MM_TERM_CNT.pgc | 106 + app/src/mm/svc/MM_TERM_DEACT.pgc | 106 + app/src/mm/svc/MM_TERM_HIST.pgc | 106 + app/src/mm/svc/MM_TERM_INSTALL.pgc | 106 + app/src/mm/svc/MM_TERM_LIST.pgc | 106 + app/src/mm/svc/MM_TERM_MODEL_UPD.pgc | 106 + app/src/mm/svc/MM_TERM_MOVE.pgc | 106 + app/src/mm/svc/MM_TERM_REASSIGN.pgc | 106 + app/src/mm/svc/MM_TERM_RETIRE.pgc | 106 + app/src/mm/svc/MM_TERM_SCRAP.pgc | 106 + app/src/mm/svc/MM_TERM_SUSPEND.pgc | 106 + app/src/mm/svc/MM_TERM_SWAP.pgc | 106 + app/src/mm/svc/MM_VALIDATE.pgc | 114 + app/src/py/svc/PAY_ACCTCHANGE.pgc | 163 +- app/src/py/svc/PAY_ACCTCHK.pgc | 136 +- app/src/py/svc/PAY_ACCTFREEZE.pgc | 160 +- app/src/py/svc/PAY_ACCTLIST.pgc | 146 +- app/src/py/svc/PAY_ACCTREOPEN.pgc | 162 +- app/src/py/svc/PAY_ACCTVERIFY.pgc | 149 +- app/src/py/svc/PAY_ADJUST.pgc | 190 +- app/src/py/svc/PAY_ADVANCE.pgc | 226 +- app/src/py/svc/PAY_ADVCALC.pgc | 145 +- app/src/py/svc/PAY_ADVGRANT.pgc | 153 +- app/src/py/svc/PAY_ADVREPAY.pgc | 175 +- app/src/py/svc/PAY_APPRBULK.pgc | 126 +- app/src/py/svc/PAY_APPRCANCEL.pgc | 151 +- app/src/py/svc/PAY_APPRHIST.pgc | 141 +- app/src/py/svc/PAY_APPRLIMIT.pgc | 161 +- app/src/py/svc/PAY_APPROVE.pgc | 150 +- app/src/py/svc/PAY_APPRPEND.pgc | 138 +- app/src/py/svc/PAY_APPRREQ.pgc | 146 +- app/src/py/svc/PAY_BANKFILE.pgc | 186 +- app/src/py/svc/PAY_BANKVERIFY.pgc | 131 +- app/src/py/svc/PAY_BULK.pgc | 234 +- app/src/py/svc/PAY_CANCEL.pgc | 179 +- app/src/py/svc/PAY_CANCELHIST.pgc | 147 +- app/src/py/svc/PAY_CARRYOVER.pgc | 173 +- app/src/py/svc/PAY_CHARGEBACK.pgc | 186 +- app/src/py/svc/PAY_CLOSE.pgc | 136 +- app/src/py/svc/PAY_CLOSEDSTOP.pgc | 144 +- app/src/py/svc/PAY_COMBINE.pgc | 194 +- app/src/py/svc/PAY_CONFIRM.pgc | 143 +- app/src/py/svc/PAY_DORMANT.pgc | 133 +- app/src/py/svc/PAY_DORMLIST.pgc | 143 +- app/src/py/svc/PAY_DORMSTOP.pgc | 181 +- app/src/py/svc/PAY_FAILCLEAR.pgc | 167 +- app/src/py/svc/PAY_FAILRATE.pgc | 146 +- app/src/py/svc/PAY_FAILRSN.pgc | 140 +- app/src/py/svc/PAY_FAILSTAT.pgc | 141 +- app/src/py/svc/PAY_FAILTOP.pgc | 136 +- app/src/py/svc/PAY_FEECAP.pgc | 200 +- app/src/py/svc/PAY_FEEDED.pgc | 181 +- app/src/py/svc/PAY_FEEFIXED.pgc | 183 +- app/src/py/svc/PAY_FEEMIN.pgc | 140 +- app/src/py/svc/PAY_FEERATE.pgc | 173 +- app/src/py/svc/PAY_FEEREFUND.pgc | 182 +- app/src/py/svc/PAY_FEESUM.pgc | 132 +- app/src/py/svc/PAY_FEEWAIVE.pgc | 188 +- app/src/py/svc/PAY_FILECANCEL.pgc | 141 +- app/src/py/svc/PAY_FILECONF.pgc | 155 +- app/src/py/svc/PAY_FILEGEN.pgc | 167 +- app/src/py/svc/PAY_FILEREBUILD.pgc | 142 +- app/src/py/svc/PAY_FILESTAT.pgc | 138 +- app/src/py/svc/PAY_FILEVERIFY.pgc | 132 +- app/src/py/svc/PAY_GARNISH.pgc | 173 +- app/src/py/svc/PAY_HISTORY.pgc | 135 +- app/src/py/svc/PAY_HOLD.pgc | 145 +- app/src/py/svc/PAY_HOLDAGING.pgc | 134 +- app/src/py/svc/PAY_HOLDBYRSN.pgc | 140 +- app/src/py/svc/PAY_HOLDCLEAR.pgc | 142 +- app/src/py/svc/PAY_HOLDEXT.pgc | 154 +- app/src/py/svc/PAY_HOLDLIST.pgc | 140 +- app/src/py/svc/PAY_HOLDNOTE.pgc | 168 +- app/src/py/svc/PAY_HOLDREASON.pgc | 159 +- app/src/py/svc/PAY_INQUIRY.pgc | 154 +- app/src/py/svc/PAY_INT.pgc | 116 +- app/src/py/svc/PAY_LEDGERSUM.pgc | 144 +- app/src/py/svc/PAY_LIMITCHK.pgc | 148 +- app/src/py/svc/PAY_LIMITHOLD.pgc | 156 +- app/src/py/svc/PAY_LIMITLIST.pgc | 141 +- app/src/py/svc/PAY_LIMITSET.pgc | 143 +- app/src/py/svc/PAY_LIMITWARN.pgc | 149 +- app/src/py/svc/PAY_MERCHSUM.pgc | 147 +- app/src/py/svc/PAY_MERGE.pgc | 194 +- app/src/py/svc/PAY_NETOFF.pgc | 176 +- app/src/py/svc/PAY_NETRECALC.pgc | 146 +- app/src/py/svc/PAY_NOTIFYALL.pgc | 136 +- app/src/py/svc/PAY_NOTIFYEMAIL.pgc | 137 +- app/src/py/svc/PAY_NOTIFYFAIL.pgc | 130 +- app/src/py/svc/PAY_NOTIFYKAKAO.pgc | 122 +- app/src/py/svc/PAY_NOTIFYPLAN.pgc | 123 +- app/src/py/svc/PAY_NOTIFYPUSH.pgc | 133 +- app/src/py/svc/PAY_NOTIFYRESEND.pgc | 153 +- app/src/py/svc/PAY_NOTIFYSMS.pgc | 128 +- app/src/py/svc/PAY_OFFSETAPPLY.pgc | 196 +- app/src/py/svc/PAY_PENDLIST.pgc | 140 +- app/src/py/svc/PAY_REACTIVATE.pgc | 153 +- app/src/py/svc/PAY_RECLAIM.pgc | 184 +- app/src/py/svc/PAY_RECON.pgc | 123 +- app/src/py/svc/PAY_REFUND.pgc | 181 +- app/src/py/svc/PAY_REJECT.pgc | 162 +- app/src/py/svc/PAY_RELEASE.pgc | 151 +- app/src/py/svc/PAY_REMIT.pgc | 148 +- app/src/py/svc/PAY_REPAYACCT.pgc | 198 +- app/src/py/svc/PAY_REPAYNEW.pgc | 190 +- app/src/py/svc/PAY_REPROC.pgc | 155 +- app/src/py/svc/PAY_REQUEUE.pgc | 135 +- app/src/py/svc/PAY_RESULT.pgc | 147 +- app/src/py/svc/PAY_RESVLIST.pgc | 141 +- app/src/py/svc/PAY_RESVMODIFY.pgc | 143 +- app/src/py/svc/PAY_RETRY.pgc | 151 +- app/src/py/svc/PAY_RETRYONE.pgc | 156 +- app/src/py/svc/PAY_REVERSE.pgc | 172 +- app/src/py/svc/PAY_ROLLFWD.pgc | 158 +- app/src/py/svc/PAY_SCHEDCANCEL.pgc | 121 +- app/src/py/svc/PAY_SCHEDULE.pgc | 148 +- app/src/py/svc/PAY_SEIZE.pgc | 173 +- app/src/py/svc/PAY_SEIZEAMT.pgc | 152 +- app/src/py/svc/PAY_SEIZECHK.pgc | 144 +- app/src/py/svc/PAY_SEIZELIST.pgc | 136 +- app/src/py/svc/PAY_SEIZEREL.pgc | 162 +- app/src/py/svc/PAY_SPLIT.pgc | 235 +- app/src/py/svc/PAY_SPLITEVEN.pgc | 198 +- app/src/py/svc/PAY_SPLITRATIO.pgc | 198 +- app/src/py/svc/PAY_STATAMT.pgc | 141 +- app/src/py/svc/PAY_STATBANK.pgc | 141 +- app/src/py/svc/PAY_STATCHAN.pgc | 146 +- app/src/py/svc/PAY_STATDAILY.pgc | 153 +- app/src/py/svc/PAY_STATEMENT.pgc | 151 +- app/src/py/svc/PAY_STATFEE.pgc | 140 +- app/src/py/svc/PAY_STATMONTH.pgc | 152 +- app/src/py/svc/PAY_STATRANGE.pgc | 137 +- app/src/py/svc/PAY_STATSTATUS.pgc | 141 +- app/src/py/svc/PAY_STATUS.pgc | 138 +- app/src/py/svc/PAY_SUMMARY.pgc | 147 +- app/src/py/svc/PAY_URGENT.pgc | 243 +- app/src/py/svc/PAY_URGENTQ.pgc | 152 +- app/src/py/svc/PAY_VOIDONE.pgc | 164 +- app/src/py/svc/PAY_XFERCONF.pgc | 148 +- app/src/py/svc/PAY_XFERDIFF.pgc | 142 +- app/src/py/svc/PAY_XFERFILE.pgc | 177 +- app/src/py/svc/PAY_XFERLINK.pgc | 154 +- app/src/py/svc/PAY_XFERMATCH.pgc | 136 +- app/src/py/svc/PAY_XFERMISS.pgc | 122 +- app/src/py/svc/PAY_XFERTYPE.pgc | 140 +- app/src/py/svc/PAY_XFERUNMATCH.pgc | 140 +- app/src/rc/svc/RC_3WAY.pgc | 162 +- app/src/rc/svc/RC_4WAY.pgc | 169 +- app/src/rc/svc/RC_ACQMATCH.pgc | 137 +- app/src/rc/svc/RC_ACQ_DEP.pgc | 163 +- app/src/rc/svc/RC_AGEING.pgc | 139 +- app/src/rc/svc/RC_ALERT.pgc | 158 +- app/src/rc/svc/RC_AMTDIFF.pgc | 159 +- app/src/rc/svc/RC_APPRGAP.pgc | 146 +- app/src/rc/svc/RC_APPRMATCH.pgc | 137 +- app/src/rc/svc/RC_APPR_ACQ.pgc | 164 +- app/src/rc/svc/RC_AUDITADD.pgc | 152 +- app/src/rc/svc/RC_AUDITLOG.pgc | 157 +- app/src/rc/svc/RC_AUDITTRAIL.pgc | 139 +- app/src/rc/svc/RC_AUTOFIX.pgc | 157 +- app/src/rc/svc/RC_BACKDATE.pgc | 158 +- app/src/rc/svc/RC_BRANDDIFF.pgc | 135 +- app/src/rc/svc/RC_BRANDRECON.pgc | 134 +- app/src/rc/svc/RC_BYCATEGORY.pgc | 143 +- app/src/rc/svc/RC_BYCHANNEL.pgc | 145 +- app/src/rc/svc/RC_BYDATE.pgc | 130 +- app/src/rc/svc/RC_BYISSUER.pgc | 132 +- app/src/rc/svc/RC_BYMERCH.pgc | 143 +- app/src/rc/svc/RC_BYMGROUP.pgc | 144 +- app/src/rc/svc/RC_CANCELREC.pgc | 163 +- app/src/rc/svc/RC_CANCMATCH.pgc | 130 +- app/src/rc/svc/RC_CARRYFWD.pgc | 149 +- app/src/rc/svc/RC_CARRYIN.pgc | 138 +- app/src/rc/svc/RC_CARRYOVER.pgc | 144 +- app/src/rc/svc/RC_CHANDIFF.pgc | 148 +- app/src/rc/svc/RC_CHANRECON.pgc | 143 +- app/src/rc/svc/RC_CLOSEDAY.pgc | 152 +- app/src/rc/svc/RC_CNTDIFF.pgc | 134 +- app/src/rc/svc/RC_CONFIRM.pgc | 134 +- app/src/rc/svc/RC_DELAY.pgc | 158 +- app/src/rc/svc/RC_DEPGAP.pgc | 134 +- app/src/rc/svc/RC_DEPMATCH.pgc | 163 +- app/src/rc/svc/RC_DEPPAY.pgc | 137 +- app/src/rc/svc/RC_DIFFCAUSE.pgc | 142 +- app/src/rc/svc/RC_DIFFRANK.pgc | 132 +- app/src/rc/svc/RC_DIFFRPT.pgc | 130 +- app/src/rc/svc/RC_DIFFTREND.pgc | 134 +- app/src/rc/svc/RC_DUPCHK.pgc | 147 +- app/src/rc/svc/RC_EDCRECON.pgc | 133 +- app/src/rc/svc/RC_EDIRECON.pgc | 162 +- app/src/rc/svc/RC_ESCALATE.pgc | 139 +- app/src/rc/svc/RC_EXCEPTION.pgc | 156 +- app/src/rc/svc/RC_EXCNOTIFY.pgc | 143 +- app/src/rc/svc/RC_FEEBRK.pgc | 133 +- app/src/rc/svc/RC_FEEDIFF.pgc | 141 +- app/src/rc/svc/RC_FEERECON.pgc | 173 +- app/src/rc/svc/RC_FIXDUP.pgc | 132 +- app/src/rc/svc/RC_FIXFEE.pgc | 155 +- app/src/rc/svc/RC_FIXFX.pgc | 155 +- app/src/rc/svc/RC_FIXROUND.pgc | 141 +- app/src/rc/svc/RC_FIXSIGN.pgc | 132 +- app/src/rc/svc/RC_FXDIFF.pgc | 138 +- app/src/rc/svc/RC_FXRATECHK.pgc | 143 +- app/src/rc/svc/RC_FXRECON.pgc | 176 +- app/src/rc/svc/RC_GAPSCAN.pgc | 140 +- app/src/rc/svc/RC_HOLD.pgc | 151 +- app/src/rc/svc/RC_HOLDAGE.pgc | 136 +- app/src/rc/svc/RC_HOLDCLR.pgc | 157 +- app/src/rc/svc/RC_HOLDLIST.pgc | 130 +- app/src/rc/svc/RC_HOLDRSN.pgc | 141 +- app/src/rc/svc/RC_HOLDSET.pgc | 159 +- app/src/rc/svc/RC_INSTRECON.pgc | 165 +- app/src/rc/svc/RC_ISSDIFF.pgc | 146 +- app/src/rc/svc/RC_ISSRECON.pgc | 142 +- app/src/rc/svc/RC_KINDSPLIT.pgc | 137 +- app/src/rc/svc/RC_LEDGERBAL.pgc | 142 +- app/src/rc/svc/RC_LEDGERLINK.pgc | 152 +- app/src/rc/svc/RC_LIMITCHK.pgc | 159 +- app/src/rc/svc/RC_MANAPPRV.pgc | 141 +- app/src/rc/svc/RC_MANCANCEL.pgc | 155 +- app/src/rc/svc/RC_MANLIST.pgc | 135 +- app/src/rc/svc/RC_MANNG.pgc | 155 +- app/src/rc/svc/RC_MANOK.pgc | 155 +- app/src/rc/svc/RC_MANREG.pgc | 162 +- app/src/rc/svc/RC_MANREJECT.pgc | 140 +- app/src/rc/svc/RC_MANREQ.pgc | 154 +- app/src/rc/svc/RC_MANUAL.pgc | 158 +- app/src/rc/svc/RC_MGRPDIFF.pgc | 145 +- app/src/rc/svc/RC_MGRPRECON.pgc | 136 +- app/src/rc/svc/RC_NOTIFY.pgc | 143 +- app/src/rc/svc/RC_ORPHAN.pgc | 135 +- app/src/rc/svc/RC_OVERAMT.pgc | 132 +- app/src/rc/svc/RC_PARTACQ.pgc | 176 +- app/src/rc/svc/RC_PARTDEP.pgc | 130 +- app/src/rc/svc/RC_PARTIAL.pgc | 158 +- app/src/rc/svc/RC_PAYGAP.pgc | 135 +- app/src/rc/svc/RC_PAYLEDGER.pgc | 137 +- app/src/rc/svc/RC_PAYMATCH.pgc | 173 +- app/src/rc/svc/RC_POSRECON.pgc | 135 +- app/src/rc/svc/RC_PROGRESS.pgc | 127 +- app/src/rc/svc/RC_RATEBYISS.pgc | 139 +- app/src/rc/svc/RC_RATECHAN.pgc | 142 +- app/src/rc/svc/RC_RATEDAY.pgc | 131 +- app/src/rc/svc/RC_RATEMERCH.pgc | 149 +- app/src/rc/svc/RC_RATEMON.pgc | 135 +- app/src/rc/svc/RC_RATESTAT.pgc | 133 +- app/src/rc/svc/RC_RATEWEEK.pgc | 135 +- app/src/rc/svc/RC_RECORR.pgc | 158 +- app/src/rc/svc/RC_REDIFF.pgc | 158 +- app/src/rc/svc/RC_REFULL.pgc | 135 +- app/src/rc/svc/RC_REFUNDREC.pgc | 130 +- app/src/rc/svc/RC_RELATE.pgc | 134 +- app/src/rc/svc/RC_RELEASE.pgc | 134 +- app/src/rc/svc/RC_REMATCH.pgc | 151 +- app/src/rc/svc/RC_REOPEN.pgc | 136 +- app/src/rc/svc/RC_REPROC.pgc | 134 +- app/src/rc/svc/RC_RERUN.pgc | 145 +- app/src/rc/svc/RC_RETRO.pgc | 173 +- app/src/rc/svc/RC_RETURN.pgc | 159 +- app/src/rc/svc/RC_ROLLBACK.pgc | 134 +- app/src/rc/svc/RC_ROUNDDIFF.pgc | 130 +- app/src/rc/svc/RC_SIGNDIFF.pgc | 131 +- app/src/rc/svc/RC_SNAPSHOT.pgc | 139 +- app/src/rc/svc/RC_STATS.pgc | 133 +- app/src/rc/svc/RC_STATUS.pgc | 129 +- app/src/rc/svc/RC_SUMCHK.pgc | 133 +- app/src/rc/svc/RC_SUMMARY.pgc | 135 +- app/src/rc/svc/RC_TAXRECON.pgc | 175 +- app/src/rc/svc/RC_THRESHOLD.pgc | 147 +- app/src/rc/svc/RC_TIMEDIFF.pgc | 131 +- app/src/rc/svc/RC_TOLERANCE.pgc | 159 +- app/src/rc/svc/RC_TOPDIFF.pgc | 134 +- app/src/rc/svc/RC_TRAILQRY.pgc | 152 +- app/src/rc/svc/RC_UNDERAMT.pgc | 132 +- app/src/rc/svc/RC_UNMATCH.pgc | 129 +- app/src/rc/svc/RC_VOIDREC.pgc | 164 +- app/src/rc/svc/RC_WHTRECON.pgc | 161 +- app/src/rc/svc/RC_ZEROAMT.pgc | 136 +- app/src/rc/svc/RECONCILE.pgc | 176 +- app/src/st/svc/ST_ADJHIST.pgc | 149 +- app/src/st/svc/ST_ADJREVERSE.pgc | 138 +- app/src/st/svc/ST_ADJUST.pgc | 142 +- app/src/st/svc/ST_ADVANCE.pgc | 142 +- app/src/st/svc/ST_APPEALSTAT.pgc | 136 +- app/src/st/svc/ST_APPROVE.pgc | 150 +- app/src/st/svc/ST_APPROVEALL.pgc | 141 +- app/src/st/svc/ST_BADDEBT.pgc | 144 +- app/src/st/svc/ST_BANDRATE.pgc | 139 +- app/src/st/svc/ST_BANDSIM.pgc | 151 +- app/src/st/svc/ST_BIWEEKLY.pgc | 148 +- app/src/st/svc/ST_BYMERCH.pgc | 136 +- app/src/st/svc/ST_CANCEL.pgc | 150 +- app/src/st/svc/ST_CAP.pgc | 139 +- app/src/st/svc/ST_CAPCHECK.pgc | 143 +- app/src/st/svc/ST_CARRYOVER.pgc | 148 +- app/src/st/svc/ST_CASHBACK.pgc | 146 +- app/src/st/svc/ST_CHAINSET.pgc | 149 +- app/src/st/svc/ST_CHARGEBACK.pgc | 147 +- app/src/st/svc/ST_CHNLSTAT.pgc | 151 +- app/src/st/svc/ST_COLLATERAL.pgc | 137 +- app/src/st/svc/ST_CONFIRM.pgc | 140 +- app/src/st/svc/ST_COUPONADJ.pgc | 138 +- app/src/st/svc/ST_CXLRECALC.pgc | 154 +- app/src/st/svc/ST_CYCLED1.pgc | 146 +- app/src/st/svc/ST_CYCLED2.pgc | 148 +- app/src/st/svc/ST_CYCLED3.pgc | 148 +- app/src/st/svc/ST_CYCLEMON.pgc | 150 +- app/src/st/svc/ST_CYCLEQTR.pgc | 145 +- app/src/st/svc/ST_CYCLESET.pgc | 147 +- app/src/st/svc/ST_CYCLEWEEK.pgc | 147 +- app/src/st/svc/ST_DAILYCLOSE.pgc | 142 +- app/src/st/svc/ST_DEBTCOLLECT.pgc | 151 +- app/src/st/svc/ST_DEBTNET.pgc | 140 +- app/src/st/svc/ST_DELAYINT.pgc | 153 +- app/src/st/svc/ST_DEPOSITCUT.pgc | 141 +- app/src/st/svc/ST_DEPOSITSET.pgc | 136 +- app/src/st/svc/ST_DIFFCHK.pgc | 150 +- app/src/st/svc/ST_DISPUTE.pgc | 153 +- app/src/st/svc/ST_DISPUTERSV.pgc | 149 +- app/src/st/svc/ST_DTLSUM.pgc | 142 +- app/src/st/svc/ST_EDIRECON.pgc | 140 +- app/src/st/svc/ST_EVENTFEE.pgc | 153 +- app/src/st/svc/ST_FEEPREVIEW.pgc | 144 +- app/src/st/svc/ST_FEERATIO.pgc | 146 +- app/src/st/svc/ST_FLATFEE.pgc | 152 +- app/src/st/svc/ST_FLATVSPCT.pgc | 157 +- app/src/st/svc/ST_FXCONV.pgc | 150 +- app/src/st/svc/ST_FXSETTLE.pgc | 155 +- app/src/st/svc/ST_GRADESTAT.pgc | 144 +- app/src/st/svc/ST_GROWTHINC.pgc | 148 +- app/src/st/svc/ST_GRPALLOC.pgc | 163 +- app/src/st/svc/ST_GRPROLLUP.pgc | 147 +- app/src/st/svc/ST_GRPSETTLE.pgc | 159 +- app/src/st/svc/ST_HOLD.pgc | 135 +- app/src/st/svc/ST_HOLDAMT.pgc | 145 +- app/src/st/svc/ST_INSTANTPAY.pgc | 144 +- app/src/st/svc/ST_ISSUERSTAT.pgc | 148 +- app/src/st/svc/ST_LEDGERLINK.pgc | 128 +- app/src/st/svc/ST_LEDGERSNAP.pgc | 139 +- app/src/st/svc/ST_LIMITCHK.pgc | 139 +- app/src/st/svc/ST_LOCKBATCH.pgc | 144 +- app/src/st/svc/ST_LOYALTY.pgc | 140 +- app/src/st/svc/ST_MERCHRATE.pgc | 154 +- app/src/st/svc/ST_MINFEE.pgc | 156 +- app/src/st/svc/ST_MKTGSHARE.pgc | 152 +- app/src/st/svc/ST_MONTHLY.pgc | 140 +- app/src/st/svc/ST_MONTHSTAT.pgc | 143 +- app/src/st/svc/ST_NEGSWEEP.pgc | 151 +- app/src/st/svc/ST_NETTING.pgc | 134 +- app/src/st/svc/ST_NEXTPAYDT.pgc | 142 +- app/src/st/svc/ST_OFFSET.pgc | 134 +- app/src/st/svc/ST_PARTIAL.pgc | 157 +- app/src/st/svc/ST_PARTNERSET.pgc | 147 +- app/src/st/svc/ST_PARTPAY.pgc | 146 +- app/src/st/svc/ST_PAYDATE.pgc | 140 +- app/src/st/svc/ST_PAYRETRY.pgc | 143 +- app/src/st/svc/ST_PAYSTATUS.pgc | 142 +- app/src/st/svc/ST_PCTFEE.pgc | 155 +- app/src/st/svc/ST_PRECONFIRM.pgc | 142 +- app/src/st/svc/ST_PROMOAPPLY.pgc | 139 +- app/src/st/svc/ST_PROMOCALC.pgc | 158 +- app/src/st/svc/ST_RATEAPPLY.pgc | 156 +- app/src/st/svc/ST_RATECARD.pgc | 139 +- app/src/st/svc/ST_RATECMP.pgc | 144 +- app/src/st/svc/ST_RATEFLOOR.pgc | 156 +- app/src/st/svc/ST_RATEMAX.pgc | 156 +- app/src/st/svc/ST_RATESIM.pgc | 150 +- app/src/st/svc/ST_REBATE.pgc | 138 +- app/src/st/svc/ST_RECALC.pgc | 142 +- app/src/st/svc/ST_RECVAGE.pgc | 139 +- app/src/st/svc/ST_RECVOFFSET.pgc | 143 +- app/src/st/svc/ST_RECVSTAT.pgc | 148 +- app/src/st/svc/ST_REFERRAL.pgc | 144 +- app/src/st/svc/ST_REFUNDCALC.pgc | 137 +- app/src/st/svc/ST_RELAYFEE.pgc | 154 +- app/src/st/svc/ST_RELAYSPLIT.pgc | 157 +- app/src/st/svc/ST_RELEASE.pgc | 135 +- app/src/st/svc/ST_REOPEN.pgc | 145 +- app/src/st/svc/ST_REPARTIAL.pgc | 150 +- app/src/st/svc/ST_REPORT.pgc | 135 +- app/src/st/svc/ST_RERUN.pgc | 144 +- app/src/st/svc/ST_RETRORATE.pgc | 153 +- app/src/st/svc/ST_ROUNDING.pgc | 155 +- app/src/st/svc/ST_SLABFEE.pgc | 167 +- app/src/st/svc/ST_SPLIT.pgc | 143 +- app/src/st/svc/ST_SPLITEVEN.pgc | 148 +- app/src/st/svc/ST_STATDETAIL.pgc | 152 +- app/src/st/svc/ST_STATS.pgc | 136 +- app/src/st/svc/ST_STATUS.pgc | 138 +- app/src/st/svc/ST_SUPPLYCALC.pgc | 153 +- app/src/st/svc/ST_TAXINVCHK.pgc | 139 +- app/src/st/svc/ST_TAXSUMMARY.pgc | 141 +- app/src/st/svc/ST_TIERFEE.pgc | 158 +- app/src/st/svc/ST_TIERINCENT.pgc | 159 +- app/src/st/svc/ST_TOPMERCH.pgc | 137 +- app/src/st/svc/ST_UNLOCK.pgc | 144 +- app/src/st/svc/ST_UNPAIDSUM.pgc | 138 +- app/src/st/svc/ST_VANDETAIL.pgc | 139 +- app/src/st/svc/ST_VANFEE.pgc | 154 +- app/src/st/svc/ST_VAT.pgc | 150 +- app/src/st/svc/ST_VATADJUST.pgc | 154 +- app/src/st/svc/ST_VATDETAIL.pgc | 138 +- app/src/st/svc/ST_VATREPORT.pgc | 147 +- app/src/st/svc/ST_VOIDRECALC.pgc | 149 +- app/src/st/svc/ST_VOLREBATE.pgc | 144 +- app/src/st/svc/ST_WHT.pgc | 151 +- app/src/st/svc/ST_WHTDETAIL.pgc | 138 +- app/src/st/svc/ST_WHTREPORT.pgc | 147 +- app/src/st/svc/ST_WORKFLOW.pgc | 155 +- app/src/st/svc/ST_XFERLINK.pgc | 130 +- app/src/vl/svc/VL_AGEDCONF.pgc | 110 + app/src/vl/svc/VL_AGEDOPEN.pgc | 116 + app/src/vl/svc/VL_AMOUNT.pgc | 118 + app/src/vl/svc/VL_ANOMALY.pgc | 144 + app/src/vl/svc/VL_APPRAGG.pgc | 113 + app/src/vl/svc/VL_APPRAMT.pgc | 144 + app/src/vl/svc/VL_APPRLATE.pgc | 137 + app/src/vl/svc/VL_APPRNOPUR.pgc | 117 + app/src/vl/svc/VL_APPR_ACQ.pgc | 138 + app/src/vl/svc/VL_AXLEDGDC.pgc | 113 + app/src/vl/svc/VL_AXPURAPPR.pgc | 113 + app/src/vl/svc/VL_AXPURSETT.pgc | 113 + app/src/vl/svc/VL_AXSETTPAY.pgc | 113 + app/src/vl/svc/VL_BALANCE.pgc | 137 + app/src/vl/svc/VL_BATCH_CHK.pgc | 113 + app/src/vl/svc/VL_BIN.pgc | 112 + app/src/vl/svc/VL_CANCELMIS.pgc | 144 + app/src/vl/svc/VL_CANCELRATE.pgc | 112 + app/src/vl/svc/VL_CARD.pgc | 112 + app/src/vl/svc/VL_CATSKEW.pgc | 112 + app/src/vl/svc/VL_CHANSPLIT.pgc | 112 + app/src/vl/svc/VL_CODEVAL.pgc | 114 + app/src/vl/svc/VL_CONFIRM.pgc | 117 + app/src/vl/svc/VL_CROSSCHK.pgc | 138 + app/src/vl/svc/VL_DATE.pgc | 113 + app/src/vl/svc/VL_DATEFUT.pgc | 117 + app/src/vl/svc/VL_DATEGAP.pgc | 112 + app/src/vl/svc/VL_DATEREV.pgc | 138 + app/src/vl/svc/VL_DAYLIMIT.pgc | 137 + app/src/vl/svc/VL_DUP.pgc | 143 + app/src/vl/svc/VL_DUPAPPR.pgc | 139 + app/src/vl/svc/VL_DUPAUTH.pgc | 138 + app/src/vl/svc/VL_DUPLEDGR.pgc | 138 + app/src/vl/svc/VL_DUPPAY.pgc | 139 + app/src/vl/svc/VL_DUPPAYDAY.pgc | 113 + app/src/vl/svc/VL_DUPPURCH.pgc | 138 + app/src/vl/svc/VL_DUPSETTLE.pgc | 143 + app/src/vl/svc/VL_DUPSETTMR.pgc | 137 + app/src/vl/svc/VL_EXPIRY.pgc | 113 + app/src/vl/svc/VL_FEE.pgc | 138 + app/src/vl/svc/VL_FEEADJNET.pgc | 138 + app/src/vl/svc/VL_FEEHIGH.pgc | 137 + app/src/vl/svc/VL_FEERECALC.pgc | 137 + app/src/vl/svc/VL_FORMAT.pgc | 136 + app/src/vl/svc/VL_FXCHK.pgc | 137 + app/src/vl/svc/VL_INSTCHK.pgc | 137 + app/src/vl/svc/VL_ISSUERMIX.pgc | 112 + app/src/vl/svc/VL_LEDGBAL.pgc | 113 + app/src/vl/svc/VL_LEDGDATE.pgc | 143 + app/src/vl/svc/VL_LEDGENTRY.pgc | 144 + app/src/vl/svc/VL_LEDGER.pgc | 139 + app/src/vl/svc/VL_LIMIT.pgc | 142 + app/src/vl/svc/VL_LIMITNEAR.pgc | 136 + app/src/vl/svc/VL_LIMITPCT.pgc | 136 + app/src/vl/svc/VL_LONGOPEN.pgc | 116 + app/src/vl/svc/VL_MDRRATE.pgc | 112 + app/src/vl/svc/VL_MERCH.pgc | 136 + app/src/vl/svc/VL_NEGBAL.pgc | 113 + app/src/vl/svc/VL_NEGFEE.pgc | 118 + app/src/vl/svc/VL_NEGNET.pgc | 113 + app/src/vl/svc/VL_NIGHTTXN.pgc | 112 + app/src/vl/svc/VL_OUTLIER.pgc | 112 + app/src/vl/svc/VL_OVERLIMIT.pgc | 113 + app/src/vl/svc/VL_PURNOAPPR.pgc | 118 + app/src/vl/svc/VL_RATEDEV.pgc | 137 + app/src/vl/svc/VL_REFINTEG.pgc | 138 + app/src/vl/svc/VL_REPORT.pgc | 113 + app/src/vl/svc/VL_REQUIRED.pgc | 142 + app/src/vl/svc/VL_RIAPPR.pgc | 112 + app/src/vl/svc/VL_RICANCEL.pgc | 113 + app/src/vl/svc/VL_RIFX.pgc | 112 + app/src/vl/svc/VL_RIINSTALL.pgc | 112 + app/src/vl/svc/VL_RILEDGER.pgc | 113 + app/src/vl/svc/VL_RIMERCH.pgc | 118 + app/src/vl/svc/VL_RIPAY.pgc | 117 + app/src/vl/svc/VL_RIPURCH.pgc | 118 + app/src/vl/svc/VL_RISETTLE.pgc | 139 + app/src/vl/svc/VL_RITAXINV.pgc | 112 + app/src/vl/svc/VL_ROUNDAMT.pgc | 112 + app/src/vl/svc/VL_RULE.pgc | 113 + app/src/vl/svc/VL_RULECOUNT.pgc | 114 + app/src/vl/svc/VL_RULEFIND.pgc | 114 + app/src/vl/svc/VL_RULEHIST.pgc | 119 + app/src/vl/svc/VL_RULEMOD.pgc | 114 + app/src/vl/svc/VL_RULEOFF.pgc | 114 + app/src/vl/svc/VL_RULEON.pgc | 114 + app/src/vl/svc/VL_RULEREG.pgc | 113 + app/src/vl/svc/VL_RULERENAME.pgc | 114 + app/src/vl/svc/VL_RULESEV.pgc | 114 + app/src/vl/svc/VL_RULESTAT.pgc | 114 + app/src/vl/svc/VL_RULEVER.pgc | 113 + app/src/vl/svc/VL_SALES.pgc | 139 + app/src/vl/svc/VL_SETTLE.pgc | 138 + app/src/vl/svc/VL_SETTLREV.pgc | 113 + app/src/vl/svc/VL_SPIKEAMT.pgc | 112 + app/src/vl/svc/VL_STATMIX.pgc | 112 + app/src/vl/svc/VL_STATUS.pgc | 112 + app/src/vl/svc/VL_STATUS_CHK.pgc | 138 + app/src/vl/svc/VL_SUMFEE.pgc | 137 + app/src/vl/svc/VL_SUMFEECHK.pgc | 113 + app/src/vl/svc/VL_SUMGROSS.pgc | 137 + app/src/vl/svc/VL_SUMMARY.pgc | 113 + app/src/vl/svc/VL_SUMNETCHK.pgc | 137 + app/src/vl/svc/VL_SUMRECON.pgc | 137 + app/src/vl/svc/VL_SUMTXNCNT.pgc | 137 + app/src/vl/svc/VL_SUSPACK.pgc | 117 + app/src/vl/svc/VL_SUSPACT.pgc | 144 + app/src/vl/svc/VL_SUSPAGEAVG.pgc | 111 + app/src/vl/svc/VL_SUSPASSIGN.pgc | 117 + app/src/vl/svc/VL_SUSPBULK.pgc | 113 + app/src/vl/svc/VL_SUSPESCAL.pgc | 117 + app/src/vl/svc/VL_SUSPFINAL.pgc | 117 + app/src/vl/svc/VL_SUSPNOTE.pgc | 117 + app/src/vl/svc/VL_SUSPRECHK.pgc | 117 + app/src/vl/svc/VL_SUSPREOPEN.pgc | 117 + app/src/vl/svc/VL_SUSPSTAT.pgc | 113 + app/src/vl/svc/VL_SUSPTRIAGE.pgc | 117 + app/src/vl/svc/VL_THRAUDIT.pgc | 114 + app/src/vl/svc/VL_THRBUMP.pgc | 114 + app/src/vl/svc/VL_THRCOMPARE.pgc | 113 + app/src/vl/svc/VL_THRESHOLD.pgc | 120 + app/src/vl/svc/VL_THRGET.pgc | 114 + app/src/vl/svc/VL_THRRESET.pgc | 114 + app/src/vl/svc/VL_THRSCALE.pgc | 113 + app/src/vl/svc/VL_THRSET.pgc | 113 + app/src/vl/svc/VL_TOLERANCE.pgc | 137 + app/src/vl/svc/VL_TRIAGG.pgc | 113 + app/src/vl/svc/VL_TRISTAGE.pgc | 138 + app/src/vl/svc/VL_TXNREV.pgc | 144 + app/src/vl/svc/VL_UNACQ.pgc | 117 + app/src/vl/svc/VL_UNACQAGE.pgc | 116 + app/src/vl/svc/VL_VELOCITY.pgc | 112 + app/src/vl/svc/VL_ZERONET.pgc | 113 + app/ui/index.html | 14903 ++++++++++++++++++----- app/ui/screens.js | 14903 ++++++++++++++++++----- 1463 files changed, 220278 insertions(+), 20405 deletions(-) diff --git a/app/src/ac/svc/ACQUIRE.pgc b/app/src/ac/svc/ACQUIRE.pgc index 987e19a..9e98a24 100644 --- a/app/src/ac/svc/ACQUIRE.pgc +++ b/app/src/ac/svc/ACQUIRE.pgc @@ -1,33 +1,136 @@ /* * ACQUIRE.pgc - ac 모듈 서비스 ACQUIRE (one service = one file). - * Moved verbatim from the former inline ac_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by ac_svr at tpsvrinit. + * 매입 접수(원팁): 가맹점/금액 검증 -> MDR 정책 조회 -> 금액구간·한도 규칙 판정 + * -> purchase/approval 생성 -> RECONCILE 체인 호출 -> 자기 브랜치에서 정산완료(S). + * 입출력 계약 보존: 입력 T_MERCHANT/T_AMOUNT/T_BIZDATE, + * 출력 T_PURCHASE_ID/T_FEE/T_NET/T_STATUS (+ RECONCILE 체인). + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQUIRE.h" void ACQUIRE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; + const char *SVC = "ACQUIRE"; + char merch[64], bizdate[16], logmsg[256]; long amount, bps = 25, fee, net, pid, rlen = 0; + long grade = 0, ratio = 0; + EXEC SQL BEGIN DECLARE SECTION; + long h_lim = 0, h_sum = 0, h_cnt = 0, h_openc = 0, h_logid = 0, h_gross = 0; + long h_amt = 0, h_fee = 0, h_net = 0, h_pid = 0; + char h_merch[64], h_bizdate[16], h_status[8], h_name[64], h_logmsg[256]; + EXEC SQL END DECLARE SECTION; + userlog("%s 진입: 신규 매입 접수 요청", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 */ 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 (merch[0] == 0) { + userlog("%s: 가맹점 ID 누락", SVC); + Bchg(b, T_MSG, 0, "가맹점 ID 누락", 0L); setl(b, T_RC, 101); FAIL(b); + } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + amount = getl(b, T_AMOUNT); + if (amount <= 0) { + Bchg(b, T_MSG, 0, "매입 금액 오류", 0L); setl(b, T_RC, 104); FAIL(b); + } + if (amount > 100000000L) { + Bchg(b, T_MSG, 0, "단건 매입 상한 초과", 0L); setl(b, T_RC, 112); FAIL(b); + } + h_amt = amount; + + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { + Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); + } + userlog("%s 컨텍스트: merch=%s amount=%ld bizdate=%s", SVC, h_merch, amount, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 가맹점 마스터 + MDR + 당일 누적 */ + EXEC SQL SELECT name, status, daily_limit INTO :h_name, :h_status, :h_lim + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 조회 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { + Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); + } if (acdb_merchant_mdr(merch, &bps) < 0) bps = 25; /* keep 25bps default */ + + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + count(*) FILTER (WHERE status IN ('A','M')) + INTO :h_cnt, :h_sum, :h_openc FROM purchase + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate + AND status NOT IN ('C','V'); + if (sqlca.sqlcode < 0) { + userlog("%s: 당일누적 조회 실패 [%d]", SVC, sqlca.sqlcode); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 가맹점 상태 / 한도 / 금액 구간 (3+ 분기) */ + if (strcmp(h_status, "ACTIVE") != 0) { + userlog("%s 거절: 가맹점 %s 상태=%s", SVC, h_merch, h_status); + Bchg(b, T_MSG, 0, "비활성 가맹점", 0L); setl(b, T_RC, 121); FAIL(b); + } + if (h_lim > 0 && h_sum + amount > h_lim) { + userlog("%s 거절: 한도초과 누적=%ld 요청=%ld 한도=%ld", SVC, h_sum, amount, h_lim); + Bchg(b, T_MSG, 0, "가맹점 일한도 초과", 0L); setl(b, T_RC, 113); FAIL(b); + } + if (amount >= 10000000L) { + grade = 3; Bchg(b, T_MSG, 0, "고액 매입 접수", 0L); + } else if (amount >= 1000000L) { + grade = 2; Bchg(b, T_MSG, 0, "중액 매입 접수", 0L); + } else { + grade = 1; Bchg(b, T_MSG, 0, "소액 매입 접수", 0L); + } + ratio = (h_lim > 0) ? ((h_sum + amount) * 10000L / h_lim) : 0; + + /* 4) 본처리부: 수수료 산출 후 매입/승인 원장 생성 */ fee = acq_fee(amount, bps); net = amount - fee; + h_fee = fee; h_net = net; 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); + if (pid < 0) { Bchg(b, T_MSG, 0, "매입 채번 실패", 0L); setl(b, T_RC, 93); FAIL(b); } + if (acdb_insert_purchase(pid, merch, amount, fee, net, "A", "POS", h_bizdate) < 0) { + Bchg(b, T_MSG, 0, "매입 생성 실패", 0L); setl(b, T_RC, 94); FAIL(b); + } + if (acdb_insert_approval(pid, 1) < 0) { + Bchg(b, T_MSG, 0, "승인 적재 실패", 0L); setl(b, T_RC, 95); FAIL(b); + } + h_pid = pid; + /* 5) 이력/감사 적재부 */ + snprintf(logmsg, sizeof(logmsg), "매입접수 pid=%ld merch=%s 금액=%ld 수수료=%ld 등급=%ld", + pid, h_merch, amount, fee, grade); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { userlog("%s: 로그채번 실패", SVC); setl(b, T_RC, 90); FAIL(b); } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQUIRE', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("%s: 로그적재 실패", SVC); setl(b, T_RC, 90); FAIL(b); } + + /* 6) 집계/요약 갱신부: 가맹점 일별 요약 재적재 */ + 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) { userlog("%s: 요약 재집계 실패", SVC); setl(b, T_RC, 90); FAIL(b); } + if (acdb_upsert_merch_sum(merch, h_bizdate, h_gross, h_fee, h_net, h_cnt) < 0) + userlog("%s 주의: 가맹점 요약 갱신 스킵", SVC); + + /* 8) 응답부 (계약 보존: PID/FEE/NET/STATUS) */ setl(b, T_PURCHASE_ID, pid); setl(b, T_FEE, fee); setl(b, T_NET, net); + setl(b, T_ARG1, grade); + setl(b, T_ARG2, ratio); + setl(b, T_COUNT, h_cnt); Bchg(b, T_STATUS, 0, "A", 0L); userlog("ACQUIRE pid=%ld merch=%s amount=%ld fee=%ld net=%ld -> RECONCILE", @@ -36,10 +139,20 @@ void ACQUIRE(TPSVCINFO *p) /* 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)); + /* 7) 예외/보상부: 체인 실패는 같은 XA 브랜치라 TPFAIL 로 전체 롤백 */ + Bchg(b, T_MSG, 0, "대사 체인 실패", 0L); + setl(b, T_RC, 114); FAIL(b); } /* our branch resumes: settle-complete the purchase we own */ - if (acdb_update_status(pid, "S") < 0) FAIL(b); + if (acdb_update_status(pid, "S") < 0) { + userlog("%s: 정산완료 전이 실패 pid=%ld", SVC, pid); + Bchg(b, T_MSG, 0, "정산완료 전이 실패", 0L); + setl(b, T_RC, 96); + FAIL(b); + } Bchg(b, T_STATUS, 0, "S", 0L); + setl(b, T_RC, 0); + userlog("%s 완료: pid=%ld 상태=S 수수료=%ld 정산=%ld", SVC, pid, fee, net); OK(b); } diff --git a/app/src/ac/svc/ACQ_ALERTAMT.pgc b/app/src/ac/svc/ACQ_ALERTAMT.pgc index 6e605c3..a490372 100644 --- a/app/src/ac/svc/ACQ_ALERTAMT.pgc +++ b/app/src/ac/svc/ACQ_ALERTAMT.pgc @@ -1,7 +1,7 @@ /* * ACQ_ALERTAMT.pgc - ac 모듈 서비스 ACQ_ALERTAMT (one service = one file). - * 매입 금액초과 알림: 가맹점의 영업일 매입 중 임계금액(T_AMOUNT)을 초과한 - * 고액 건수와 최고 매입액을 조회해 경보 여부를 판정한다. + * 매입 금액초과 알림: 가맹점 영업일 매입 중 임계금액(T_AMOUNT)을 초과한 + * 고액 건수와 최고 매입액을 조회해 경보 등급을 판정한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_ALERTAMT.h" @@ -9,22 +9,159 @@ void ACQ_ALERTAMT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_ALERTAMT"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_lim, h_over = 0, h_max = 0; - char h_merch[64], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - h_lim = getl(b, T_AMOUNT); if (h_lim <= 0) h_lim = 1000000; - gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL SELECT count(*) FILTER (WHERE amount > :h_lim), coalesce(max(amount),0) - INTO :h_over, :h_max FROM purchase - WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); - setl(b, T_COUNT, h_over); - setl(b, T_AMT1, h_max); - setl(b, T_RC, h_over > 0 ? 1 : 0); + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + if (merch[0] == 0) { Bchg(b, T_MSG, 0, "가맹점 ID 누락", 0L); setl(b, T_RC, 101); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + threshold = getl(b, T_AMOUNT); if (threshold <= 0) threshold = 1000000; + h_amt = threshold; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } + + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + EXEC SQL SELECT name, status, daily_limit INTO :h_str1, :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (strcmp(h_status, "ACTIVE") != 0) + userlog("%s 주의: 가맹점 %s 상태=%s", SVC, h_merch, h_status); + /* 가맹점 MDR(bps) 및 상태분포 컨텍스트 로드 */ + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + EXEC SQL SELECT count(*) FILTER (WHERE status IN ('A','M')), count(*) FILTER (WHERE status = 'S') INTO :h_openc, :h_setc FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 상태분포 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산 진행 컨텍스트(기 정산 순액) */ + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_ctx FROM settlement WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*), coalesce(max(amount),0), coalesce(sum(amount),0) INTO :h_cnt, :h_max, :h_gross FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 총매입집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_a FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate AND amount > :h_amt; + if (sqlca.sqlcode < 0) { + userlog("%s: 고액집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 임계 초과 건의 최고액 배수로 경보 등급을 산정한다 */ + if (h_a <= 0) { h_val = 0; } + else if (h_max >= h_amt * 3) { h_val = 3; userlog("%s 위험: 최고액 %ld", SVC, h_max); } + else if (h_max >= h_amt * 2) { h_val = 2; } + else { h_val = 1; } + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :h_val2 FROM merchant_summary WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 누적집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_pdgross FROM purchase WHERE merchant_id = :h_merch AND biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_val2 > 0 && h_gross > 0) ratio = (h_gross * 10000L) / (h_val2 + h_gross); + else ratio = 0; + h_share = (h_pdgross > 0) ? (h_gross * 100L / h_pdgross) : 0; + userlog("%s 추이: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_pdgross, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "고액경보 merch=%s 임계=%ld 초과=%ld 등급=%ld", h_merch, h_amt, h_a, h_val); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_ALERTAMT', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_ALERTAMT' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_COUNT, h_a); + setl(b, T_AMOUNT, h_max); + setl(b, T_GROSS, h_gross); + setl(b, T_ARG1, h_val); + setl(b, T_ARG2, ratio); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_ALERTCHK.pgc b/app/src/ac/svc/ACQ_ALERTCHK.pgc index f87f850..e2f3647 100644 --- a/app/src/ac/svc/ACQ_ALERTCHK.pgc +++ b/app/src/ac/svc/ACQ_ALERTCHK.pgc @@ -1,7 +1,7 @@ /* * ACQ_ALERTCHK.pgc - ac 모듈 서비스 ACQ_ALERTCHK (one service = one file). - * 매입 알림 점검: 가맹점의 당일 고액거래(임계금액 이상)와 보류 적체 건수를 - * 점검해 경보 여부와 알림 메시지를 만들어 돌려준다. + * 매입 알림 점검: 가맹점의 당일 고액거래와 보류 적체 건수를 점검해 + * 경보 여부와 알림 메시지를 만들어 돌려준다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_ALERTCHK.h" @@ -9,35 +9,169 @@ void ACQ_ALERTCHK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long threshold; - char msg[160]; + const char *SVC = "ACQ_ALERTCHK"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_thr, h_big = 0, h_hold = 0; - char h_merch[64], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - threshold = getl(b, T_AMT1); - if (threshold <= 0) threshold = 1000000L; /* 기본 임계: 100만원 */ - h_thr = threshold; - gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL SELECT count(*) FILTER (WHERE amount >= :h_thr), - count(*) FILTER (WHERE status = 'H') - INTO :h_big, :h_hold - FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) { userlog("ACQ_ALERTCHK FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); - if (h_big > 0 || h_hold > 0) { - snprintf(msg, sizeof(msg), "ALERT merchant=%s big=%ld hold=%ld thr=%ld", - h_merch, h_big, h_hold, threshold); - Bchg(b, T_MSG, 0, msg, 0L); - setl(b, T_RC, 1); /* 경보 */ - } else { - Bchg(b, T_MSG, 0, "CLEAR", 0L); - setl(b, T_RC, 0); /* 정상 */ + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + if (merch[0] == 0) { Bchg(b, T_MSG, 0, "가맹점 ID 누락", 0L); setl(b, T_RC, 101); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + threshold = getl(b, T_AMOUNT); if (threshold <= 0) threshold = 500000; + h_amt = threshold; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); } - setl(b, T_ARG1, h_big); - setl(b, T_ARG2, h_hold); + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } + + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + EXEC SQL SELECT name, status, daily_limit INTO :h_str1, :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (strcmp(h_status, "ACTIVE") != 0) + userlog("%s 주의: 가맹점 %s 상태=%s", SVC, h_merch, h_status); + /* 가맹점 MDR(bps) 및 상태분포 컨텍스트 로드 */ + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + EXEC SQL SELECT count(*) FILTER (WHERE status IN ('A','M')), count(*) FILTER (WHERE status = 'S') INTO :h_openc, :h_setc FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 상태분포 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산 진행 컨텍스트(기 정산 순액) */ + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_ctx FROM settlement WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*) INTO :h_a FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate AND amount >= :h_amt; + if (sqlca.sqlcode < 0) { + userlog("%s: 고액건수 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_b, :h_sum FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate AND status = 'H'; + if (sqlca.sqlcode < 0) { + userlog("%s: 보류적체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_gross FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 전체실적 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 고액/보류 조합으로 알림 메시지와 등급을 결정 */ + if (h_a > 0 && h_b > 0) { + h_val = 3; Bchg(b, T_MSG, 0, "고액거래 및 보류 적체 동시 발생", 0L); + } else if (h_b >= 5) { + h_val = 2; Bchg(b, T_MSG, 0, "보류 적체 과다", 0L); + } else if (h_a > 0) { + h_val = 1; Bchg(b, T_MSG, 0, "고액거래 발생", 0L); + } else { + h_val = 0; Bchg(b, T_MSG, 0, "정상", 0L); + } + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :h_val2 FROM merchant_summary WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 누적집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_pdgross FROM purchase WHERE merchant_id = :h_merch AND biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_val2 > 0 && h_gross > 0) ratio = (h_gross * 10000L) / (h_val2 + h_gross); + else ratio = 0; + h_share = (h_pdgross > 0) ? (h_gross * 100L / h_pdgross) : 0; + userlog("%s 추이: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_pdgross, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "알림점검 merch=%s 고액=%ld 보류=%ld 등급=%ld", h_merch, h_a, h_b, h_val); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_ALERTCHK', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_ALERTCHK' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_ARG1, h_a); + setl(b, T_ARG2, h_b); + setl(b, T_ARG3, h_val); + setl(b, T_AMT1, h_sum); + setl(b, T_COUNT, h_cnt); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_ALERTDUP.pgc b/app/src/ac/svc/ACQ_ALERTDUP.pgc index 8730ce3..c5c414b 100644 --- a/app/src/ac/svc/ACQ_ALERTDUP.pgc +++ b/app/src/ac/svc/ACQ_ALERTDUP.pgc @@ -9,20 +9,151 @@ void ACQ_ALERTDUP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_ALERTDUP"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_grp = 0; - char h_merch[64], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL SELECT count(*) INTO :h_grp FROM ( - SELECT amount FROM purchase WHERE merchant_id = :h_merch - AND biz_date = :h_bizdate GROUP BY amount HAVING count(*) > 1) d; - if (sqlca.sqlcode < 0) FAIL(b); + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); - setl(b, T_COUNT, h_grp); - setl(b, T_RC, h_grp > 0 ? 1 : 0); + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + if (merch[0] == 0) { Bchg(b, T_MSG, 0, "가맹점 ID 누락", 0L); setl(b, T_RC, 101); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } + + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + EXEC SQL SELECT name, status, daily_limit INTO :h_str1, :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (strcmp(h_status, "ACTIVE") != 0) + userlog("%s 주의: 가맹점 %s 상태=%s", SVC, h_merch, h_status); + /* 가맹점 MDR(bps) 및 상태분포 컨텍스트 로드 */ + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + EXEC SQL SELECT count(*) FILTER (WHERE status IN ('A','M')), count(*) FILTER (WHERE status = 'S') INTO :h_openc, :h_setc FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 상태분포 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산 진행 컨텍스트(기 정산 순액) */ + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_ctx FROM settlement WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*) INTO :h_a FROM (SELECT amount FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate GROUP BY amount HAVING count(*) > 1) g; + if (sqlca.sqlcode < 0) { + userlog("%s: 중복그룹 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(sum(c),0), coalesce(max(c),0) INTO :h_b, :h_max FROM (SELECT count(*) AS c FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate GROUP BY amount HAVING count(*) > 1) g2; + if (sqlca.sqlcode < 0) { + userlog("%s: 중복건수 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_gross FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 모집단 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 중복 그룹 비중으로 의심 등급 산정 */ + if (h_a <= 0) { h_val = 0; } + else if (h_max >= 5) { h_val = 3; } + else if (h_a >= 3) { h_val = 2; } + else { h_val = 1; } + if (h_cnt > 0) ratio = (h_b * 10000L) / h_cnt; else ratio = 0; + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 중복 연루 비중(bps)을 맥락 지표로 사용 */ + h_share = ratio; + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "중복경보 merch=%s 그룹=%ld 연루=%ld 등급=%ld", h_merch, h_a, h_b, h_val); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_ALERTDUP', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_ALERTDUP' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_COUNT, h_a); + setl(b, T_ARG1, h_b); + setl(b, T_ARG2, h_max); + setl(b, T_ARG3, h_val); + setl(b, T_ARG4, ratio); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_ALERTQRY.pgc b/app/src/ac/svc/ACQ_ALERTQRY.pgc index 94d9142..4fee7b2 100644 --- a/app/src/ac/svc/ACQ_ALERTQRY.pgc +++ b/app/src/ac/svc/ACQ_ALERTQRY.pgc @@ -9,22 +9,146 @@ void ACQ_ALERTQRY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_ALERTQRY"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_vel = 0, h_lim = 0, h_dup = 0; - char h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL SELECT count(*) FILTER (WHERE reason LIKE 'VEL%'), - count(*) FILTER (WHERE reason LIKE 'LIMIT%'), - count(*) FILTER (WHERE reason LIKE 'DUP%') - INTO :h_vel, :h_lim, :h_dup FROM ac_unmatch WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); - setl(b, T_ARG1, h_vel); - setl(b, T_ARG2, h_lim); - setl(b, T_ARG3, h_dup); - setl(b, T_COUNT, h_vel + h_lim + h_dup); + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } + + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*) FILTER (WHERE reason LIKE 'VEL%'), count(*) FILTER (WHERE reason LIKE 'LIMIT%'), count(*) FILTER (WHERE reason LIKE 'DUP%'), count(*) INTO :h_a, :h_b, :h_c, :h_cnt FROM ac_unmatch WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 유형분해 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(count(DISTINCT purchase_id),0) INTO :h_d FROM ac_unmatch WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 대상매입 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 최다 유형을 판별해 우선 대응 유형으로 표시 */ + if (h_a >= h_b && h_a >= h_c) { h_top = 1; } + else if (h_b >= h_a && h_b >= h_c) { h_top = 2; } + else if (h_c > 0) { h_top = 3; } + else { h_top = 0; } + h_e = h_cnt - (h_a + h_b + h_c); + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "알림유형 bizdate=%s 속도=%ld 한도=%ld 중복=%ld 기타=%ld", h_bizdate, h_a, h_b, h_c, h_e); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_ALERTQRY', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_ALERTQRY' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_ARG1, h_a); + setl(b, T_ARG2, h_b); + setl(b, T_ARG3, h_c); + setl(b, T_ARG4, h_e); + setl(b, T_ARG5, h_top); + setl(b, T_COUNT, h_cnt); + setl(b, T_ID1, h_d); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_ALERTVEL.pgc b/app/src/ac/svc/ACQ_ALERTVEL.pgc index 869738c..9058829 100644 --- a/app/src/ac/svc/ACQ_ALERTVEL.pgc +++ b/app/src/ac/svc/ACQ_ALERTVEL.pgc @@ -9,22 +9,165 @@ void ACQ_ALERTVEL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long allow; + const char *SVC = "ACQ_ALERTVEL"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_5m = 0, h_30m = 0; - char h_chan[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - allow = getl(b, T_ARG1); if (allow <= 0) allow = 50; - gets_(b, T_CHANNEL, h_chan, sizeof(h_chan)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL SELECT count(*) FILTER (WHERE created_at >= now() - interval '5 minutes'), - count(*) FILTER (WHERE created_at >= now() - interval '30 minutes') - INTO :h_5m, :h_30m FROM purchase WHERE channel = :h_chan; - if (sqlca.sqlcode < 0) FAIL(b); + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); - setl(b, T_ARG2, h_5m); - setl(b, T_ARG3, h_30m); - setl(b, T_RC, h_5m > allow ? 1 : 0); + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + if (merch[0] == 0) { Bchg(b, T_MSG, 0, "가맹점 ID 누락", 0L); setl(b, T_RC, 101); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + gets_(b, T_CHANNEL, channel, sizeof(channel)); if (channel[0] == 0) strcpy(channel, "POS"); + strncpy(h_chan, channel, sizeof(h_chan)-1); h_chan[sizeof(h_chan)-1] = 0; + threshold = getl(b, T_ARG1); if (threshold <= 0) threshold = 30; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } + + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + EXEC SQL SELECT name, status, daily_limit INTO :h_str1, :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (strcmp(h_status, "ACTIVE") != 0) + userlog("%s 주의: 가맹점 %s 상태=%s", SVC, h_merch, h_status); + /* 가맹점 MDR(bps) 및 상태분포 컨텍스트 로드 */ + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + EXEC SQL SELECT count(*) FILTER (WHERE status IN ('A','M')), count(*) FILTER (WHERE status = 'S') INTO :h_openc, :h_setc FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 상태분포 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산 진행 컨텍스트(기 정산 순액) */ + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_ctx FROM settlement WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*) INTO :h_a FROM purchase WHERE merchant_id = :h_merch AND channel = :h_chan AND created_at >= now() - interval '5 minutes'; + if (sqlca.sqlcode < 0) { + userlog("%s: 5분집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_b FROM purchase WHERE merchant_id = :h_merch AND channel = :h_chan AND created_at >= now() - interval '30 minutes'; + if (sqlca.sqlcode < 0) { + userlog("%s: 30분집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_gross FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 당일실적 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + h_cnt = (h_cnt > 0) ? h_cnt : h_a; + /* 분당 환산 후 허용치 대비 경보 등급 */ + ratio = h_a / 5; + if (ratio > threshold) { h_val = 2; userlog("%s 경보: 분당 %ld 건", SVC, ratio); } + else if (ratio > threshold / 2) { h_val = 1; } + else { h_val = 0; } + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :h_val2 FROM merchant_summary WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 누적집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_pdgross FROM purchase WHERE merchant_id = :h_merch AND biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_val2 > 0 && h_gross > 0) ratio = (h_gross * 10000L) / (h_val2 + h_gross); + else ratio = 0; + h_share = (h_pdgross > 0) ? (h_gross * 100L / h_pdgross) : 0; + userlog("%s 추이: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_pdgross, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "속도경보 merch=%s ch=%s 5m=%ld 30m=%ld 등급=%ld", h_merch, h_chan, h_a, h_b, h_val); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_ALERTVEL', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_ALERTVEL' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_COUNT, h_a); + setl(b, T_ARG2, h_b); + setl(b, T_ARG3, ratio); + setl(b, T_ARG1, h_val); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_AMTFIX.pgc b/app/src/ac/svc/ACQ_AMTFIX.pgc index af82085..af24df1 100644 --- a/app/src/ac/svc/ACQ_AMTFIX.pgc +++ b/app/src/ac/svc/ACQ_AMTFIX.pgc @@ -1,6 +1,7 @@ /* * ACQ_AMTFIX.pgc - ac 모듈 서비스 ACQ_AMTFIX (one service = one file). - * 금액 확정 정정: 직전 금액 조회 후 하우스 25bps 로 절대 재계산, 증감(delta) 회신. + * 금액 확정 정정: 직전 금액을 조회한 뒤 하우스 기준 MDR 로 수수료를 절대 + * 재계산하고 증감(delta)을 회신한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_AMTFIX.h" @@ -8,24 +9,144 @@ void ACQ_AMTFIX(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long amount, fee, net; + const char *SVC = "ACQ_AMTFIX"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_old = 0, h_amount, h_fee, h_net; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 매입 금액/수수료 조정 요청", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ h_pid = getl(b, T_PURCHASE_ID); - amount = getl(b, T_AMOUNT); - /* absolute fix: read prior amount, recompute fee at flat house 25bps (no MDR lookup) */ - EXEC SQL SELECT amount INTO :h_old FROM purchase WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - fee = acq_fee(amount, 25); - net = amount - fee; - h_amount = amount; h_fee = fee; h_net = net; - 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) FAIL(b); - setl(b, T_DELTA, amount - h_old); - setl(b, T_FEE, fee); - setl(b, T_NET, net); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + h_amt = getl(b, T_AMOUNT); + if (h_amt < 1L) { Bchg(b, T_MSG, 0, "금액 범위 오류", 0L); setl(b, T_RC, 104); FAIL(b); } + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + + /* 3) 업무규칙 분기부: 조정 유형/부호/한도 판정(3+ 분기) */ + /* 확정 금액과 기존 금액의 증감 방향으로 처리 경로를 나눈다 */ + h_delta = h_amt - h_gross; + if (h_delta == 0) { Bchg(b, T_MSG, 0, "금액 변동 없음", 0L); setl(b, T_RC, 169); FAIL(b); } + else if (h_delta > 0) { h_val = 3; Bchg(b, T_MSG, 0, "금액 증액 확정", 0L); } + else { h_val = 2; Bchg(b, T_MSG, 0, "금액 감액 확정", 0L); } + /* 하우스 기준 25bps 로 절대 재계산 */ + computed = acq_fee(h_amt, 25); + + /* 4) 본처리부: 조정 원장 기록 및 매입 금액 보정 */ + h_fee = computed; + h_net = h_amt - h_fee; + if (acdb_update_amount(h_pid, h_amt, h_fee, h_net) < 0) + { Bchg(b, T_MSG, 0, "금액 갱신 실패", 0L); setl(b, T_RC, 97); FAIL(b); } + h_aff = 1; + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "금액확정 pid=%ld %ld -> %ld 증감=%ld", h_pid, h_gross, h_amt, h_delta); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_AMTFIX', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_AMTFIX' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_AMOUNT, h_amt); + setl(b, T_DELTA, h_delta); + setl(b, T_FEE, h_fee); + setl(b, T_NET, h_net); + setl(b, T_ARG1, h_val); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_APPRGAP.pgc b/app/src/ac/svc/ACQ_APPRGAP.pgc index 2b784eb..f6d258d 100644 --- a/app/src/ac/svc/ACQ_APPRGAP.pgc +++ b/app/src/ac/svc/ACQ_APPRGAP.pgc @@ -1,7 +1,7 @@ /* * ACQ_APPRGAP.pgc - ac 모듈 서비스 ACQ_APPRGAP (one service = one file). - * 승인-매입 갭 점검: 승인 없는 매입(무승인 매입)과 매입 없는 승인(고아 - * 승인)을 양방향으로 세어 갭 규모를 보고한다. + * 승인-매입 갭 점검: 승인 없는 매입(무승인 매입)과 매입 없는 승인(고아 승인)을 + * 양방향으로 세어 갭 규모를 보고한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_APPRGAP.h" @@ -9,30 +9,151 @@ void ACQ_APPRGAP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_APPRGAP"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_noappr = 0, h_orphan = 0, h_amt = 0; - char h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - /* 승인 없는 매입: 건수 + 금액 */ - EXEC SQL SELECT count(*), coalesce(sum(u.amount), 0) - INTO :h_noappr, :h_amt - FROM purchase u - WHERE u.biz_date = :h_bizdate AND u.status NOT IN ('C', 'V', 'P') - AND NOT EXISTS (SELECT 1 FROM approval a WHERE a.purchase_id = u.purchase_id AND a.approved = true); - if (sqlca.sqlcode < 0) FAIL(b); + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); - /* 매입 없는 고아 승인 */ - EXEC SQL SELECT count(*) INTO :h_orphan - FROM approval a LEFT JOIN purchase u ON u.purchase_id = a.purchase_id - WHERE u.purchase_id IS NULL; - if (sqlca.sqlcode < 0) { userlog("ACQ_APPRGAP FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } - setl(b, T_ARG1, h_noappr); /* 무승인 매입 건수 */ - setl(b, T_AMT1, h_amt); /* 무승인 매입 금액 */ - setl(b, T_ARG2, h_orphan); /* 고아 승인 건수 */ - setl(b, T_RC, (h_noappr + h_orphan) > 0 ? 1 : 0); + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*) INTO :h_a FROM purchase p WHERE p.biz_date = :h_bizdate AND NOT EXISTS (SELECT 1 FROM approval a WHERE a.purchase_id = p.purchase_id); + if (sqlca.sqlcode < 0) { + userlog("%s: 무승인매입 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_b FROM approval a WHERE NOT EXISTS (SELECT 1 FROM purchase p WHERE p.purchase_id = a.purchase_id); + if (sqlca.sqlcode < 0) { + userlog("%s: 고아승인 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_gross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 모집단 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 갭 규모를 모집단 대비 비율로 환산해 심각도 판정 */ + h_e = h_a + h_b; + if (h_cnt > 0) ratio = (h_a * 10000L) / h_cnt; else ratio = 0; + if (h_e == 0) { h_val = 0; } + else if (ratio > 500) { h_val = 3; } + else if (ratio > 100) { h_val = 2; } + else { h_val = 1; } + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "승인갭 bizdate=%s 무승인=%ld 고아=%ld 등급=%ld", h_bizdate, h_a, h_b, h_val); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_APPRGAP', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_APPRGAP' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_ARG1, h_a); + setl(b, T_ARG2, h_b); + setl(b, T_ARG3, h_e); + setl(b, T_ARG4, h_val); + setl(b, T_COUNT, h_cnt); + setl(b, T_GROSS, h_gross); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_APPRLINK.pgc b/app/src/ac/svc/ACQ_APPRLINK.pgc index 026d2f1..8f34858 100644 --- a/app/src/ac/svc/ACQ_APPRLINK.pgc +++ b/app/src/ac/svc/ACQ_APPRLINK.pgc @@ -1,14 +1,159 @@ /* * ACQ_APPRLINK.pgc - ac 모듈 서비스 ACQ_APPRLINK (one service = one file). - * Moved verbatim from the former inline ac_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by ac_svr at tpsvrinit. + * 승인 연계: 매입 건에 승인 원장 행을 연결한다. 이미 승인이 있으면 추가 + * 승인 회차로 기록하고, 과다 승인 연계는 차단한다. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_APPRLINK.h" 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); + const char *SVC = "ACQ_APPRLINK"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; + EXEC SQL BEGIN DECLARE SECTION; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; + EXEC SQL END DECLARE SECTION; + + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 매입 상태 전이 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + + /* 3) 업무규칙 분기부: 상태 전이 가능여부/유형 판정(3+ 분기) */ + if (!(strcmp(h_prev, "A") == 0 || strcmp(h_prev, "M") == 0 || strcmp(h_prev, "S") == 0 || strcmp(h_prev, "C") == 0 || strcmp(h_prev, "H") == 0 || strcmp(h_prev, "V") == 0 || strcmp(h_prev, "P") == 0)) { + userlog("%s 거절: 상태 %s 에서 전이 불가", SVC, h_prev); + Bchg(b, T_MSG, 0, "상태 전이 불가", 0L); setl(b, T_RC, 120); FAIL(b); + } + /* 기존 승인 회차를 세어 연계 가능 여부를 판정 */ + EXEC SQL SELECT count(*), count(*) FILTER (WHERE approved) INTO :h_cnt, :h_a + FROM approval WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 승인회차 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_cnt >= 10) { Bchg(b, T_MSG, 0, "승인 연계 한도 초과", 0L); setl(b, T_RC, 145); FAIL(b); } + else if (h_cnt == 0) { h_val = 1; Bchg(b, T_MSG, 0, "최초 승인 연계", 0L); } + else if (h_a == 0) { h_val = 3; Bchg(b, T_MSG, 0, "거절 이력 후 재연계", 0L); } + else { h_val = 2; Bchg(b, T_MSG, 0, "추가 승인 연계", 0L); } + + /* 4) 본처리부: 부수행 기록 및 상태 갱신 */ + if (acdb_insert_approval(h_pid, 1) < 0) { Bchg(b, T_MSG, 0, "승인 연계 실패", 0L); setl(b, T_RC, 95); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(max(approval_id),0) INTO :h_b, :h_id FROM approval WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 연계후집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_aff = 1; + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "승인연계 pid=%ld 이전=%ld 이후=%ld id=%ld", h_pid, h_cnt, h_b, h_id); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_APPRLINK', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_APPRLINK' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_COUNT, h_b); + setl(b, T_ID1, h_id); + setl(b, T_ARG1, h_cnt); + setl(b, T_ARG2, h_val); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_APPRMAP.pgc b/app/src/ac/svc/ACQ_APPRMAP.pgc index f83b2aa..f361325 100644 --- a/app/src/ac/svc/ACQ_APPRMAP.pgc +++ b/app/src/ac/svc/ACQ_APPRMAP.pgc @@ -1,19 +1,151 @@ /* * ACQ_APPRMAP.pgc - ac 모듈 서비스 ACQ_APPRMAP (one service = one file). - * Moved verbatim from the former inline ac_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by ac_svr at tpsvrinit. + * 승인 매핑 조회: 한 매입에 연결된 승인 원장의 총 건수와 승인/거절 분포를 + * 집계해 승인 매핑 상태를 응답한다. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_APPRMAP.h" void ACQ_APPRMAP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_APPRMAP"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_cnt; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; + + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ h_pid = getl(b, T_PURCHASE_ID); - EXEC SQL SELECT count(*) INTO :h_cnt FROM approval WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*), count(*) FILTER (WHERE approved), count(*) FILTER (WHERE NOT approved) INTO :h_cnt, :h_a, :h_b FROM approval WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 승인분포 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(max(approval_id),0) INTO :h_top FROM approval WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 최근승인 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT status, amount INTO :h_status, :h_gross FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입상태 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 승인 유무와 거절 이력으로 매핑 상태 코드를 산정 */ + if (h_cnt == 0) { h_val = 0; Bchg(b, T_MSG, 0, "승인 없음", 0L); } + else if (h_b > 0 && h_a == 0) { h_val = 2; Bchg(b, T_MSG, 0, "전건 거절", 0L); } + else if (h_b > 0) { h_val = 3; Bchg(b, T_MSG, 0, "승인/거절 혼재", 0L); } + else { h_val = 1; Bchg(b, T_MSG, 0, "정상 승인", 0L); } + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + ratio = (h_cnt > 0) ? (h_a * 10000L / h_cnt) : 0; + h_share = ratio; + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "승인매핑 pid=%ld 총=%ld 승인=%ld 거절=%ld 상태=%ld", h_pid, h_cnt, h_a, h_b, h_val); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_APPRMAP', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_APPRMAP' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ setl(b, T_COUNT, h_cnt); + setl(b, T_ARG1, h_a); + setl(b, T_ARG2, h_b); + setl(b, T_ARG3, h_val); + setl(b, T_ID1, h_top); + Bchg(b, T_STATUS, 0, h_status, 0L); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_BRANDQRY.pgc b/app/src/ac/svc/ACQ_BRANDQRY.pgc index a963d63..89411ad 100644 --- a/app/src/ac/svc/ACQ_BRANDQRY.pgc +++ b/app/src/ac/svc/ACQ_BRANDQRY.pgc @@ -9,22 +9,140 @@ void ACQ_BRANDQRY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_BRANDQRY"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_n = 0, h_sum = 0, h_fee = 0; - char h_iss[32], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_ISSUER, h_iss, sizeof(h_iss)); - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0) - INTO :h_n, :h_sum, :h_fee FROM purchase - WHERE issuer = :h_iss AND biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); - setl(b, T_COUNT, h_n); - setl(b, T_GROSS, h_sum); + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + 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; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } + + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(avg(fee),0)::bigint INTO :h_cnt, :h_gross, :h_fee, :h_avg FROM purchase WHERE issuer = :h_issuer AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 브랜드집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_a, :h_sum FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 전체집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 브랜드 점유율(bps) 및 규모 등급 판정 */ + if (h_sum > 0) ratio = (h_gross * 10000L) / h_sum; else ratio = 0; + if (h_cnt == 0) { h_val = 0; } + else if (ratio >= 5000) { h_val = 3; } + else if (ratio >= 2000) { h_val = 2; } + else { h_val = 1; } + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + h_share = ratio; + h_net = h_gross - h_fee; + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "브랜드조회 issuer=%s 건수=%ld 총액=%ld 점유=%ld", h_issuer, h_cnt, h_gross, ratio); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_BRANDQRY', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_BRANDQRY' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_GROSS, h_gross); setl(b, T_FEE, h_fee); - setl(b, T_RC, h_n > 0 ? 0 : 1); + setl(b, T_ARG1, h_avg); + setl(b, T_ARG2, ratio); + setl(b, T_ARG3, h_val); + setl(b, T_NET, h_net); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_BRANDRANK.pgc b/app/src/ac/svc/ACQ_BRANDRANK.pgc index 84f6d16..7c1c45d 100644 --- a/app/src/ac/svc/ACQ_BRANDRANK.pgc +++ b/app/src/ac/svc/ACQ_BRANDRANK.pgc @@ -1,7 +1,7 @@ /* * ACQ_BRANDRANK.pgc - ac 모듈 서비스 ACQ_BRANDRANK (one service = one file). - * 매입 브랜드별 순위: 발급사 매출 상위 순으로 커서 조회하며 누적 점유율을 - * 계산해 상위 브랜드가 전체의 절반을 넘는 지점(집중도)을 함께 낸다. + * 매입 브랜드별 순위: 발급사 매출 상위 순으로 1~3위를 뽑아 점유율을 계산하고 + * 상위 브랜드 집중도(상위 3사 합계 비중)를 함께 낸다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_BRANDRANK.h" @@ -9,34 +9,154 @@ void ACQ_BRANDRANK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long rank = 0, run = 0, half_at = 0; + const char *SVC = "ACQ_BRANDRANK"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_sum, h_grand = 0; - char h_iss[32], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_grand FROM purchase WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); - EXEC SQL DECLARE brr_cur CURSOR FOR - SELECT issuer, sum(amount) FROM purchase WHERE biz_date = :h_bizdate - GROUP BY issuer ORDER BY sum(amount) DESC; - EXEC SQL OPEN brr_cur; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH brr_cur INTO :h_iss, :h_sum; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE brr_cur; FAIL(b); } - rank++; run += h_sum; - if (half_at == 0 && h_grand > 0 && run * 2 >= h_grand) half_at = rank; - if (rank <= 6) Badd(b, T_ISSUER, h_iss, 0L); + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); } - EXEC SQL CLOSE brr_cur; + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } - setl(b, T_GROSS, h_grand); - setl(b, T_ARG1, rank); - setl(b, T_ARG2, half_at); + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT coalesce(sum(amount),0), count(DISTINCT issuer) INTO :h_gross, :h_n FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 모집단 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(issuer,''), coalesce(sum(amount),0) INTO :h_str2, :h_a FROM purchase WHERE biz_date = :h_bizdate GROUP BY issuer ORDER BY sum(amount) DESC LIMIT 1; + if (sqlca.sqlcode < 0) { + userlog("%s: 1위 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(sum(s),0) INTO :h_b FROM (SELECT sum(amount) AS s FROM purchase WHERE biz_date = :h_bizdate GROUP BY issuer ORDER BY sum(amount) DESC LIMIT 3) t; + if (sqlca.sqlcode < 0) { + userlog("%s: 상위3사 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 1위 점유율과 상위 3사 집중도로 시장 구조를 판정 */ + if (h_gross > 0) ratio = (h_a * 10000L) / h_gross; else ratio = 0; + if (h_gross > 0) h_share = (h_b * 10000L) / h_gross; else h_share = 0; + if (h_share >= 8000) { h_val = 3; } + else if (h_share >= 5000) { h_val = 2; } + else if (h_share > 0) { h_val = 1; } + else { h_val = 0; } + h_cnt = h_n; + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "브랜드순위 bizdate=%s 1위=%s 점유=%ld 집중=%ld", h_bizdate, h_str2, ratio, h_share); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_BRANDRANK', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_BRANDRANK' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + Bchg(b, T_ISSUER, 0, h_str2, 0L); + setl(b, T_AMT1, h_a); + setl(b, T_AMT2, h_b); + setl(b, T_ARG1, ratio); + setl(b, T_ARG2, h_share); + setl(b, T_ARG3, h_val); + setl(b, T_COUNT, h_n); + setl(b, T_GROSS, h_gross); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_BRANDSUM.pgc b/app/src/ac/svc/ACQ_BRANDSUM.pgc index 058adf6..25ca98c 100644 --- a/app/src/ac/svc/ACQ_BRANDSUM.pgc +++ b/app/src/ac/svc/ACQ_BRANDSUM.pgc @@ -1,7 +1,7 @@ /* * ACQ_BRANDSUM.pgc - ac 모듈 서비스 ACQ_BRANDSUM (one service = one file). - * 매입 브랜드별 집계: 발급사(카드 브랜드) 기준으로 매출 합계를 그룹 집계해 - * 브랜드명·매출액 쌍을 occurrence 로 내리고 최다 브랜드 건수를 함께 계산한다. + * 매입 브랜드별 집계: 발급사(카드 브랜드) 기준 매출 합계를 그룹 집계해 + * 브랜드 수와 최다 브랜드의 건수/금액을 계산한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_BRANDSUM.h" @@ -9,29 +9,154 @@ void ACQ_BRANDSUM(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long grand = 0, kinds = 0; + const char *SVC = "ACQ_BRANDSUM"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_sum, h_n; - char h_iss[32], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL DECLARE brs_cur CURSOR FOR - SELECT issuer, sum(amount), count(*) FROM purchase - WHERE biz_date = :h_bizdate GROUP BY issuer ORDER BY sum(amount) DESC; - EXEC SQL OPEN brs_cur; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH brs_cur INTO :h_iss, :h_sum, :h_n; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE brs_cur; FAIL(b); } - if (kinds < 8) { Badd(b, T_ISSUER, h_iss, 0L); Badd(b, T_AMT1, (char *)&h_sum, 0L); } - grand += h_sum; kinds++; + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); } - EXEC SQL CLOSE brs_cur; + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } - setl(b, T_GROSS, grand); - setl(b, T_ARG1, kinds); + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(DISTINCT issuer), count(*), coalesce(sum(amount),0) INTO :h_n, :h_cnt, :h_gross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 브랜드수 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(issuer,''), count(*), coalesce(sum(amount),0) INTO :h_str2, :h_a, :h_b FROM purchase WHERE biz_date = :h_bizdate GROUP BY issuer ORDER BY count(*) DESC LIMIT 1; + if (sqlca.sqlcode < 0) { + userlog("%s: 최다브랜드 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(sum(fee),0) INTO :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 수수료합 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 브랜드 다양성과 최다 브랜드 편중도를 판정 */ + if (h_cnt > 0) ratio = (h_a * 10000L) / h_cnt; else ratio = 0; + if (h_n <= 1) { h_val = 3; } + else if (ratio >= 6000) { h_val = 2; } + else if (h_n >= 4) { h_val = 0; } + else { h_val = 1; } + h_avg = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "브랜드집계 bizdate=%s 브랜드수=%ld 최다=%s 편중=%ld", h_bizdate, h_n, h_str2, ratio); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_BRANDSUM', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_BRANDSUM' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_COUNT, h_n); + Bchg(b, T_ISSUER, 0, h_str2, 0L); + setl(b, T_ARG1, h_a); + setl(b, T_AMT1, h_b); + setl(b, T_ARG2, ratio); + setl(b, T_ARG3, h_val); + setl(b, T_GROSS, h_gross); + setl(b, T_FEE, h_fee); + setl(b, T_ARG4, h_avg); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_BULKCHK.pgc b/app/src/ac/svc/ACQ_BULKCHK.pgc index 19cb829..a2d718c 100644 --- a/app/src/ac/svc/ACQ_BULKCHK.pgc +++ b/app/src/ac/svc/ACQ_BULKCHK.pgc @@ -1,7 +1,7 @@ /* * ACQ_BULKCHK.pgc - ac 모듈 서비스 ACQ_BULKCHK (one service = one file). - * 대량매입 배치검증: 영업일 접수(A) 매입을 커서로 순회하며 금액 양수 및 - * 수수료 정합(net=amount-fee)을 점검해 정상/오류 건수를 분리 집계한다. + * 대량매입 배치검증: 영업일 접수(A) 매입의 금액 양수 및 수수료 정합 + * (net = amount - fee)을 일괄 점검해 정상/오류 건수를 분리 집계한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_BULKCHK.h" @@ -9,30 +9,157 @@ void ACQ_BULKCHK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long ok = 0, bad = 0; + const char *SVC = "ACQ_BULKCHK"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_amt, h_fee, h_net; - char h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL DECLARE blk_cur CURSOR FOR - SELECT purchase_id, amount, fee, net FROM purchase - WHERE biz_date = :h_bizdate AND status = 'A'; - EXEC SQL OPEN blk_cur; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH blk_cur INTO :h_pid, :h_amt, :h_fee, :h_net; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE blk_cur; FAIL(b); } - if (h_amt > 0 && h_net == h_amt - h_fee) ok++; - else { bad++; if (bad <= 8) Badd(b, T_PURCHASE_ID, (char *)&h_pid, 0L); } + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); } - EXEC SQL CLOSE blk_cur; + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } - setl(b, T_ARG1, ok); - setl(b, T_ARG2, bad); - setl(b, T_RC, bad > 0 ? 1 : 0); + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*) INTO :h_cnt FROM purchase WHERE biz_date = :h_bizdate AND status = 'A'; + if (sqlca.sqlcode < 0) { + userlog("%s: 검증모집단 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_a FROM purchase WHERE biz_date = :h_bizdate AND status = 'A' AND amount <= 0; + if (sqlca.sqlcode < 0) { + userlog("%s: 금액오류 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_b FROM purchase WHERE biz_date = :h_bizdate AND status = 'A' AND net <> amount - fee; + if (sqlca.sqlcode < 0) { + userlog("%s: 정합오류 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_gross FROM purchase WHERE biz_date = :h_bizdate AND status = 'A'; + if (sqlca.sqlcode < 0) { + userlog("%s: 금액합 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 오류 유형별 건수를 합산해 배치 판정 결과를 만든다 */ + h_e = h_a + h_b; + h_c = h_cnt - h_e; + if (h_e == 0) { h_val = 0; Bchg(b, T_MSG, 0, "전건 정상", 0L); } + else if (h_a > 0 && h_b > 0) { h_val = 3; Bchg(b, T_MSG, 0, "금액/정합 복합 오류", 0L); } + else if (h_b > 0) { h_val = 2; Bchg(b, T_MSG, 0, "수수료 정합 오류", 0L); } + else { h_val = 1; Bchg(b, T_MSG, 0, "금액 오류", 0L); } + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "대량검증 bizdate=%s 대상=%ld 정상=%ld 오류=%ld", h_bizdate, h_cnt, h_c, h_e); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_BULKCHK', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_BULKCHK' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_ARG1, h_c); + setl(b, T_ARG2, h_e); + setl(b, T_ARG3, h_a); + setl(b, T_ARG4, h_b); + setl(b, T_ARG5, h_val); + setl(b, T_GROSS, h_gross); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_BULKRPT.pgc b/app/src/ac/svc/ACQ_BULKRPT.pgc index 1ea55bb..caeeaad 100644 --- a/app/src/ac/svc/ACQ_BULKRPT.pgc +++ b/app/src/ac/svc/ACQ_BULKRPT.pgc @@ -1,7 +1,7 @@ /* * ACQ_BULKRPT.pgc - ac 모듈 서비스 ACQ_BULKRPT (one service = one file). - * 대량매입 리포트: 영업일 매입을 상태(접수/대사/정산/취소)별로 한 번에 분해 - * 집계해 상태별 건수와 총 매출·순정산 금액을 요약 리포트로 낸다. + * 대량매입 리포트: 영업일 매입을 상태(접수/대사/정산/취소)별로 한 번에 + * 분해 집계해 상태별 건수와 총 매출·순정산 금액을 요약 리포트로 낸다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_BULKRPT.h" @@ -9,24 +9,150 @@ void ACQ_BULKRPT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_BULKRPT"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_a = 0, h_m = 0, h_s = 0, h_c = 0, h_g = 0, h_net = 0; - char h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL SELECT count(*) FILTER (WHERE status = 'A'), count(*) FILTER (WHERE status = 'M'), - count(*) FILTER (WHERE status = 'S'), count(*) FILTER (WHERE status = 'C'), - coalesce(sum(amount),0), coalesce(sum(net) FILTER (WHERE status = 'S'),0) - INTO :h_a, :h_m, :h_s, :h_c, :h_g, :h_net FROM purchase WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } + + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*) FILTER (WHERE status = 'A'), count(*) FILTER (WHERE status = 'M'), count(*) FILTER (WHERE status = 'S'), count(*) FILTER (WHERE status = 'C') INTO :h_a, :h_b, :h_c, :h_d FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 상태분해 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) INTO :h_cnt, :h_gross, :h_net, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 금액집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 진행 단계 비중으로 마감 준비도를 판정 */ + h_e = h_a + h_b; + if (h_cnt > 0) ratio = (h_c * 10000L) / h_cnt; else ratio = 0; + if (h_cnt == 0) { h_val = 0; } + else if (ratio >= 9000) { h_val = 3; } + else if (ratio >= 5000) { h_val = 2; } + else { h_val = 1; } + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "대량리포트 bizdate=%s 총=%ld 접수=%ld 정산=%ld 마감도=%ld", h_bizdate, h_cnt, h_a, h_c, ratio); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_BULKRPT', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_BULKRPT' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_COUNT, h_cnt); setl(b, T_ARG1, h_a); - setl(b, T_ARG2, h_m); - setl(b, T_ARG3, h_s); - setl(b, T_ARG4, h_c); - setl(b, T_GROSS, h_g); + setl(b, T_ARG2, h_b); + setl(b, T_ARG3, h_c); + setl(b, T_ARG4, h_d); + setl(b, T_ARG5, h_e); + setl(b, T_GROSS, h_gross); setl(b, T_NET, h_net); + setl(b, T_FEE, h_fee); + setl(b, T_ARG6, ratio); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_BULKVERIFY.pgc b/app/src/ac/svc/ACQ_BULKVERIFY.pgc index 8796e75..a98a3ba 100644 --- a/app/src/ac/svc/ACQ_BULKVERIFY.pgc +++ b/app/src/ac/svc/ACQ_BULKVERIFY.pgc @@ -9,29 +9,168 @@ void ACQ_BULKVERIFY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long bps = 0; + const char *SVC = "ACQ_BULKVERIFY"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_bps, h_total = 0, h_feebad = 0, h_netbad = 0; - char h_merch[64], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - if (acdb_merchant_mdr(h_merch, &bps) < 0) { setl(b, T_RC, 5); FAIL(b); } - h_bps = bps; + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); - EXEC SQL SELECT count(*), - count(*) FILTER (WHERE fee <> amount * :h_bps / 10000), - count(*) FILTER (WHERE net <> amount - fee) - INTO :h_total, :h_feebad, :h_netbad - FROM purchase - WHERE merchant_id = :h_merch AND biz_date = :h_bizdate AND status NOT IN ('C', 'V'); - if (sqlca.sqlcode < 0) { userlog("ACQ_BULKVERIFY FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + if (merch[0] == 0) { Bchg(b, T_MSG, 0, "가맹점 ID 누락", 0L); setl(b, T_RC, 101); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } - setl(b, T_COUNT, h_total); - setl(b, T_ARG1, h_feebad); /* 수수료 오류 건수 */ - setl(b, T_ARG2, h_netbad); /* 정산금 오류 건수 */ - setl(b, T_RC, (h_feebad + h_netbad) > 0 ? 1 : 0); + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + EXEC SQL SELECT name, status, daily_limit INTO :h_str1, :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (strcmp(h_status, "ACTIVE") != 0) + userlog("%s 주의: 가맹점 %s 상태=%s", SVC, h_merch, h_status); + /* 가맹점 MDR(bps) 및 상태분포 컨텍스트 로드 */ + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + EXEC SQL SELECT count(*) FILTER (WHERE status IN ('A','M')), count(*) FILTER (WHERE status = 'S') INTO :h_openc, :h_setc FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 상태분포 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산 진행 컨텍스트(기 정산 순액) */ + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_ctx FROM settlement WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0) INTO :h_cnt, :h_gross, :h_fee FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 모집단 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_a FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate AND net <> amount - fee; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산정합오류 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_b FROM purchase p WHERE p.merchant_id = :h_merch AND p.biz_date = :h_bizdate AND p.fee <> (p.amount * :h_rate / 10000); + if (sqlca.sqlcode < 0) { + userlog("%s: 수수료정합오류 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* MDR 재계산 차이와 정산 정합 오류를 등급화 */ + h_e = h_a + h_b; + if (h_cnt > 0) ratio = (h_e * 10000L) / h_cnt; else ratio = 0; + if (h_e == 0) { h_val = 0; } + else if (ratio >= 3000) { h_val = 3; } + else if (ratio >= 1000) { h_val = 2; } + else { h_val = 1; } + h_net = h_gross - h_fee; + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :h_val2 FROM merchant_summary WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 누적집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_pdgross FROM purchase WHERE merchant_id = :h_merch AND biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_val2 > 0 && h_gross > 0) ratio = (h_gross * 10000L) / (h_val2 + h_gross); + else ratio = 0; + h_share = (h_pdgross > 0) ? (h_gross * 100L / h_pdgross) : 0; + userlog("%s 추이: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_pdgross, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "대량정합 merch=%s 대상=%ld 정산오류=%ld 수수료오류=%ld", h_merch, h_cnt, h_a, h_b); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_BULKVERIFY', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_BULKVERIFY' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_ARG1, h_a); + setl(b, T_ARG2, h_b); + setl(b, T_ARG3, h_e); + setl(b, T_ARG4, h_val); + setl(b, T_GROSS, h_gross); + setl(b, T_FEE, h_fee); + setl(b, T_NET, h_net); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_CANCEL.pgc b/app/src/ac/svc/ACQ_CANCEL.pgc index 732b698..6e38039 100644 --- a/app/src/ac/svc/ACQ_CANCEL.pgc +++ b/app/src/ac/svc/ACQ_CANCEL.pgc @@ -1,33 +1,120 @@ /* * ACQ_CANCEL.pgc - ac 모듈 서비스 ACQ_CANCEL (one service = one file). - * Moved verbatim from the former inline ac_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by ac_svr at tpsvrinit. + * 매입 전액 취소: 취소 가능 상태를 확인하고 ac_cancel 에 FULL 취소행을 만든 뒤 + * 매입을 취소(C)로 전이한다. 취소 수수료 환급분과 가맹점 요약도 함께 갱신한다. + * 입출력 계약 보존: 입력 T_PURCHASE_ID/T_AMOUNT/T_BIZDATE/T_REASON, + * 출력 T_CANCEL_ID / T_STATUS='C'. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_CANCEL.h" void ACQ_CANCEL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], reason[128]; + const char *SVC = "ACQ_CANCEL"; + char bizdate[16], reason[128], logmsg[256]; + long grade = 0, ratio = 0; EXEC SQL BEGIN DECLARE SECTION; long h_id, h_pid, h_amt; - char h_bizdate[16], h_reason[128]; + long h_orig = 0, h_fee = 0, h_prior = 0, h_cnt = 0, h_sum = 0, h_logid = 0; + char h_bizdate[16], h_reason[128], h_prev[8], h_merch[64], h_logmsg[256]; EXEC SQL END DECLARE SECTION; + userlog("%s 진입: 매입 전액 취소 요청", SVC); + + /* 1) 입력 검증부 */ h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { + Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); + } 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; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + /* 2) 마스터/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee INTO :h_prev, :h_merch, :h_orig, :h_fee + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 조회 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { + Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); + } + /* 취소 금액 미지정 시 원 매입금액을 전액 취소한다 */ + if (h_amt <= 0) h_amt = h_orig; + userlog("%s 컨텍스트: pid=%ld merch=%s 상태=%s 원금=%ld 취소=%ld", + SVC, h_pid, h_merch, h_prev, h_orig, h_amt); + + /* 기취소 누계 확인 */ + EXEC SQL SELECT coalesce(sum(amount),0), count(*) INTO :h_prior, :h_cnt + FROM ac_cancel WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); } + + /* 3) 업무규칙 분기부: 상태/누계/금액 검증 (3+ 분기) */ + if (strcmp(h_prev, "C") == 0 || strcmp(h_prev, "V") == 0) { + userlog("%s 거절: 이미 종료된 매입 상태=%s", SVC, h_prev); + Bchg(b, T_MSG, 0, "이미 취소/무효된 매입", 0L); setl(b, T_RC, 115); FAIL(b); + } + if (h_prior + h_amt > h_orig) { + Bchg(b, T_MSG, 0, "취소 누계가 원금 초과", 0L); setl(b, T_RC, 116); FAIL(b); + } + if (strcmp(h_prev, "S") == 0) { + grade = 3; Bchg(b, T_MSG, 0, "정산완료건 취소", 0L); + } else if (h_prior > 0) { + grade = 2; Bchg(b, T_MSG, 0, "부분취소 후 잔액 취소", 0L); + } else { + grade = 1; Bchg(b, T_MSG, 0, "전액 취소", 0L); + } + /* 수수료 환급분(비례배분) */ + ratio = (h_orig > 0) ? (h_fee * h_amt / h_orig) : 0; + + /* 4) 본처리부 */ EXEC SQL SELECT nextval('ac_cancel_seq') INTO :h_id; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); 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); + if (sqlca.sqlcode < 0) { + userlog("%s: 취소행 적재 실패 [%d]", SVC, sqlca.sqlcode); + setl(b, T_RC, 90); FAIL(b); + } + if (acdb_update_status(h_pid, "C") < 0) { + userlog("%s: 취소 상태 전이 실패 pid=%ld", SVC, h_pid); + Bchg(b, T_MSG, 0, "상태 전이 실패", 0L); setl(b, T_RC, 96); FAIL(b); + } + + /* 6) 집계/요약 갱신부: 가맹점 취소 실적 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate + AND status = 'C'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); } + + /* 5) 이력/감사 적재부 */ + snprintf(logmsg, sizeof(logmsg), "매입취소 pid=%ld cid=%ld 직전=%s 취소=%ld 환급=%ld", + h_pid, h_id, h_prev, h_amt, ratio); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_CANCEL', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); } + + /* 8) 응답부 (계약 보존: T_CANCEL_ID / T_STATUS='C') */ setl(b, T_CANCEL_ID, h_id); Bchg(b, T_STATUS, 0, "C", 0L); + Bchg(b, T_MERCHANT, 0, h_merch, 0L); + setl(b, T_AMOUNT, h_amt); + setl(b, T_FEE, ratio); + setl(b, T_ARG1, grade); + setl(b, T_COUNT, h_cnt); + setl(b, T_GROSS, h_sum); + setl(b, T_RC, 0); + userlog("%s 완료: pid=%ld cid=%ld 취소=%ld 등급=%ld", SVC, h_pid, h_id, h_amt, grade); OK(b); } diff --git a/app/src/ac/svc/ACQ_CB_ARB.pgc b/app/src/ac/svc/ACQ_CB_ARB.pgc index a69d1e6..463673c 100644 --- a/app/src/ac/svc/ACQ_CB_ARB.pgc +++ b/app/src/ac/svc/ACQ_CB_ARB.pgc @@ -1,7 +1,7 @@ /* * ACQ_CB_ARB.pgc - ac 모듈 서비스 ACQ_CB_ARB (one service = one file). - * 지불거절 중재: 라운드2까지 미해결 건을 카드사 중재로 넘긴다. 해당 CB - * 행 사유를 ARB 로 갱신하고 원매입을 보류(H)로 유지한다. + * 지불거절 중재: 라운드2까지 미해결 건을 카드사 중재로 넘긴다. 해당 CB 행 + * 사유를 ARB 로 갱신하고 원매입을 보류(H)로 유지한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_CB_ARB.h" @@ -9,28 +9,162 @@ void ACQ_CB_ARB(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char note[80]; + const char *SVC = "ACQ_CB_ARB"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_cid, h_pid = 0; - char h_reason[100]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; - h_cid = getl(b, T_CANCEL_ID); - gets_(b, T_REASON, note, sizeof(note)); - snprintf(h_reason, sizeof(h_reason), "ARB:%s", note[0] ? note : "PENDING"); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); - EXEC SQL SELECT purchase_id INTO :h_pid FROM ac_cancel - WHERE cancel_id = :h_cid AND cancel_type IN ('CB', 'CB2'); - if (sqlca.sqlcode == 100) { setl(b, T_RC, 4); FAIL(b); } - if (sqlca.sqlcode < 0) FAIL(b); + userlog("%s 진입: 매입 상태 전이 요청 접수", SVC); - EXEC SQL UPDATE ac_cancel SET reason = :h_reason WHERE cancel_id = :h_cid; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL UPDATE purchase SET status = 'H', updated_at = now() - WHERE purchase_id = :h_pid AND status <> 'C'; - if (sqlca.sqlcode < 0) { userlog("ACQ_CB_ARB FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "일반"); + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - setl(b, T_PURCHASE_ID, h_pid); + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + + /* 3) 업무규칙 분기부: 상태 전이 가능여부/유형 판정(3+ 분기) */ + if (!(strcmp(h_prev, "H") == 0)) { + userlog("%s 거절: 상태 %s 에서 전이 불가", SVC, h_prev); + Bchg(b, T_MSG, 0, "상태 전이 불가", 0L); setl(b, T_RC, 120); FAIL(b); + } + /* 라운드2(CB2) 이력이 있어야 중재 이관 가능 */ + EXEC SQL SELECT count(*) INTO :h_cnt FROM ac_cancel + WHERE purchase_id = :h_pid AND cancel_type = 'CB2'; + if (sqlca.sqlcode < 0) { + userlog("%s: CB2조회 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_cnt == 0) { Bchg(b, T_MSG, 0, "라운드2 미진행 건은 중재 불가", 0L); setl(b, T_RC, 128); FAIL(b); } + EXEC SQL SELECT coalesce(max(cancel_id),0), coalesce(sum(amount),0) INTO :h_canid, :h_sum + FROM ac_cancel WHERE purchase_id = :h_pid AND cancel_type LIKE 'CB%'; + if (sqlca.sqlcode < 0) { + userlog("%s: 중재대상 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_sum >= 1000000) { h_val = 3; } + else if (h_sum >= 100000) { h_val = 2; } + else { h_val = 1; } + snprintf(logmsg, sizeof(logmsg), "ARB:%s", h_reason); + strncpy(h_reason, logmsg, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + + /* 4) 본처리부: 부수행 기록 및 상태 갱신 */ + EXEC SQL UPDATE ac_cancel SET reason = :h_reason WHERE cancel_id = :h_canid; + if (sqlca.sqlcode < 0) { + userlog("%s: 중재표시 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_rem = sqlca.sqlerrd[2]; + h_aff = 1; + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "CB중재 pid=%ld 라운드2=%ld 청구합=%ld 등급=%ld", h_pid, h_cnt, h_sum, h_val); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_CB_ARB', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_CB_ARB' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_CANCEL_ID, h_canid); + setl(b, T_AMOUNT, h_sum); + setl(b, T_ARG1, h_rem); + setl(b, T_ARG2, h_val); + Bchg(b, T_STATUS, 0, h_prev, 0L); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_CB_DEFEND.pgc b/app/src/ac/svc/ACQ_CB_DEFEND.pgc index 468e7de..6032209 100644 --- a/app/src/ac/svc/ACQ_CB_DEFEND.pgc +++ b/app/src/ac/svc/ACQ_CB_DEFEND.pgc @@ -1,7 +1,7 @@ /* * ACQ_CB_DEFEND.pgc - ac 모듈 서비스 ACQ_CB_DEFEND (one service = one file). * 지불거절 방어: 접수된 CB 건에 방어 증빙 사유를 덧붙이고, 보류 중이던 - * 원매입을 대사완료(M) 상태로 복원한다. 방어 결과는 T_RC 로 응답. + * 원매입을 대사완료(M) 상태로 복원한다. 방어 결과를 결과코드로 응답한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_CB_DEFEND.h" @@ -9,32 +9,161 @@ void ACQ_CB_DEFEND(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char evid[96]; + const char *SVC = "ACQ_CB_DEFEND"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_cid, h_pid = 0, h_upd; - char h_evid[110]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; - h_cid = getl(b, T_CANCEL_ID); - gets_(b, T_REASON, evid, sizeof(evid)); - if (evid[0] == 0) strcpy(evid, "EVIDENCE"); - snprintf(h_evid, sizeof(h_evid), "DEFEND:%s", evid); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); - EXEC SQL SELECT purchase_id INTO :h_pid FROM ac_cancel - WHERE cancel_id = :h_cid AND cancel_type = 'CB'; - if (sqlca.sqlcode == 100) { setl(b, T_RC, 4); FAIL(b); } - if (sqlca.sqlcode < 0) FAIL(b); + userlog("%s 진입: 매입 상태 전이 요청 접수", SVC); - EXEC SQL UPDATE ac_cancel SET reason = :h_evid WHERE cancel_id = :h_cid; - if (sqlca.sqlcode < 0) FAIL(b); + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "일반"); + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - /* 방어 접수 시 보류(H) 원매입을 대사완료로 복원 */ - EXEC SQL UPDATE purchase SET status = 'M', updated_at = now() - WHERE purchase_id = :h_pid AND status = 'H'; - if (sqlca.sqlcode < 0) { userlog("ACQ_CB_DEFEND FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } - h_upd = sqlca.sqlerrd[2]; + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; - setl(b, T_PURCHASE_ID, h_pid); - setl(b, T_RC, h_upd > 0 ? 0 : 1); + /* 3) 업무규칙 분기부: 상태 전이 가능여부/유형 판정(3+ 분기) */ + if (!(strcmp(h_prev, "H") == 0)) { + userlog("%s 거절: 상태 %s 에서 전이 불가", SVC, h_prev); + Bchg(b, T_MSG, 0, "상태 전이 불가", 0L); setl(b, T_RC, 120); FAIL(b); + } + /* 방어 대상 CB 행이 실제 존재하는지 확인 */ + EXEC SQL SELECT count(*), coalesce(max(cancel_id),0) INTO :h_cnt, :h_canid + FROM ac_cancel WHERE purchase_id = :h_pid AND cancel_type = 'CB'; + if (sqlca.sqlcode < 0) { + userlog("%s: CB조회 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_cnt == 0) { Bchg(b, T_MSG, 0, "방어 대상 CB 없음", 0L); setl(b, T_RC, 126); FAIL(b); } + if (h_cnt >= 3) { h_val = 3; } + else if (h_cnt == 2) { h_val = 2; } + else { h_val = 1; } + snprintf(logmsg, sizeof(logmsg), "DEFEND:%s", h_reason); + strncpy(h_reason, logmsg, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + + /* 4) 본처리부: 부수행 기록 및 상태 갱신 */ + EXEC SQL UPDATE ac_cancel SET reason = :h_reason WHERE cancel_id = :h_canid; + if (sqlca.sqlcode < 0) { + userlog("%s: 방어사유 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_rem = sqlca.sqlerrd[2]; + EXEC SQL UPDATE purchase SET status = 'M', updated_at = now() WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 상태갱신 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_aff = sqlca.sqlerrd[2]; + if (h_aff > 0) Bchg(b, T_STATUS, 0, "M", 0L); + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "CB방어 pid=%ld CB건수=%ld 반영=%ld 등급=%ld", h_pid, h_cnt, h_rem, h_val); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_CB_DEFEND', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_CB_DEFEND' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_CANCEL_ID, h_canid); + setl(b, T_COUNT, h_cnt); + setl(b, T_ARG1, h_rem); + setl(b, T_ARG2, h_val); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_CB_FINAL.pgc b/app/src/ac/svc/ACQ_CB_FINAL.pgc index 1eb80ca..6286ed2 100644 --- a/app/src/ac/svc/ACQ_CB_FINAL.pgc +++ b/app/src/ac/svc/ACQ_CB_FINAL.pgc @@ -1,7 +1,7 @@ /* * ACQ_CB_FINAL.pgc - ac 모듈 서비스 ACQ_CB_FINAL (one service = one file). * 지불거절 최종확정: 중재 결과로 매입사 패소가 확정되면 원매입을 취소(C)로 - * 확정 전이하고, 확정 여부(반영 건수)를 결과코드로 응답한다. + * 확정 전이하고, 확정 금액을 원장에 손실 분개로 남긴다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_CB_FINAL.h" @@ -9,18 +9,165 @@ void ACQ_CB_FINAL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_CB_FINAL"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_aff; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); - EXEC SQL UPDATE purchase SET status = 'C', updated_at = now() - WHERE purchase_id = :h_pid AND status = 'H'; - if (sqlca.sqlcode < 0) { userlog("ACQ_CB_FINAL FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + userlog("%s 진입: 매입 상태 전이 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "일반"); + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + + /* 3) 업무규칙 분기부: 상태 전이 가능여부/유형 판정(3+ 분기) */ + if (!(strcmp(h_prev, "H") == 0)) { + userlog("%s 거절: 상태 %s 에서 전이 불가", SVC, h_prev); + Bchg(b, T_MSG, 0, "상태 전이 불가", 0L); setl(b, T_RC, 120); FAIL(b); + } + /* 중재 단계까지 진행된 건만 최종확정 대상 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM ac_cancel WHERE purchase_id = :h_pid AND cancel_type LIKE 'CB%'; + if (sqlca.sqlcode < 0) { + userlog("%s: CB이력 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_cnt == 0) { Bchg(b, T_MSG, 0, "확정 대상 CB 없음", 0L); setl(b, T_RC, 127); FAIL(b); } + if (h_sum >= h_gross) { h_val = 3; Bchg(b, T_MSG, 0, "전액 손실 확정", 0L); } + else if (h_sum * 2 >= h_gross) { h_val = 2; Bchg(b, T_MSG, 0, "과반 손실 확정", 0L); } + else { h_val = 1; Bchg(b, T_MSG, 0, "부분 손실 확정", 0L); } + h_delta = -h_sum; + + /* 4) 본처리부: 부수행 기록 및 상태 갱신 */ + EXEC SQL SELECT nextval('ledger_seq') INTO :h_ledid; + if (sqlca.sqlcode < 0) { + userlog("%s: 원장채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) VALUES (:h_ledid, :h_pid, 'CBLOSS', :h_delta, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 손실분개 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL UPDATE purchase SET status = 'C', updated_at = now() WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 상태갱신 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } h_aff = sqlca.sqlerrd[2]; if (h_aff > 0) Bchg(b, T_STATUS, 0, "C", 0L); - setl(b, T_COUNT, h_aff); - setl(b, T_RC, h_aff > 0 ? 0 : 1); + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "CB확정 pid=%ld CB=%ld 손실=%ld 등급=%ld", h_pid, h_cnt, h_sum, h_val); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_CB_FINAL', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_CB_FINAL' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_AMOUNT, h_sum); + setl(b, T_DELTA, h_delta); + setl(b, T_ARG1, h_val); + setl(b, T_ID1, h_ledid); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_CB_OPEN.pgc b/app/src/ac/svc/ACQ_CB_OPEN.pgc index c85ddaa..c5f1a64 100644 --- a/app/src/ac/svc/ACQ_CB_OPEN.pgc +++ b/app/src/ac/svc/ACQ_CB_OPEN.pgc @@ -1,7 +1,7 @@ /* * ACQ_CB_OPEN.pgc - ac 모듈 서비스 ACQ_CB_OPEN (one service = one file). - * 지불거절(chargeback) 접수: 대사완료/정산완료 매입만 접수 가능, ac_cancel 에 - * CB 유형 행을 생성하고 원매입을 보류(H) 상태로 전이한다. + * 지불거절(chargeback) 접수: 대사완료/정산완료 매입만 접수 가능하며 + * ac_cancel 에 CB 유형 행을 생성하고 원매입을 보류(H) 상태로 전이한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_CB_OPEN.h" @@ -9,35 +9,159 @@ void ACQ_CB_OPEN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char reason[128]; + const char *SVC = "ACQ_CB_OPEN"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_id, h_pid, h_amt, h_orig; - char h_st[4], h_bizdate[16], h_reason[140]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 매입 상태 전이 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } h_amt = getl(b, T_AMOUNT); - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - gets_(b, T_REASON, reason, sizeof(reason)); - snprintf(h_reason, sizeof(h_reason), "CB:%s", reason[0] ? reason : "ISSUER"); + if (h_amt < 1L) { Bchg(b, T_MSG, 0, "금액 범위 오류", 0L); setl(b, T_RC, 104); FAIL(b); } + gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "일반"); + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - /* 원매입 상태·금액 검증: 대사완료(M) 또는 정산완료(S)만 지불거절 대상 */ - EXEC SQL SELECT status, amount INTO :h_st, :h_orig FROM purchase WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - if (h_st[0] != 'M' && h_st[0] != 'S') { setl(b, T_RC, 2); FAIL(b); } - if (h_amt <= 0 || h_amt > h_orig) { setl(b, T_RC, 3); FAIL(b); } + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; - 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, 'CB', :h_amt, :h_reason, :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); + /* 3) 업무규칙 분기부: 상태 전이 가능여부/유형 판정(3+ 분기) */ + if (!(strcmp(h_prev, "M") == 0 || strcmp(h_prev, "S") == 0)) { + userlog("%s 거절: 상태 %s 에서 전이 불가", SVC, h_prev); + Bchg(b, T_MSG, 0, "상태 전이 불가", 0L); setl(b, T_RC, 120); FAIL(b); + } + /* 청구액이 원금을 넘지 않는지 확인하고 규모별 등급 산정 */ + if (h_amt > h_gross) { Bchg(b, T_MSG, 0, "청구액이 원금 초과", 0L); setl(b, T_RC, 125); FAIL(b); } + if (h_amt == h_gross) { h_val = 3; Bchg(b, T_MSG, 0, "전액 지불거절", 0L); } + else if (h_amt * 2 >= h_gross) { h_val = 2; Bchg(b, T_MSG, 0, "과반 지불거절", 0L); } + else { h_val = 1; Bchg(b, T_MSG, 0, "부분 지불거절", 0L); } - /* 분쟁 중 원매입은 보류 전이 */ + /* 4) 본처리부: 부수행 기록 및 상태 갱신 */ + EXEC SQL SELECT nextval('ac_cancel_seq') INTO :h_canid; + if (sqlca.sqlcode < 0) { + userlog("%s: CB채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO ac_cancel (cancel_id, purchase_id, cancel_type, amount, reason, biz_date) VALUES (:h_canid, :h_pid, 'CB', :h_amt, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: CB접수행 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } EXEC SQL UPDATE purchase SET status = 'H', updated_at = now() WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0) { userlog("ACQ_CB_OPEN FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + if (sqlca.sqlcode < 0) { + userlog("%s: 상태갱신 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_aff = sqlca.sqlerrd[2]; + if (h_aff > 0) Bchg(b, T_STATUS, 0, "H", 0L); - setl(b, T_CANCEL_ID, h_id); + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "CB접수 pid=%ld 청구=%ld 원금=%ld 등급=%ld", h_pid, h_amt, h_gross, h_val); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_CB_OPEN', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_CB_OPEN' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_CANCEL_ID, h_canid); + setl(b, T_AMOUNT, h_amt); + setl(b, T_GROSS, h_gross); + setl(b, T_ARG1, h_val); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_CB_QRY.pgc b/app/src/ac/svc/ACQ_CB_QRY.pgc index f2a6681..1f481ea 100644 --- a/app/src/ac/svc/ACQ_CB_QRY.pgc +++ b/app/src/ac/svc/ACQ_CB_QRY.pgc @@ -1,7 +1,7 @@ /* * ACQ_CB_QRY.pgc - ac 모듈 서비스 ACQ_CB_QRY (one service = one file). - * 지불거절 목록조회: 가맹점·영업일 기준 CB 접수 건을 커서로 훑어 건수/합계를 - * 집계하고 최근 5건의 접수 ID 를 다중 occurrence 로 반환한다. + * 지불거절 목록조회: 가맹점·영업일 기준 CB 접수 건을 집계해 건수/합계와 + * 가장 최근 접수 ID, 평균 청구액을 반환한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_CB_QRY.h" @@ -9,32 +9,161 @@ void ACQ_CB_QRY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long cnt = 0, total = 0, shown = 0; + const char *SVC = "ACQ_CB_QRY"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_cid, h_amt; - char h_merch[64], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL DECLARE cbq_cur CURSOR FOR - SELECT c.cancel_id, c.amount - FROM ac_cancel c JOIN purchase u ON u.purchase_id = c.purchase_id - WHERE c.cancel_type = 'CB' AND c.biz_date = :h_bizdate AND u.merchant_id = :h_merch - ORDER BY c.cancel_id DESC; - EXEC SQL OPEN cbq_cur; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH cbq_cur INTO :h_cid, :h_amt; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cbq_cur; FAIL(b); } - cnt++; total += h_amt; - if (shown < 5) { Badd(b, T_ID1, (char *)&h_cid, 0L); shown++; } + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + if (merch[0] == 0) { Bchg(b, T_MSG, 0, "가맹점 ID 누락", 0L); setl(b, T_RC, 101); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); } - EXEC SQL CLOSE cbq_cur; + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } - setl(b, T_COUNT, cnt); - setl(b, T_AMOUNT, total); + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + EXEC SQL SELECT name, status, daily_limit INTO :h_str1, :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (strcmp(h_status, "ACTIVE") != 0) + userlog("%s 주의: 가맹점 %s 상태=%s", SVC, h_merch, h_status); + /* 가맹점 MDR(bps) 및 상태분포 컨텍스트 로드 */ + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + EXEC SQL SELECT count(*) FILTER (WHERE status IN ('A','M')), count(*) FILTER (WHERE status = 'S') INTO :h_openc, :h_setc FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 상태분포 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산 진행 컨텍스트(기 정산 순액) */ + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_ctx FROM settlement WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*), coalesce(sum(c.amount),0), coalesce(max(c.cancel_id),0) INTO :h_cnt, :h_gross, :h_top FROM ac_cancel c JOIN purchase p ON p.purchase_id = c.purchase_id WHERE p.merchant_id = :h_merch AND c.biz_date = :h_bizdate AND c.cancel_type LIKE 'CB%'; + if (sqlca.sqlcode < 0) { + userlog("%s: CB집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_a FROM ac_cancel c JOIN purchase p ON p.purchase_id = c.purchase_id WHERE p.merchant_id = :h_merch AND c.biz_date = :h_bizdate AND c.reason LIKE 'DEFEND%'; + if (sqlca.sqlcode < 0) { + userlog("%s: 방어건수 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 방어 진행률로 대응 상태를 판정 */ + h_avg = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + if (h_cnt > 0) ratio = (h_a * 10000L) / h_cnt; else ratio = 0; + if (h_cnt == 0) { h_val = 0; } + else if (ratio >= 8000) { h_val = 1; } + else if (ratio >= 3000) { h_val = 2; } + else { h_val = 3; } + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :h_val2 FROM merchant_summary WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 누적집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_pdgross FROM purchase WHERE merchant_id = :h_merch AND biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_val2 > 0 && h_gross > 0) ratio = (h_gross * 10000L) / (h_val2 + h_gross); + else ratio = 0; + h_share = (h_pdgross > 0) ? (h_gross * 100L / h_pdgross) : 0; + userlog("%s 추이: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_pdgross, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "CB조회 merch=%s 건수=%ld 금액=%ld 방어=%ld", h_merch, h_cnt, h_gross, h_a); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_CB_QRY', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_CB_QRY' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_GROSS, h_gross); + setl(b, T_CANCEL_ID, h_top); + setl(b, T_ARG1, h_a); + setl(b, T_ARG2, h_avg); + setl(b, T_ARG3, ratio); + setl(b, T_ARG4, h_val); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_CB_REVERSE.pgc b/app/src/ac/svc/ACQ_CB_REVERSE.pgc index 8ed892c..89beaf9 100644 --- a/app/src/ac/svc/ACQ_CB_REVERSE.pgc +++ b/app/src/ac/svc/ACQ_CB_REVERSE.pgc @@ -1,7 +1,7 @@ /* * ACQ_CB_REVERSE.pgc - ac 모듈 서비스 ACQ_CB_REVERSE (one service = one file). - * 지불거절 역전: 방어 승소로 CB 를 무효화한다. 최근 CB/CB2 행을 조회해 - * 취소 사유를 REVERSED 로 바꾸고 보류 원매입을 대사완료(M)로 복원한다. + * 지불거절 역전: 방어 승소로 CB 를 무효화한다. 최근 CB/CB2 행 사유를 + * REVERSED 로 바꾸고 보류 원매입을 대사완료(M)로 복원한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_CB_REVERSE.h" @@ -9,25 +9,169 @@ void ACQ_CB_REVERSE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long restored; + const char *SVC = "ACQ_CB_REVERSE"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_cid = 0, h_aff; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 매입 상태 전이 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - EXEC SQL SELECT max(cancel_id) INTO :h_cid FROM ac_cancel - WHERE purchase_id = :h_pid AND cancel_type IN ('CB', 'CB2'); - if (sqlca.sqlcode < 0 || h_cid == 0) { setl(b, T_RC, 4); FAIL(b); } + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; - EXEC SQL UPDATE ac_cancel SET reason = 'REVERSED' WHERE cancel_id = :h_cid; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL UPDATE purchase SET status = 'M', updated_at = now() - WHERE purchase_id = :h_pid AND status = 'H'; - if (sqlca.sqlcode < 0) { userlog("ACQ_CB_REVERSE FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + /* 3) 업무규칙 분기부: 상태 전이 가능여부/유형 판정(3+ 분기) */ + if (!(strcmp(h_prev, "H") == 0)) { + userlog("%s 거절: 상태 %s 에서 전이 불가", SVC, h_prev); + Bchg(b, T_MSG, 0, "상태 전이 불가", 0L); setl(b, T_RC, 120); FAIL(b); + } + /* 역전 대상 CB 행을 찾고 금액 규모로 등급 산정 */ + EXEC SQL SELECT count(*), coalesce(max(cancel_id),0), coalesce(sum(amount),0) + INTO :h_cnt, :h_canid, :h_sum FROM ac_cancel + WHERE purchase_id = :h_pid AND cancel_type LIKE 'CB%'; + if (sqlca.sqlcode < 0) { + userlog("%s: 역전대상 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_cnt == 0) { Bchg(b, T_MSG, 0, "역전 대상 없음", 0L); setl(b, T_RC, 129); FAIL(b); } + if (h_sum >= h_gross) { h_val = 3; } + else if (h_cnt >= 2) { h_val = 2; } + else { h_val = 1; } + + /* 4) 본처리부: 부수행 기록 및 상태 갱신 */ + EXEC SQL UPDATE ac_cancel SET reason = 'REVERSED' WHERE purchase_id = :h_pid AND cancel_type LIKE 'CB%'; + if (sqlca.sqlcode < 0) { + userlog("%s: 역전표시 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_rem = sqlca.sqlerrd[2]; + EXEC SQL SELECT nextval('ledger_seq') INTO :h_ledid; + if (sqlca.sqlcode < 0) { + userlog("%s: 원장채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) VALUES (:h_ledid, :h_pid, 'CBREV', :h_sum, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 역전분개 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL UPDATE purchase SET status = 'M', updated_at = now() WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 상태갱신 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } h_aff = sqlca.sqlerrd[2]; - restored = h_aff; - setl(b, T_CANCEL_ID, h_cid); - setl(b, T_RC, restored > 0 ? 0 : 1); + if (h_aff > 0) Bchg(b, T_STATUS, 0, "M", 0L); + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "CB역전 pid=%ld 대상=%ld 복원=%ld 등급=%ld", h_pid, h_cnt, h_rem, h_val); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_CB_REVERSE', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_CB_REVERSE' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_ARG1, h_rem); + setl(b, T_AMOUNT, h_sum); + setl(b, T_ARG2, h_val); + setl(b, T_ID1, h_ledid); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_CB_RND2.pgc b/app/src/ac/svc/ACQ_CB_RND2.pgc index 4a965dd..e2f8336 100644 --- a/app/src/ac/svc/ACQ_CB_RND2.pgc +++ b/app/src/ac/svc/ACQ_CB_RND2.pgc @@ -9,30 +9,167 @@ void ACQ_CB_RND2(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char why[80]; + const char *SVC = "ACQ_CB_RND2"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_id, h_pid, h_amt, h_prev = 0; - char h_reason[100], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 매입 상태 전이 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } h_amt = getl(b, T_AMOUNT); - gets_(b, T_REASON, why, sizeof(why)); - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_amt < 1L) { Bchg(b, T_MSG, 0, "금액 범위 오류", 0L); setl(b, T_RC, 104); FAIL(b); } + gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "일반"); + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - EXEC SQL SELECT count(*) INTO :h_prev FROM ac_cancel - WHERE purchase_id = :h_pid AND cancel_type = 'CB'; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_prev == 0) { setl(b, T_RC, 4); FAIL(b); } - snprintf(h_reason, sizeof(h_reason), "CB2:%s", why[0] ? why : "REPRESENT"); + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; - 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, 'CB2', :h_amt, :h_reason, :h_bizdate); - if (sqlca.sqlcode < 0) { userlog("ACQ_CB_RND2 FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + /* 3) 업무규칙 분기부: 상태 전이 가능여부/유형 판정(3+ 분기) */ + if (!(strcmp(h_prev, "H") == 0 || strcmp(h_prev, "M") == 0)) { + userlog("%s 거절: 상태 %s 에서 전이 불가", SVC, h_prev); + Bchg(b, T_MSG, 0, "상태 전이 불가", 0L); setl(b, T_RC, 120); FAIL(b); + } + /* 1차 CB 이력 확인 후 재청구액 검증 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM ac_cancel WHERE purchase_id = :h_pid AND cancel_type = 'CB'; + if (sqlca.sqlcode < 0) { + userlog("%s: 1차CB SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_cnt == 0) { Bchg(b, T_MSG, 0, "1차 CB 이력 없음", 0L); setl(b, T_RC, 130); FAIL(b); } + if (h_amt + h_sum > h_gross) { Bchg(b, T_MSG, 0, "누적 청구액이 원금 초과", 0L); setl(b, T_RC, 131); FAIL(b); } + if (h_amt >= h_sum) { h_val = 3; } + else if (h_amt * 2 >= h_sum) { h_val = 2; } + else { h_val = 1; } - setl(b, T_CANCEL_ID, h_id); + /* 4) 본처리부: 부수행 기록 및 상태 갱신 */ + EXEC SQL SELECT nextval('ac_cancel_seq') INTO :h_canid; + if (sqlca.sqlcode < 0) { + userlog("%s: CB2채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO ac_cancel (cancel_id, purchase_id, cancel_type, amount, reason, biz_date) VALUES (:h_canid, :h_pid, 'CB2', :h_amt, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: CB2행 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL UPDATE purchase SET status = 'H', updated_at = now() WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 상태갱신 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_aff = sqlca.sqlerrd[2]; + if (h_aff > 0) Bchg(b, T_STATUS, 0, "H", 0L); + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "CB라운드2 pid=%ld 1차=%ld 재청구=%ld 누적=%ld", h_pid, h_sum, h_amt, h_sum + h_amt); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_CB_RND2', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_CB_RND2' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_CANCEL_ID, h_canid); + setl(b, T_AMOUNT, h_amt); + setl(b, T_AMT1, h_sum); + setl(b, T_ARG1, h_val); + setl(b, T_COUNT, h_cnt); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_CB_STAT.pgc b/app/src/ac/svc/ACQ_CB_STAT.pgc index 3d34192..42910c9 100644 --- a/app/src/ac/svc/ACQ_CB_STAT.pgc +++ b/app/src/ac/svc/ACQ_CB_STAT.pgc @@ -1,7 +1,7 @@ /* * ACQ_CB_STAT.pgc - ac 모듈 서비스 ACQ_CB_STAT (one service = one file). - * 지불거절 일별 통계: 영업일 기준 CB 건수·합계·평균·최대 금액과 방어 완료 - * (DEFEND 사유) 건수를 한 번의 FILTER 집계로 산출한다. + * 지불거절 일별 통계: 영업일 기준 CB 건수·합계·평균·최대 금액과 + * 방어 완료(DEFEND 사유) 건수를 한 번의 FILTER 집계로 산출한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_CB_STAT.h" @@ -9,26 +9,148 @@ void ACQ_CB_STAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_CB_STAT"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt = 0, h_sum = 0, h_avg = 0, h_max = 0, h_def = 0; - char h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL SELECT count(*), - coalesce(sum(amount), 0), - coalesce(avg(amount)::bigint, 0), - coalesce(max(amount), 0), - count(*) FILTER (WHERE reason LIKE 'DEFEND:%') - INTO :h_cnt, :h_sum, :h_avg, :h_max, :h_def - FROM ac_cancel WHERE cancel_type = 'CB' AND biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) { userlog("ACQ_CB_STAT FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } + + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(max(amount),0), coalesce(avg(amount),0)::bigint INTO :h_cnt, :h_gross, :h_max, :h_avg FROM ac_cancel WHERE biz_date = :h_bizdate AND cancel_type LIKE 'CB%'; + if (sqlca.sqlcode < 0) { + userlog("%s: CB통계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*) FILTER (WHERE reason LIKE 'DEFEND%'), count(*) FILTER (WHERE reason LIKE 'ARB%'), count(*) FILTER (WHERE reason LIKE 'REVERSED%') INTO :h_a, :h_b, :h_c FROM ac_cancel WHERE biz_date = :h_bizdate AND cancel_type LIKE 'CB%'; + if (sqlca.sqlcode < 0) { + userlog("%s: 처리단계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 중재 이행 및 역전 비중으로 손실 위험을 판정 */ + if (h_cnt > 0) ratio = (h_c * 10000L) / h_cnt; else ratio = 0; + if (h_cnt == 0) { h_val = 0; } + else if (h_b > 0) { h_val = 3; } + else if (h_a > 0) { h_val = 2; } + else { h_val = 1; } + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "CB통계 bizdate=%s 건수=%ld 합계=%ld 방어=%ld 중재=%ld", h_bizdate, h_cnt, h_gross, h_a, h_b); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_CB_STAT', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_CB_STAT' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_sum); - setl(b, T_AMT1, h_avg); - setl(b, T_AMT2, h_max); - setl(b, T_ARG1, h_def); + setl(b, T_GROSS, h_gross); + setl(b, T_AMOUNT, h_max); + setl(b, T_ARG1, h_avg); + setl(b, T_ARG2, h_a); + setl(b, T_ARG3, h_b); + setl(b, T_ARG4, h_c); + setl(b, T_ARG5, ratio); + setl(b, T_ARG6, h_val); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_CCYQRY.pgc b/app/src/ac/svc/ACQ_CCYQRY.pgc index 913ab58..cb7eeab 100644 --- a/app/src/ac/svc/ACQ_CCYQRY.pgc +++ b/app/src/ac/svc/ACQ_CCYQRY.pgc @@ -9,21 +9,150 @@ void ACQ_CCYQRY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long diff; + const char *SVC = "ACQ_CCYQRY"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_fx = 0, h_krw = 0; - char h_ccy[8]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_CCY, h_ccy, sizeof(h_ccy)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL SELECT coalesce(sum(fx_amount),0), coalesce(sum(krw_amount),0) - INTO :h_fx, :h_krw FROM ac_fx WHERE ccy = :h_ccy; - if (sqlca.sqlcode < 0) FAIL(b); + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); - diff = h_krw - h_fx; - setl(b, T_FX_AMT, h_fx); - setl(b, T_GROSS, h_krw); - setl(b, T_DELTA, diff); + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_CCY, ccy, sizeof(ccy)); + if (ccy[0] == 0) { Bchg(b, T_MSG, 0, "통화코드 누락", 0L); setl(b, T_RC, 103); FAIL(b); } + strncpy(h_ccy, ccy, sizeof(h_ccy)-1); h_ccy[sizeof(h_ccy)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } + + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*), coalesce(sum(fx_amount),0), coalesce(sum(krw_amount),0) INTO :h_cnt, :h_a, :h_gross FROM ac_fx WHERE ccy = :h_ccy AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 통화집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(avg(rate_bps),0)::bigint, coalesce(min(rate_bps),0), coalesce(max(rate_bps),0) INTO :h_rate, :h_min, :h_max FROM ac_fx WHERE ccy = :h_ccy AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 환율범위 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 환산차와 환율 변동폭으로 통화 리스크를 판정 */ + h_b = h_gross - h_a; + h_e = h_max - h_min; + if (h_cnt == 0) { h_val = 0; } + else if (h_min > 0 && h_e * 10000L / h_min > 500) { h_val = 3; } + else if (h_e > 0) { h_val = 2; } + else { h_val = 1; } + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "통화조회 ccy=%s 건수=%ld 원화=%ld 환산차=%ld", h_ccy, h_cnt, h_gross, h_b); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_CCYQRY', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_CCYQRY' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_FX_AMT, h_a); + setl(b, T_GROSS, h_gross); + setl(b, T_ARG1, h_b); + setl(b, T_ARG2, h_rate); + setl(b, T_ARG3, h_e); + setl(b, T_ARG4, h_val); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_CCYRANK.pgc b/app/src/ac/svc/ACQ_CCYRANK.pgc index 5693d32..c6c4f47 100644 --- a/app/src/ac/svc/ACQ_CCYRANK.pgc +++ b/app/src/ac/svc/ACQ_CCYRANK.pgc @@ -1,7 +1,7 @@ /* * ACQ_CCYRANK.pgc - ac 모듈 서비스 ACQ_CCYRANK (one service = one file). - * 매입 통화별 순위: 해외매입 통화 그룹을 원화 합계 내림차순으로 커서 조회해 - * 상위 통화코드·원화액 쌍을 occurrence 로 내리고 1위 통화를 표시한다. + * 매입 통화별 순위: 해외매입 통화를 원화 합계 내림차순으로 정렬해 1위 통화와 + * 그 원화액·점유율을 뽑고 통화 종수를 함께 응답한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_CCYRANK.h" @@ -9,32 +9,151 @@ void ACQ_CCYRANK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long rank = 0; - char top[8]; + const char *SVC = "ACQ_CCYRANK"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_krw; - char h_ccy[8], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - top[0] = 0; - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL DECLARE ccr_cur CURSOR FOR - SELECT ccy, sum(krw_amount) FROM ac_fx WHERE biz_date = :h_bizdate - GROUP BY ccy ORDER BY sum(krw_amount) DESC; - EXEC SQL OPEN ccr_cur; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH ccr_cur INTO :h_ccy, :h_krw; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE ccr_cur; FAIL(b); } - if (rank == 0) { strncpy(top, h_ccy, sizeof(top)-1); top[sizeof(top)-1] = 0; } - if (rank < 5) { Badd(b, T_CCY, h_ccy, 0L); Badd(b, T_AMT1, (char *)&h_krw, 0L); } - rank++; + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); } - EXEC SQL CLOSE ccr_cur; + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } - if (top[0]) Bchg(b, T_STR1, 0, top, 0L); - setl(b, T_ARG1, rank); + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(DISTINCT ccy), coalesce(sum(krw_amount),0), count(*) INTO :h_n, :h_gross, :h_cnt FROM ac_fx WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 통화모집단 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(ccy,''), coalesce(sum(krw_amount),0) INTO :h_ccy, :h_a FROM ac_fx WHERE biz_date = :h_bizdate GROUP BY ccy ORDER BY sum(krw_amount) DESC LIMIT 1; + if (sqlca.sqlcode < 0) { + userlog("%s: 1위통화 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(sum(s),0) INTO :h_b FROM (SELECT sum(krw_amount) AS s FROM ac_fx WHERE biz_date = :h_bizdate GROUP BY ccy ORDER BY sum(krw_amount) DESC LIMIT 2) t2; + if (sqlca.sqlcode < 0) { + userlog("%s: 상위2통화 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 1위 통화 편중도로 통화 분산 수준을 판정 */ + if (h_gross > 0) ratio = (h_a * 10000L) / h_gross; else ratio = 0; + if (h_n <= 1) { h_val = 3; } + else if (ratio >= 7000) { h_val = 2; } + else if (h_n >= 4) { h_val = 0; } + else { h_val = 1; } + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "통화순위 bizdate=%s 종수=%ld 1위=%s 점유=%ld", h_bizdate, h_n, h_ccy, ratio); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_CCYRANK', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_CCYRANK' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_COUNT, h_n); + Bchg(b, T_CCY, 0, h_ccy, 0L); + setl(b, T_AMT1, h_a); + setl(b, T_AMT2, h_b); + setl(b, T_ARG1, ratio); + setl(b, T_ARG2, h_val); + setl(b, T_GROSS, h_gross); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_CCYSUM.pgc b/app/src/ac/svc/ACQ_CCYSUM.pgc index 7364313..8a328d5 100644 --- a/app/src/ac/svc/ACQ_CCYSUM.pgc +++ b/app/src/ac/svc/ACQ_CCYSUM.pgc @@ -9,32 +9,151 @@ void ACQ_CCYSUM(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_CCYSUM"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt = 0, h_fx = 0, h_krw = 0, h_nccy = 0; - char h_ccy[8], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_CCY, h_ccy, sizeof(h_ccy)); - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - if (h_ccy[0]) { - /* 단일 통화 집계 */ - EXEC SQL SELECT count(*), coalesce(sum(fx_amount),0), coalesce(sum(krw_amount),0) - INTO :h_cnt, :h_fx, :h_krw - FROM ac_fx WHERE ccy = :h_ccy AND biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - h_nccy = 1; - } else { - /* 전체 통화 요약: 통화 종수 + 원화 환산 총액 */ - EXEC SQL SELECT count(*), count(DISTINCT ccy), coalesce(sum(krw_amount),0) - INTO :h_cnt, :h_nccy, :h_krw - FROM ac_fx WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) { userlog("ACQ_CCYSUM FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_CCY, ccy, sizeof(ccy)); + strncpy(h_ccy, ccy, sizeof(h_ccy)-1); h_ccy[sizeof(h_ccy)-1] = 0; + k = (h_ccy[0] != 0) ? 1 : 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } + + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(DISTINCT ccy), count(*), coalesce(sum(krw_amount),0), coalesce(sum(fx_amount),0) INTO :h_n, :h_cnt, :h_gross, :h_a FROM ac_fx WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 전체통화 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); } + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 통화 지정 여부에 따라 집계 범위를 달리한다 */ + if (k == 1) { + EXEC SQL SELECT count(*), coalesce(sum(krw_amount),0), coalesce(sum(fx_amount),0) + INTO :h_cnt, :h_gross, :h_a FROM ac_fx + WHERE biz_date = :h_bizdate AND ccy = :h_ccy; + if (sqlca.sqlcode < 0) { userlog("%s: 지정통화 집계 실패", SVC); setl(b, T_RC, 90); FAIL(b); } + h_n = (h_cnt > 0) ? 1 : 0; + h_val = 1; + } else if (h_n >= 3) { + h_val = 2; + } else { + h_val = 0; + } + h_avg = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "통화집계 bizdate=%s 지정=%ld 종수=%ld 원화=%ld", h_bizdate, k, h_n, h_gross); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_CCYSUM', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_CCYSUM' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ setl(b, T_COUNT, h_cnt); - setl(b, T_FX_AMT, h_fx); - setl(b, T_AMOUNT, h_krw); - setl(b, T_ARG1, h_nccy); + setl(b, T_ARG1, h_n); + setl(b, T_GROSS, h_gross); + setl(b, T_FX_AMT, h_a); + setl(b, T_ARG2, h_avg); + setl(b, T_ARG3, h_val); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_CHANDETAIL.pgc b/app/src/ac/svc/ACQ_CHANDETAIL.pgc index b2a7a3d..51619f9 100644 --- a/app/src/ac/svc/ACQ_CHANDETAIL.pgc +++ b/app/src/ac/svc/ACQ_CHANDETAIL.pgc @@ -1,7 +1,7 @@ /* * ACQ_CHANDETAIL.pgc - ac 모듈 서비스 ACQ_CHANDETAIL (one service = one file). - * 매입 채널별 상세 조회: 영업일 채널 그룹의 건수/총액/평균을 커서로 훑어 - * 채널명·평균단가 쌍을 occurrence 로 내리고 전체 평균을 함께 반환한다. + * 매입 채널별 상세 조회: 지정 채널의 건수/총액/평균 단가와 전체 평균 대비 + * 편차를 산출해 채널 단가 특성을 응답한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_CHANDETAIL.h" @@ -9,31 +9,156 @@ void ACQ_CHANDETAIL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long tot = 0, cnt = 0, kinds = 0; + const char *SVC = "ACQ_CHANDETAIL"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_n, h_sum, h_avg; - char h_chan[16], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL DECLARE chd_cur CURSOR FOR - SELECT channel, count(*), sum(amount), avg(amount)::bigint FROM purchase - WHERE biz_date = :h_bizdate AND status <> 'C' GROUP BY channel ORDER BY channel; - EXEC SQL OPEN chd_cur; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH chd_cur INTO :h_chan, :h_n, :h_sum, :h_avg; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE chd_cur; FAIL(b); } - if (kinds < 8) { Badd(b, T_STR1, h_chan, 0L); Badd(b, T_AMT1, (char *)&h_avg, 0L); } - tot += h_sum; cnt += h_n; kinds++; + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_CHANNEL, channel, sizeof(channel)); if (channel[0] == 0) strcpy(channel, "POS"); + strncpy(h_chan, channel, sizeof(h_chan)-1); h_chan[sizeof(h_chan)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); } - EXEC SQL CLOSE chd_cur; + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } - setl(b, T_GROSS, tot); - setl(b, T_COUNT, cnt); - setl(b, T_ARG1, kinds); - setl(b, T_AMT2, cnt > 0 ? tot / cnt : 0); + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(avg(amount),0)::bigint INTO :h_cnt, :h_gross, :h_avg FROM purchase WHERE biz_date = :h_bizdate AND channel = :h_chan; + if (sqlca.sqlcode < 0) { + userlog("%s: 채널집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(avg(amount),0)::bigint INTO :h_a, :h_b FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 전체평균 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(DISTINCT channel) INTO :h_n FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 채널종수 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 채널 평균단가를 전체 평균과 비교해 고단가/저단가로 분류 */ + h_e = h_avg - h_b; + if (h_cnt == 0) { h_val = 0; } + else if (h_b > 0 && h_avg > h_b * 2) { h_val = 3; } + else if (h_avg > h_b) { h_val = 2; } + else { h_val = 1; } + if (h_a > 0) ratio = (h_cnt * 10000L) / h_a; else ratio = 0; + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "채널상세 ch=%s 건수=%ld 평균=%ld 편차=%ld", h_chan, h_cnt, h_avg, h_e); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_CHANDETAIL', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_CHANDETAIL' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + Bchg(b, T_CHANNEL, 0, h_chan, 0L); + setl(b, T_COUNT, h_cnt); + setl(b, T_GROSS, h_gross); + setl(b, T_ARG1, h_avg); + setl(b, T_ARG2, h_b); + setl(b, T_ARG3, h_e); + setl(b, T_ARG4, ratio); + setl(b, T_ARG5, h_val); + setl(b, T_ID1, h_n); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_CHANRANK.pgc b/app/src/ac/svc/ACQ_CHANRANK.pgc index a4259d2..a810620 100644 --- a/app/src/ac/svc/ACQ_CHANRANK.pgc +++ b/app/src/ac/svc/ACQ_CHANRANK.pgc @@ -1,7 +1,7 @@ /* * ACQ_CHANRANK.pgc - ac 모듈 서비스 ACQ_CHANRANK (one service = one file). - * 매입 채널별 순위: 영업일 채널을 건수 내림차순으로 정렬해 커서 조회하고 - * 채널명·건수 쌍을 occurrence 로 내려 최다 유입 채널을 판별한다. + * 매입 채널별 순위: 영업일 채널을 건수 내림차순으로 정렬해 최다 유입 채널과 + * 그 건수/점유율을 판별하고 채널 종수를 함께 낸다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_CHANRANK.h" @@ -9,33 +9,154 @@ void ACQ_CHANRANK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long rank = 0, busiest = 0; - char topchan[16]; + const char *SVC = "ACQ_CHANRANK"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_n; - char h_chan[16], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - topchan[0] = 0; - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL DECLARE chr_cur CURSOR FOR - SELECT channel, count(*) FROM purchase WHERE biz_date = :h_bizdate - GROUP BY channel ORDER BY count(*) DESC, channel; - EXEC SQL OPEN chr_cur; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH chr_cur INTO :h_chan, :h_n; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE chr_cur; FAIL(b); } - if (rank == 0) { strncpy(topchan, h_chan, sizeof(topchan)-1); topchan[sizeof(topchan)-1] = 0; busiest = h_n; } - if (rank < 6) { Badd(b, T_CHANNEL, h_chan, 0L); Badd(b, T_ARG3, (char *)&h_n, 0L); } - rank++; + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); } - EXEC SQL CLOSE chr_cur; + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } - if (topchan[0]) Bchg(b, T_STR2, 0, topchan, 0L); - setl(b, T_COUNT, busiest); - setl(b, T_ARG1, rank); + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*), count(DISTINCT channel), coalesce(sum(amount),0) INTO :h_cnt, :h_n, :h_gross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 모집단 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(channel,''), count(*), coalesce(sum(amount),0) INTO :h_chan, :h_a, :h_b FROM purchase WHERE biz_date = :h_bizdate GROUP BY channel ORDER BY count(*) DESC LIMIT 1; + if (sqlca.sqlcode < 0) { + userlog("%s: 최다채널 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(channel,'') INTO :h_str2 FROM purchase WHERE biz_date = :h_bizdate GROUP BY channel ORDER BY count(*) ASC LIMIT 1; + if (sqlca.sqlcode < 0) { + userlog("%s: 최소채널 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 최다 채널 점유율로 유입 편중을 판정 */ + if (h_cnt > 0) ratio = (h_a * 10000L) / h_cnt; else ratio = 0; + if (h_cnt == 0) { h_val = 0; } + else if (ratio >= 8000) { h_val = 3; } + else if (ratio >= 5000) { h_val = 2; } + else { h_val = 1; } + h_avg = (h_a > 0) ? (h_b / h_a) : 0; + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "채널순위 bizdate=%s 최다=%s 건수=%ld 점유=%ld", h_bizdate, h_chan, h_a, ratio); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_CHANRANK', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_CHANRANK' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + Bchg(b, T_CHANNEL, 0, h_chan, 0L); + Bchg(b, T_STR2, 0, h_str2, 0L); + setl(b, T_COUNT, h_a); + setl(b, T_AMT1, h_b); + setl(b, T_ARG1, ratio); + setl(b, T_ARG2, h_n); + setl(b, T_ARG3, h_val); + setl(b, T_ARG4, h_avg); + setl(b, T_GROSS, h_gross); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_CHANSTAT.pgc b/app/src/ac/svc/ACQ_CHANSTAT.pgc index 0e3dbd2..96b3169 100644 --- a/app/src/ac/svc/ACQ_CHANSTAT.pgc +++ b/app/src/ac/svc/ACQ_CHANSTAT.pgc @@ -1,7 +1,7 @@ /* * ACQ_CHANSTAT.pgc - ac 모듈 서비스 ACQ_CHANSTAT (one service = one file). - * 매입 채널별 통계: 영업일 채널 그룹 집계를 커서로 순회하며 채널 종수와 - * 최대 매출 채널을 찾고, 채널명/금액 쌍을 다중 occurrence 로 반환한다. + * 매입 채널별 통계: 영업일 채널을 POS/EDC/EDI/해외 4군으로 분해 집계해 + * 군별 건수와 금액을 산출하고 최대 매출 채널군을 판정한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_CHANSTAT.h" @@ -9,37 +9,152 @@ void ACQ_CHANSTAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long kinds = 0, best = 0; - char bestchan[16]; + const char *SVC = "ACQ_CHANSTAT"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_sum, h_n; - char h_chan[16], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - bestchan[0] = 0; - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL DECLARE chs_cur CURSOR FOR - SELECT channel, count(*), sum(amount) FROM purchase - WHERE biz_date = :h_bizdate AND status <> 'C' - GROUP BY channel ORDER BY sum(amount) DESC; - EXEC SQL OPEN chs_cur; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH chs_cur INTO :h_chan, :h_n, :h_sum; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE chs_cur; FAIL(b); } - if (kinds == 0) { /* 정렬 첫 행 = 최대 매출 채널 */ - strncpy(bestchan, h_chan, sizeof(bestchan)-1); bestchan[sizeof(bestchan)-1] = 0; - best = h_sum; - } - if (kinds < 6) { Badd(b, T_STR2, h_chan, 0L); Badd(b, T_AMT3, (char *)&h_sum, 0L); } - kinds++; + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); } - EXEC SQL CLOSE chs_cur; + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } - if (bestchan[0]) Bchg(b, T_CHANNEL, 0, bestchan, 0L); - setl(b, T_AMT1, best); - setl(b, T_ARG1, kinds); /* 채널 종수 */ + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT coalesce(sum(amount) FILTER (WHERE channel = 'POS'),0), coalesce(sum(amount) FILTER (WHERE channel = 'EDC'),0), coalesce(sum(amount) FILTER (WHERE channel = 'EDI'),0), coalesce(sum(amount) FILTER (WHERE channel = 'FOREIGN'),0) INTO :h_a, :h_b, :h_c, :h_d FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 채널군금액 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), count(DISTINCT channel) INTO :h_cnt, :h_gross, :h_n FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 총계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 4개 채널군 중 최대 매출군을 인덱스로 판정 */ + h_max = h_a; h_top = 1; + if (h_b > h_max) { h_max = h_b; h_top = 2; } + if (h_c > h_max) { h_max = h_c; h_top = 3; } + if (h_d > h_max) { h_max = h_d; h_top = 4; } + if (h_gross > 0) ratio = (h_max * 10000L) / h_gross; else ratio = 0; + if (h_d > 0) { h_val = 2; } + else if (h_c > 0) { h_val = 1; } + else { h_val = 0; } + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "채널통계 bizdate=%s POS=%ld EDC=%ld EDI=%ld 해외=%ld", h_bizdate, h_a, h_b, h_c, h_d); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_CHANSTAT', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_CHANSTAT' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_AMT1, h_a); + setl(b, T_AMT2, h_b); + setl(b, T_AMT3, h_c); + setl(b, T_AMT4, h_d); + setl(b, T_ARG1, h_top); + setl(b, T_ARG2, ratio); + setl(b, T_ARG3, h_val); + setl(b, T_COUNT, h_cnt); + setl(b, T_GROSS, h_gross); + setl(b, T_ID1, h_n); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_CLAIMPROC.pgc b/app/src/ac/svc/ACQ_CLAIMPROC.pgc index e784287..2e77c90 100644 --- a/app/src/ac/svc/ACQ_CLAIMPROC.pgc +++ b/app/src/ac/svc/ACQ_CLAIMPROC.pgc @@ -1,7 +1,7 @@ /* * ACQ_CLAIMPROC.pgc - ac 모듈 서비스 ACQ_CLAIMPROC (one service = one file). * 클레임 처리(종결): 등록된 클레임 행을 삭제하고 처리 결과를 원장에 - * CLAIMRES 분개(금액 0)로 남긴다. 클레임이 아니거나 없으면 T_RC=4. + * CLAIMRES 분개로 남긴다. 클레임이 없으면 결과코드로 알린다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_CLAIMPROC.h" @@ -9,30 +9,163 @@ void ACQ_CLAIMPROC(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], result[64]; + const char *SVC = "ACQ_CLAIMPROC"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_uid, h_pid = 0, h_del; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; - h_uid = getl(b, T_ID1); - gets_(b, T_STR1, result, sizeof(result)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 매입 상태 전이 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "일반"); + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (result[0] == 0) strcpy(result, "ACCEPTED"); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - EXEC SQL SELECT purchase_id INTO :h_pid FROM ac_unmatch - WHERE unmatch_id = :h_uid AND reason LIKE 'CLAIM:%'; - if (sqlca.sqlcode == 100) { setl(b, T_RC, 4); FAIL(b); } - if (sqlca.sqlcode < 0) FAIL(b); + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; - EXEC SQL DELETE FROM ac_unmatch WHERE unmatch_id = :h_uid; - if (sqlca.sqlcode < 0) FAIL(b); - h_del = sqlca.sqlerrd[2]; + /* 3) 업무규칙 분기부: 상태 전이 가능여부/유형 판정(3+ 분기) */ + if (!(strcmp(h_prev, "A") == 0 || strcmp(h_prev, "M") == 0 || strcmp(h_prev, "S") == 0 || strcmp(h_prev, "C") == 0 || strcmp(h_prev, "H") == 0 || strcmp(h_prev, "V") == 0 || strcmp(h_prev, "P") == 0)) { + userlog("%s 거절: 상태 %s 에서 전이 불가", SVC, h_prev); + Bchg(b, T_MSG, 0, "상태 전이 불가", 0L); setl(b, T_RC, 120); FAIL(b); + } + /* 종결 대상 클레임 수를 확인하고 처리 방식을 결정 */ + EXEC SQL SELECT count(*) INTO :h_cnt FROM ac_unmatch + WHERE purchase_id = :h_pid AND reason LIKE 'CLAIM%'; + if (sqlca.sqlcode < 0) { + userlog("%s: 클레임조회 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_cnt == 0) { Bchg(b, T_MSG, 0, "종결 대상 클레임 없음", 0L); setl(b, T_RC, 133); FAIL(b); } + if (h_cnt >= 3) { h_val = 3; } + else if (h_cnt == 2) { h_val = 2; } + else { h_val = 1; } - /* 처리 흔적을 원장에 남긴다 (금액 0, 유형 CLAIMRES) */ - if (h_del > 0 && acdb_insert_ledger(h_pid, "CLAIMRES", 0, bizdate) < 0) FAIL(b); + /* 4) 본처리부: 부수행 기록 및 상태 갱신 */ + EXEC SQL DELETE FROM ac_unmatch WHERE purchase_id = :h_pid AND reason LIKE 'CLAIM%'; + if (sqlca.sqlcode < 0) { + userlog("%s: 클레임종결 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_rem = sqlca.sqlerrd[2]; + EXEC SQL SELECT nextval('ledger_seq') INTO :h_ledid; + if (sqlca.sqlcode < 0) { + userlog("%s: 원장채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) VALUES (:h_ledid, :h_pid, 'CLAIMRES', 0, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 종결분개 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_aff = 1; - Bchg(b, T_MSG, 0, result, 0L); - setl(b, T_PURCHASE_ID, h_pid); - setl(b, T_RC, h_del > 0 ? 0 : 1); + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "클레임종결 pid=%ld 대상=%ld 삭제=%ld 사유=%s", h_pid, h_cnt, h_rem, h_reason); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_CLAIMPROC', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_CLAIMPROC' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_ARG1, h_rem); + setl(b, T_ARG2, h_val); + setl(b, T_ID1, h_ledid); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_CLAIMQRY.pgc b/app/src/ac/svc/ACQ_CLAIMQRY.pgc index fe8cadb..7dff5eb 100644 --- a/app/src/ac/svc/ACQ_CLAIMQRY.pgc +++ b/app/src/ac/svc/ACQ_CLAIMQRY.pgc @@ -1,7 +1,7 @@ /* * ACQ_CLAIMQRY.pgc - ac 모듈 서비스 ACQ_CLAIMQRY (one service = one file). - * 클레임 목록조회: 영업일의 미종결 클레임을 커서로 순회, 총건수와 함께 - * 상위 5건의 클레임 ID/사유를 다중 occurrence 로 내려준다. + * 클레임 목록조회: 영업일의 미종결 클레임(ac_unmatch CLAIM)을 집계해 + * 총건수와 최근 클레임 ID, 연루 매입 수를 내려준다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_CLAIMQRY.h" @@ -9,34 +9,150 @@ void ACQ_CLAIMQRY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long total = 0, listed = 0; + const char *SVC = "ACQ_CLAIMQRY"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_uid; - char h_reason[128], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL DECLARE clq_cur CURSOR FOR - SELECT unmatch_id, reason FROM ac_unmatch - WHERE reason LIKE 'CLAIM:%' AND biz_date = :h_bizdate - ORDER BY unmatch_id; - EXEC SQL OPEN clq_cur; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH clq_cur INTO :h_uid, :h_reason; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE clq_cur; FAIL(b); } - total++; - if (listed < 5) { - Badd(b, T_ID1, (char *)&h_uid, 0L); - Badd(b, T_STR1, h_reason, 0L); - listed++; - } + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); } - EXEC SQL CLOSE clq_cur; + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } - setl(b, T_COUNT, total); - setl(b, T_ARG1, listed); + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*), coalesce(max(unmatch_id),0), count(DISTINCT purchase_id) INTO :h_cnt, :h_top, :h_n FROM ac_unmatch WHERE biz_date = :h_bizdate AND reason LIKE 'CLAIM%'; + if (sqlca.sqlcode < 0) { + userlog("%s: 클레임집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(sum(p.amount),0) INTO :h_gross FROM ac_unmatch u JOIN purchase p ON p.purchase_id = u.purchase_id WHERE u.biz_date = :h_bizdate AND u.reason LIKE 'CLAIM%'; + if (sqlca.sqlcode < 0) { + userlog("%s: 연루금액 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_a FROM ac_unmatch WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 전체미매입 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 클레임 비중으로 처리 우선순위를 결정 */ + if (h_a > 0) ratio = (h_cnt * 10000L) / h_a; else ratio = 0; + if (h_cnt == 0) { h_val = 0; } + else if (h_cnt >= 10) { h_val = 3; } + else if (h_cnt >= 3) { h_val = 2; } + else { h_val = 1; } + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "클레임조회 bizdate=%s 건수=%ld 매입=%ld 금액=%ld", h_bizdate, h_cnt, h_n, h_gross); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_CLAIMQRY', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_CLAIMQRY' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_ID1, h_top); + setl(b, T_ARG1, h_n); + setl(b, T_GROSS, h_gross); + setl(b, T_ARG2, ratio); + setl(b, T_ARG3, h_val); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_CLAIMREG.pgc b/app/src/ac/svc/ACQ_CLAIMREG.pgc index 58f06eb..ffaba8f 100644 --- a/app/src/ac/svc/ACQ_CLAIMREG.pgc +++ b/app/src/ac/svc/ACQ_CLAIMREG.pgc @@ -1,7 +1,7 @@ /* * ACQ_CLAIMREG.pgc - ac 모듈 서비스 ACQ_CLAIMREG (one service = one file). * 클레임 등록: 가맹점 이의신청을 미매입 원장(ac_unmatch)에 CLAIM 사유로 - * 기록한다. 동일 매입에 미종결 클레임이 이미 있으면 중복 등록을 막는다. + * 기록한다. 동일 매입에 미종결 클레임이 있으면 중복 등록을 막는다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_CLAIMREG.h" @@ -9,34 +9,159 @@ void ACQ_CLAIMREG(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char why[100]; + const char *SVC = "ACQ_CLAIMREG"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_id, h_pid, h_exist = 0, h_have = 0; - char h_reason[120], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 매입 상태 전이 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ h_pid = getl(b, T_PURCHASE_ID); - gets_(b, T_REASON, why, sizeof(why)); - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - snprintf(h_reason, sizeof(h_reason), "CLAIM:%s", why[0] ? why : "MERCHANT"); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "일반"); + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - EXEC SQL SELECT count(*) INTO :h_exist FROM purchase WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_exist == 0) { setl(b, T_RC, 5); FAIL(b); } + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; - /* 미종결 클레임 중복 방지 */ - EXEC SQL SELECT count(*) INTO :h_have FROM ac_unmatch - WHERE purchase_id = :h_pid AND reason LIKE 'CLAIM:%'; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_have > 0) { setl(b, T_RC, 2); FAIL(b); } + /* 3) 업무규칙 분기부: 상태 전이 가능여부/유형 판정(3+ 분기) */ + if (!(strcmp(h_prev, "A") == 0 || strcmp(h_prev, "M") == 0 || strcmp(h_prev, "S") == 0 || strcmp(h_prev, "C") == 0 || strcmp(h_prev, "H") == 0 || strcmp(h_prev, "V") == 0 || strcmp(h_prev, "P") == 0)) { + userlog("%s 거절: 상태 %s 에서 전이 불가", SVC, h_prev); + Bchg(b, T_MSG, 0, "상태 전이 불가", 0L); setl(b, T_RC, 120); FAIL(b); + } + /* 미종결 클레임 중복 여부를 먼저 확인 */ + EXEC SQL SELECT count(*) INTO :h_cnt FROM ac_unmatch + WHERE purchase_id = :h_pid AND reason LIKE 'CLAIM%'; + if (sqlca.sqlcode < 0) { + userlog("%s: 중복확인 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_cnt > 0) { Bchg(b, T_MSG, 0, "미종결 클레임 존재", 0L); setl(b, T_RC, 132); FAIL(b); } + if (h_gross >= 1000000) { h_val = 3; } + else if (strcmp(h_prev, "S") == 0) { h_val = 2; } + else { h_val = 1; } + snprintf(logmsg, sizeof(logmsg), "CLAIM:%s", h_reason); + strncpy(h_reason, logmsg, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + /* 4) 본처리부: 부수행 기록 및 상태 갱신 */ EXEC SQL SELECT nextval('ac_unmatch_seq') INTO :h_id; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL INSERT INTO ac_unmatch (unmatch_id, purchase_id, reason, biz_date) - VALUES (:h_id, :h_pid, :h_reason, :h_bizdate); - if (sqlca.sqlcode < 0) { userlog("ACQ_CLAIMREG FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + if (sqlca.sqlcode < 0) { + userlog("%s: 클레임채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO ac_unmatch (unmatch_id, purchase_id, reason, biz_date) VALUES (:h_id, :h_pid, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 클레임등록 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_aff = 1; + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "클레임등록 pid=%ld id=%ld 상태=%s 등급=%ld", h_pid, h_id, h_prev, h_val); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_CLAIMREG', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_CLAIMREG' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ setl(b, T_ID1, h_id); + setl(b, T_ARG1, h_val); + setl(b, T_AMOUNT, h_gross); + Bchg(b, T_STATUS, 0, h_prev, 0L); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_CORRAPPR.pgc b/app/src/ac/svc/ACQ_CORRAPPR.pgc index e469424..d5f9b55 100644 --- a/app/src/ac/svc/ACQ_CORRAPPR.pgc +++ b/app/src/ac/svc/ACQ_CORRAPPR.pgc @@ -9,22 +9,151 @@ void ACQ_CORRAPPR(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_CORRAPPR"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_id, h_aff; - char h_reason[128]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; - h_id = getl(b, T_ADJ); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); - EXEC SQL SELECT reason INTO :h_reason FROM ac_fee_adj WHERE adj_id = :h_id; - if (sqlca.sqlcode == 100) { setl(b, T_RC, 5); FAIL(b); } - if (sqlca.sqlcode < 0) FAIL(b); - if (strncmp(h_reason, "OK:", 3) == 0) { setl(b, T_RC, 2); FAIL(b); } + userlog("%s 진입: 매입 상태 전이 요청 접수", SVC); - EXEC SQL UPDATE ac_fee_adj SET reason = 'OK:' || reason WHERE adj_id = :h_id; - if (sqlca.sqlcode < 0) { userlog("ACQ_CORRAPPR FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } - h_aff = sqlca.sqlerrd[2]; - setl(b, T_ADJ, h_id); - setl(b, T_RC, h_aff > 0 ? 0 : 1); + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + + /* 3) 업무규칙 분기부: 상태 전이 가능여부/유형 판정(3+ 분기) */ + if (!(strcmp(h_prev, "A") == 0 || strcmp(h_prev, "M") == 0 || strcmp(h_prev, "S") == 0 || strcmp(h_prev, "C") == 0 || strcmp(h_prev, "H") == 0 || strcmp(h_prev, "V") == 0 || strcmp(h_prev, "P") == 0)) { + userlog("%s 거절: 상태 %s 에서 전이 불가", SVC, h_prev); + Bchg(b, T_MSG, 0, "상태 전이 불가", 0L); setl(b, T_RC, 120); FAIL(b); + } + /* 미승인 조정 건이 있어야 승인 처리 가능 */ + EXEC SQL SELECT count(*), coalesce(sum(delta),0) INTO :h_cnt, :h_delta + FROM ac_fee_adj WHERE purchase_id = :h_pid AND reason NOT LIKE 'OK:%'; + if (sqlca.sqlcode < 0) { + userlog("%s: 미승인조정 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_cnt == 0) { Bchg(b, T_MSG, 0, "승인 대상 정정 없음", 0L); setl(b, T_RC, 138); FAIL(b); } + if (h_delta > 0) { h_val = 3; Bchg(b, T_MSG, 0, "증액 정정 승인", 0L); } + else if (h_delta < 0) { h_val = 2; Bchg(b, T_MSG, 0, "감액 정정 승인", 0L); } + else { h_val = 1; Bchg(b, T_MSG, 0, "중립 정정 승인", 0L); } + + /* 4) 본처리부: 부수행 기록 및 상태 갱신 */ + EXEC SQL UPDATE ac_fee_adj SET reason = 'OK:' || reason WHERE purchase_id = :h_pid AND reason NOT LIKE 'OK:%'; + if (sqlca.sqlcode < 0) { + userlog("%s: 승인표식 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_rem = sqlca.sqlerrd[2]; + h_aff = 1; + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "정정승인 pid=%ld 대상=%ld 승인=%ld 증감=%ld", h_pid, h_cnt, h_rem, h_delta); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_CORRAPPR', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_CORRAPPR' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_ARG1, h_rem); + setl(b, T_DELTA, h_delta); + setl(b, T_ARG2, h_val); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_CORRECT.pgc b/app/src/ac/svc/ACQ_CORRECT.pgc index 9c801d7..1d51771 100644 --- a/app/src/ac/svc/ACQ_CORRECT.pgc +++ b/app/src/ac/svc/ACQ_CORRECT.pgc @@ -1,20 +1,157 @@ /* * ACQ_CORRECT.pgc - ac 모듈 서비스 ACQ_CORRECT (one service = one file). - * Moved verbatim from the former inline ac_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by ac_svr at tpsvrinit. + * 매입 정정: 금액/수수료를 함께 정정한다. 정정 전후 값을 조정 원장에 + * 남기고 purchase 를 한 번에 갱신해 정합을 유지한다. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_CORRECT.h" 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); + const char *SVC = "ACQ_CORRECT"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; + EXEC SQL BEGIN DECLARE SECTION; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; + EXEC SQL END DECLARE SECTION; + + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 매입 금액/수수료 조정 요청", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + h_amt = getl(b, T_AMOUNT); + if (h_amt < 1L) { Bchg(b, T_MSG, 0, "금액 범위 오류", 0L); setl(b, T_RC, 104); FAIL(b); } + gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "일반"); + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + + /* 3) 업무규칙 분기부: 조정 유형/부호/한도 판정(3+ 분기) */ + /* 정정 폭에 따라 승인 등급을 나누고 정합을 검증 */ + h_delta = h_amt - h_gross; + if (h_amt <= 0) { Bchg(b, T_MSG, 0, "정정 금액 오류", 0L); setl(b, T_RC, 175); FAIL(b); } + computed = acq_fee(h_amt, h_rate); + if (h_delta == 0 && computed == h_fee) { Bchg(b, T_MSG, 0, "정정 대상 없음", 0L); setl(b, T_RC, 176); FAIL(b); } + if (h_gross > 0 && (h_delta > h_gross / 2 || h_delta < -(h_gross / 2))) { h_val = 3; } + else if (h_delta != 0) { h_val = 2; } + else { h_val = 1; } + + /* 4) 본처리부: 조정 원장 기록 및 매입 금액 보정 */ + h_adjid = acdb_insert_fee_adj(h_pid, computed - h_fee, h_reason, h_bizdate); + if (h_adjid < 0) { Bchg(b, T_MSG, 0, "정정 이력 실패", 0L); setl(b, T_RC, 98); FAIL(b); } + h_fee = computed; h_net = h_amt - h_fee; + if (acdb_update_amount(h_pid, h_amt, h_fee, h_net) < 0) + { Bchg(b, T_MSG, 0, "정정 반영 실패", 0L); setl(b, T_RC, 97); FAIL(b); } + h_aff = 1; + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "매입정정 pid=%ld 금액 %ld -> %ld 수수료=%ld", h_pid, h_gross, h_amt, h_fee); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_CORRECT', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_CORRECT' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_ADJ, h_adjid); + setl(b, T_AMOUNT, h_amt); + setl(b, T_FEE, h_fee); + setl(b, T_NET, h_net); + setl(b, T_DELTA, h_delta); + setl(b, T_ARG1, h_val); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_CORRHIST.pgc b/app/src/ac/svc/ACQ_CORRHIST.pgc index ffa0735..8f28a02 100644 --- a/app/src/ac/svc/ACQ_CORRHIST.pgc +++ b/app/src/ac/svc/ACQ_CORRHIST.pgc @@ -1,7 +1,7 @@ /* * ACQ_CORRHIST.pgc - ac 모듈 서비스 ACQ_CORRHIST (one service = one file). - * 정정 이력조회: 매입 건에 붙은 수수료 조정 이력(최근 10건)을 역순 커서로 - * 내려주고, 취소계 정정(ac_cancel) 건수를 별도 카운트로 함께 응답한다. + * 정정 이력조회: 매입 건에 붙은 수수료 조정 이력의 건수·순증감과 + * 취소계 정정(ac_cancel) 건수를 함께 집계해 응답한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_CORRHIST.h" @@ -9,33 +9,154 @@ void ACQ_CORRHIST(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long rows = 0; + const char *SVC = "ACQ_CORRHIST"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_adj, h_delta, h_cxl = 0; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ h_pid = getl(b, T_PURCHASE_ID); - - EXEC SQL DECLARE crh_cur CURSOR FOR - SELECT adj_id, delta FROM ac_fee_adj - WHERE purchase_id = :h_pid ORDER BY adj_id DESC LIMIT 10; - EXEC SQL OPEN crh_cur; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH crh_cur INTO :h_adj, :h_delta; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE crh_cur; FAIL(b); } - Badd(b, T_ADJ, (char *)&h_adj, 0L); - Badd(b, T_DELTA, (char *)&h_delta, 0L); - rows++; + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); } - EXEC SQL CLOSE crh_cur; - /* 취소·환불류 정정 건수 병기 */ - EXEC SQL SELECT count(*) INTO :h_cxl FROM ac_cancel WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0) { userlog("ACQ_CORRHIST FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - setl(b, T_COUNT, rows); - setl(b, T_ARG1, h_cxl); + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*), coalesce(sum(delta),0), coalesce(max(adj_id),0) INTO :h_cnt, :h_a, :h_top FROM ac_fee_adj WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 조정이력 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_b, :h_gross FROM ac_cancel WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 취소이력 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT status, amount, fee INTO :h_status, :h_amt, :h_fee FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입현황 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 정정 유형 조합으로 이력 성격을 분류 */ + h_e = h_cnt + h_b; + if (h_e == 0) { h_val = 0; Bchg(b, T_MSG, 0, "정정 이력 없음", 0L); } + else if (h_cnt > 0 && h_b > 0) { h_val = 3; Bchg(b, T_MSG, 0, "수수료/취소 복합 정정", 0L); } + else if (h_b > 0) { h_val = 2; Bchg(b, T_MSG, 0, "취소계 정정", 0L); } + else { h_val = 1; Bchg(b, T_MSG, 0, "수수료 조정", 0L); } + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "정정이력 pid=%ld 조정=%ld 순증감=%ld 취소=%ld", h_pid, h_cnt, h_a, h_b); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_CORRHIST', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_CORRHIST' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_DELTA, h_a); + setl(b, T_ARG1, h_b); + setl(b, T_ARG2, h_e); + setl(b, T_ARG3, h_val); + setl(b, T_ADJ, h_top); + setl(b, T_GROSS, h_gross); + Bchg(b, T_STATUS, 0, h_status, 0L); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_CORRRSN.pgc b/app/src/ac/svc/ACQ_CORRRSN.pgc index 993d151..6e7ccab 100644 --- a/app/src/ac/svc/ACQ_CORRRSN.pgc +++ b/app/src/ac/svc/ACQ_CORRRSN.pgc @@ -1,7 +1,7 @@ /* * ACQ_CORRRSN.pgc - ac 모듈 서비스 ACQ_CORRRSN (one service = one file). - * 매입 정정 사유별 처리: 수수료 조정 원장(ac_fee_adj)에 사유 코드가 붙은 - * 증감 행을 남긴다. 증감액(T_DELTA)과 사유(T_REASON)를 그대로 기록한다. + * 매입 정정 사유별 처리: 수수료 조정 원장에 사유 코드가 붙은 증감 행을 + * 남긴다. 증감액과 사유를 그대로 기록하고 사유별 누계를 회신한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_CORRRSN.h" @@ -9,26 +9,149 @@ void ACQ_CORRRSN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char why[80]; + const char *SVC = "ACQ_CORRRSN"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_id, h_pid, h_delta; - char h_reason[100], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 매입 금액/수수료 조정 요청", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "일반"); + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + + /* 3) 업무규칙 분기부: 조정 유형/부호/한도 판정(3+ 분기) */ + /* 사유별 기존 조정 누계를 확인해 반복 정정을 감시 */ h_delta = getl(b, T_DELTA); - gets_(b, T_REASON, why, sizeof(why)); - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - if (h_delta == 0) { setl(b, T_RC, 3); FAIL(b); } - snprintf(h_reason, sizeof(h_reason), "CORR:%s", why[0] ? why : "MISC"); + if (h_delta == 0) { Bchg(b, T_MSG, 0, "증감액 0", 0L); setl(b, T_RC, 177); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(delta),0) INTO :h_cnt, :h_sum + FROM ac_fee_adj WHERE purchase_id = :h_pid AND reason = :h_reason; + if (sqlca.sqlcode < 0) { + userlog("%s: 사유누계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_cnt >= 5) { Bchg(b, T_MSG, 0, "동일 사유 정정 한도 초과", 0L); setl(b, T_RC, 178); FAIL(b); } + else if (h_cnt >= 2) { h_val = 3; } + else if (h_cnt == 1) { h_val = 2; } + else { h_val = 1; } - EXEC SQL SELECT nextval('ac_feeadj_seq') INTO :h_id; - if (sqlca.sqlcode < 0) FAIL(b); - 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("ACQ_CORRRSN FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + /* 4) 본처리부: 조정 원장 기록 및 매입 금액 보정 */ + h_adjid = acdb_insert_fee_adj(h_pid, h_delta, h_reason, h_bizdate); + if (h_adjid < 0) { Bchg(b, T_MSG, 0, "사유별 정정 실패", 0L); setl(b, T_RC, 98); FAIL(b); } + h_aff = 1; - setl(b, T_ADJ, h_id); + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "사유별정정 pid=%ld 사유=%s 증감=%ld 누계=%ld", h_pid, h_reason, h_delta, h_sum + h_delta); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_CORRRSN', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_CORRRSN' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_ADJ, h_adjid); + setl(b, T_DELTA, h_delta); + setl(b, T_AMT1, h_sum + h_delta); + setl(b, T_COUNT, h_cnt + 1); + setl(b, T_ARG1, h_val); + Bchg(b, T_REASON, 0, h_reason, 0L); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_CORRSTAT.pgc b/app/src/ac/svc/ACQ_CORRSTAT.pgc index d00c28f..bccc292 100644 --- a/app/src/ac/svc/ACQ_CORRSTAT.pgc +++ b/app/src/ac/svc/ACQ_CORRSTAT.pgc @@ -1,7 +1,7 @@ /* * ACQ_CORRSTAT.pgc - ac 모듈 서비스 ACQ_CORRSTAT (one service = one file). - * 매입 정정 사유별 통계: 조정 원장을 사유 접두어별로 그룹 집계해 사유코드· - * 증감합계 쌍을 occurrence 로 내리고 순증감 총액을 계산한다. + * 매입 정정 사유별 통계: 조정 원장을 사유 접두어별로 그룹 집계해 증액분/ + * 감액분을 분해하고 순증감 총액과 최다 사유를 계산한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_CORRSTAT.h" @@ -9,29 +9,148 @@ void ACQ_CORRSTAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long net = 0, kinds = 0; + const char *SVC = "ACQ_CORRSTAT"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_sum; - char h_reason[64], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL DECLARE crs_cur CURSOR FOR - SELECT split_part(reason, ':', 1), sum(delta) FROM ac_fee_adj - WHERE biz_date = :h_bizdate GROUP BY split_part(reason, ':', 1) ORDER BY 1; - EXEC SQL OPEN crs_cur; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH crs_cur INTO :h_reason, :h_sum; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE crs_cur; FAIL(b); } - if (kinds < 8) { Badd(b, T_REASON, h_reason, 0L); Badd(b, T_DELTA, (char *)&h_sum, 0L); } - net += h_sum; kinds++; + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); } - EXEC SQL CLOSE crs_cur; + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } - setl(b, T_GROSS, net); - setl(b, T_ARG1, kinds); + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*), coalesce(sum(delta),0), coalesce(sum(delta) FILTER (WHERE delta > 0),0), coalesce(sum(delta) FILTER (WHERE delta < 0),0) INTO :h_cnt, :h_a, :h_b, :h_c FROM ac_fee_adj WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 증감분해 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(reason,''), count(*) INTO :h_str2, :h_d FROM ac_fee_adj WHERE biz_date = :h_bizdate GROUP BY reason ORDER BY count(*) DESC LIMIT 1; + if (sqlca.sqlcode < 0) { + userlog("%s: 최다사유 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 순증감 방향으로 정정 성격을 판정 */ + if (h_a > 0) { h_val = 1; Bchg(b, T_MSG, 0, "수수료 순증", 0L); } + else if (h_a < 0) { h_val = 2; Bchg(b, T_MSG, 0, "수수료 순감", 0L); } + else if (h_cnt > 0) { h_val = 3; Bchg(b, T_MSG, 0, "증감 상쇄", 0L); } + else { h_val = 0; Bchg(b, T_MSG, 0, "정정 없음", 0L); } + if (h_cnt > 0) ratio = (h_d * 10000L) / h_cnt; else ratio = 0; + h_gross = (h_b > -h_c) ? h_b : -h_c; + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "정정통계 bizdate=%s 건수=%ld 순증감=%ld 최다사유=%s", h_bizdate, h_cnt, h_a, h_str2); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_CORRSTAT', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_CORRSTAT' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_DELTA, h_a); + setl(b, T_AMT1, h_b); + setl(b, T_AMT2, h_c); + Bchg(b, T_REASON, 0, h_str2, 0L); + setl(b, T_ARG1, h_d); + setl(b, T_ARG2, ratio); + setl(b, T_ARG3, h_val); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_CTRYQRY.pgc b/app/src/ac/svc/ACQ_CTRYQRY.pgc index c7607c3..559ad52 100644 --- a/app/src/ac/svc/ACQ_CTRYQRY.pgc +++ b/app/src/ac/svc/ACQ_CTRYQRY.pgc @@ -9,21 +9,155 @@ void ACQ_CTRYQRY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_CTRYQRY"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_n = 0, h_krw = 0, h_rate = 0; - char h_ccy[8], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_CCY, h_ccy, sizeof(h_ccy)); - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL SELECT count(*), coalesce(sum(krw_amount),0), coalesce(avg(rate_bps),0)::bigint - INTO :h_n, :h_krw, :h_rate FROM ac_fx - WHERE ccy = :h_ccy AND biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); - setl(b, T_COUNT, h_n); - setl(b, T_GROSS, h_krw); + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_CCY, ccy, sizeof(ccy)); + if (ccy[0] == 0) { Bchg(b, T_MSG, 0, "통화코드 누락", 0L); setl(b, T_RC, 103); FAIL(b); } + strncpy(h_ccy, ccy, sizeof(h_ccy)-1); h_ccy[sizeof(h_ccy)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } + + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*), coalesce(sum(krw_amount),0), coalesce(avg(rate_bps),0)::bigint INTO :h_cnt, :h_gross, :h_rate FROM ac_fx WHERE ccy = :h_ccy AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 권역집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(sum(krw_amount),0) INTO :h_a FROM ac_fx WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 해외총액 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(rate_bps,0) INTO :h_b FROM cm_fx_rate WHERE ccy = :h_ccy ORDER BY base_date DESC LIMIT 1; + if (sqlca.sqlcode < 0) { + userlog("%s: 기준환율 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 적용환율과 기준환율 괴리로 환리스크를 판정 */ + h_e = h_rate - h_b; + if (h_a > 0) ratio = (h_gross * 10000L) / h_a; else ratio = 0; + if (h_cnt == 0) { h_val = 0; } + else if (h_b > 0 && (h_e > h_b / 20 || h_e < -(h_b / 20))) { h_val = 3; } + else if (h_e != 0) { h_val = 2; } + else { h_val = 1; } + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "국가조회 ccy=%s 건수=%ld 원화=%ld 환율차=%ld", h_ccy, h_cnt, h_gross, h_e); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_CTRYQRY', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_CTRYQRY' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_GROSS, h_gross); setl(b, T_ARG1, h_rate); + setl(b, T_ARG2, h_b); + setl(b, T_ARG3, h_e); + setl(b, T_ARG4, ratio); + setl(b, T_ARG5, h_val); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_CTRYRANK.pgc b/app/src/ac/svc/ACQ_CTRYRANK.pgc index 09bae5f..784c7dd 100644 --- a/app/src/ac/svc/ACQ_CTRYRANK.pgc +++ b/app/src/ac/svc/ACQ_CTRYRANK.pgc @@ -1,49 +1,159 @@ /* * ACQ_CTRYRANK.pgc - ac 모듈 서비스 ACQ_CTRYRANK (one service = one file). - * 매입 국가별 순위: 통화코드를 권역(미주/일본/유럽/기타)으로 분류하는 C - * 분류기로 원화 합계를 권역 버킷에 모아 최대 권역 인덱스를 판정한다. + * 매입 국가별 순위: 통화코드를 권역(미주/일본/유럽/기타)으로 분류하는 + * C 분류기로 원화 합계를 권역 버킷에 모아 최대 권역을 판정한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_CTRYRANK.h" -/* 통화 → 권역 인덱스 (0=미주 1=일본 2=유럽 3=기타) */ -static int ac_region(const char *c) -{ - if (strcmp(c, "USD") == 0 || strcmp(c, "CAD") == 0) return 0; - if (strcmp(c, "JPY") == 0) return 1; - if (strcmp(c, "EUR") == 0 || strcmp(c, "GBP") == 0) return 2; - return 3; -} - void ACQ_CTRYRANK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long bucket[4] = {0, 0, 0, 0}; - int i, best = 0; + const char *SVC = "ACQ_CTRYRANK"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_krw; - char h_ccy[8], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL DECLARE ctr_rk CURSOR FOR - SELECT ccy, sum(krw_amount) FROM ac_fx WHERE biz_date = :h_bizdate GROUP BY ccy; - EXEC SQL OPEN ctr_rk; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH ctr_rk INTO :h_ccy, :h_krw; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE ctr_rk; FAIL(b); } - bucket[ac_region(h_ccy)] += h_krw; + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); } - EXEC SQL CLOSE ctr_rk; + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } - for (i = 1; i < 4; i++) if (bucket[i] > bucket[best]) best = i; - setl(b, T_ARG1, bucket[0]); - setl(b, T_ARG2, bucket[1]); - setl(b, T_ARG3, bucket[2]); - setl(b, T_ARG4, bucket[3]); - setl(b, T_RC, best); + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT coalesce(sum(krw_amount) FILTER (WHERE ccy = 'USD'),0), coalesce(sum(krw_amount) FILTER (WHERE ccy = 'JPY'),0), coalesce(sum(krw_amount) FILTER (WHERE ccy = 'EUR'),0), coalesce(sum(krw_amount) FILTER (WHERE ccy NOT IN ('USD','JPY','EUR')),0) INTO :h_a, :h_b, :h_c, :h_d FROM ac_fx WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 권역버킷 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(sum(krw_amount),0) INTO :h_cnt, :h_gross FROM ac_fx WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 해외총계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 권역 버킷 최대값을 골라 순위 1위 권역 인덱스를 만든다 */ + h_max = h_a; h_top = 1; + if (h_b > h_max) { h_max = h_b; h_top = 2; } + if (h_c > h_max) { h_max = h_c; h_top = 3; } + if (h_d > h_max) { h_max = h_d; h_top = 4; } + if (h_gross > 0) ratio = (h_max * 10000L) / h_gross; else ratio = 0; + if (h_top == 1) Bchg(b, T_STR1, 0, "미주", 0L); + else if (h_top == 2) Bchg(b, T_STR1, 0, "일본", 0L); + else if (h_top == 3) Bchg(b, T_STR1, 0, "유럽", 0L); + else Bchg(b, T_STR1, 0, "기타", 0L); + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "국가순위 bizdate=%s 미주=%ld 일본=%ld 유럽=%ld 1위=%ld", h_bizdate, h_a, h_b, h_c, h_top); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_CTRYRANK', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_CTRYRANK' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_AMT1, h_a); + setl(b, T_AMT2, h_b); + setl(b, T_AMT3, h_c); + setl(b, T_AMT4, h_d); + setl(b, T_ARG1, h_top); + setl(b, T_ARG2, ratio); + setl(b, T_COUNT, h_cnt); + setl(b, T_GROSS, h_gross); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_CTRYSUM.pgc b/app/src/ac/svc/ACQ_CTRYSUM.pgc index 9717d75..24d1998 100644 --- a/app/src/ac/svc/ACQ_CTRYSUM.pgc +++ b/app/src/ac/svc/ACQ_CTRYSUM.pgc @@ -1,48 +1,162 @@ /* * ACQ_CTRYSUM.pgc - ac 모듈 서비스 ACQ_CTRYSUM (one service = one file). - * 국가별 매입 집계: 통화코드를 국가권역(미주/일본/유럽/기타)으로 매핑하는 - * C 분류기를 거쳐 해외매입 원화금액을 권역별 버킷에 누적한다. + * 국가별 매입 집계: 통화코드를 국가권역으로 매핑해 해외매입 원화금액을 + * 권역별 버킷에 누적하고 권역 종수와 평균 건단가를 산출한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_CTRYSUM.h" -/* 통화코드 → 권역 인덱스 (0=미주, 1=일본, 2=유럽, 3=기타) */ -static int ac_ccy_region(const char *ccy) -{ - if (strcmp(ccy, "USD") == 0 || strcmp(ccy, "CAD") == 0) return 0; - if (strcmp(ccy, "JPY") == 0) return 1; - if (strcmp(ccy, "EUR") == 0 || strcmp(ccy, "GBP") == 0 || strcmp(ccy, "CHF") == 0) return 2; - return 3; -} - void ACQ_CTRYSUM(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long bucket[4] = {0, 0, 0, 0}; + const char *SVC = "ACQ_CTRYSUM"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_krw; - char h_ccy[8], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL DECLARE ctr_cur CURSOR FOR - SELECT ccy, sum(krw_amount) FROM ac_fx - WHERE biz_date = :h_bizdate GROUP BY ccy; - EXEC SQL OPEN ctr_cur; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH ctr_cur INTO :h_ccy, :h_krw; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE ctr_cur; FAIL(b); } - bucket[ac_ccy_region(h_ccy)] += h_krw; + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); } - EXEC SQL CLOSE ctr_cur; + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } - setl(b, T_ARG1, bucket[0]); /* 미주 */ - setl(b, T_ARG2, bucket[1]); /* 일본 */ - setl(b, T_ARG3, bucket[2]); /* 유럽 */ - setl(b, T_ARG4, bucket[3]); /* 기타 */ - setl(b, T_GROSS, bucket[0] + bucket[1] + bucket[2] + bucket[3]); + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*) FILTER (WHERE ccy = 'USD'), count(*) FILTER (WHERE ccy = 'JPY'), count(*) FILTER (WHERE ccy = 'EUR'), count(*) FILTER (WHERE ccy NOT IN ('USD','JPY','EUR')) INTO :h_a, :h_b, :h_c, :h_d FROM ac_fx WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 권역건수 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(sum(krw_amount),0), count(DISTINCT ccy) INTO :h_cnt, :h_gross, :h_n FROM ac_fx WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 총계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 권역 참여 수로 해외매입 분산도를 판정 */ + k = 0; + if (h_a > 0) k++; + if (h_b > 0) k++; + if (h_c > 0) k++; + if (h_d > 0) k++; + if (k == 0) { h_val = 0; } + else if (k == 1) { h_val = 3; } + else if (k == 2) { h_val = 2; } + else { h_val = 1; } + h_avg = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "국가집계 bizdate=%s 권역수=%ld 통화종수=%ld 원화=%ld", h_bizdate, k, h_n, h_gross); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_CTRYSUM', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_CTRYSUM' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_ARG1, h_a); + setl(b, T_ARG2, h_b); + setl(b, T_ARG3, h_c); + setl(b, T_ARG4, h_d); + setl(b, T_ARG5, k); + setl(b, T_ARG6, h_val); + setl(b, T_COUNT, h_cnt); + setl(b, T_GROSS, h_gross); + setl(b, T_ID1, h_n); + setl(b, T_AMT1, h_avg); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_DATECHG.pgc b/app/src/ac/svc/ACQ_DATECHG.pgc index 278472f..c3a5fa1 100644 --- a/app/src/ac/svc/ACQ_DATECHG.pgc +++ b/app/src/ac/svc/ACQ_DATECHG.pgc @@ -1,22 +1,161 @@ /* * ACQ_DATECHG.pgc - ac 모듈 서비스 ACQ_DATECHG (one service = one file). - * Moved verbatim from the former inline ac_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by ac_svr at tpsvrinit. + * 영업일 변경: 매입의 귀속 영업일을 요청일로 옮긴다. 정산이 끝난 건은 + * 이동할 수 없으며, 이동 전후 일자를 모두 로그에 남긴다. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_DATECHG.h" void ACQ_DATECHG(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + const char *SVC = "ACQ_DATECHG"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid; - char h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; + + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 매입 상태 전이 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_STR1, bizdate, sizeof(bizdate)); + strncpy(h_d1, bizdate, sizeof(h_d1)-1); h_d1[sizeof(h_d1)-1] = 0; + if (h_d1[0] == 0) { Bchg(b, T_MSG, 0, "변경 대상 영업일 누락", 0L); setl(b, T_RC, 107); FAIL(b); } + bizdate[0] = 0; 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); + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + + /* 3) 업무규칙 분기부: 상태 전이 가능여부/유형 판정(3+ 분기) */ + if (!(strcmp(h_prev, "A") == 0 || strcmp(h_prev, "M") == 0 || strcmp(h_prev, "H") == 0)) { + userlog("%s 거절: 상태 %s 에서 전이 불가", SVC, h_prev); + Bchg(b, T_MSG, 0, "상태 전이 불가", 0L); setl(b, T_RC, 120); FAIL(b); + } + /* 이동 전 원 영업일을 확보하고 이동 폭을 판정 */ + EXEC SQL SELECT biz_date::text INTO :h_d2 FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 원영업일 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (strcmp(h_d1, h_d2) == 0) { Bchg(b, T_MSG, 0, "동일 영업일", 0L); setl(b, T_RC, 137); FAIL(b); } + else if (strcmp(h_d1, h_d2) < 0) { h_val = 3; Bchg(b, T_MSG, 0, "과거일 소급 이동", 0L); } + else { h_val = 1; Bchg(b, T_MSG, 0, "미래일 이월", 0L); } + + /* 4) 본처리부: 부수행 기록 및 상태 갱신 */ + EXEC SQL UPDATE purchase SET biz_date = :h_d1::date, updated_at = now() WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 영업일변경 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_rem = sqlca.sqlerrd[2]; + h_aff = 1; + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "영업일변경 pid=%ld %s -> %s 등급=%ld", h_pid, h_d2, h_d1, h_val); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_DATECHG', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_DATECHG' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + Bchg(b, T_STR1, 0, h_d1, 0L); + Bchg(b, T_STR2, 0, h_d2, 0L); + setl(b, T_ARG1, h_rem); + setl(b, T_ARG2, h_val); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_DAYSTAT.pgc b/app/src/ac/svc/ACQ_DAYSTAT.pgc index 19733b4..1eab0f7 100644 --- a/app/src/ac/svc/ACQ_DAYSTAT.pgc +++ b/app/src/ac/svc/ACQ_DAYSTAT.pgc @@ -1,7 +1,7 @@ /* * ACQ_DAYSTAT.pgc - ac 모듈 서비스 ACQ_DAYSTAT (one service = one file). * 매입 일별 상태 통계: 영업일 전 건을 상태별(접수/대사/정산/취소/보류/무효) - * FILTER 집계 한 방으로 나눠 세고, 총건수·총액을 함께 내려준다. + * FILTER 집계 한 번으로 나눠 세고 총건수·총액을 함께 내려준다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_DAYSTAT.h" @@ -9,32 +9,150 @@ void ACQ_DAYSTAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_DAYSTAT"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_a = 0, h_m = 0, h_s = 0, h_c = 0, h_h = 0, h_v = 0, h_tot = 0, h_sum = 0; - char h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL SELECT count(*) FILTER (WHERE status = 'A'), - count(*) FILTER (WHERE status = 'M'), - count(*) FILTER (WHERE status = 'S'), - count(*) FILTER (WHERE status = 'C'), - count(*) FILTER (WHERE status = 'H'), - count(*) FILTER (WHERE status = 'V'), - count(*), - coalesce(sum(amount), 0) - INTO :h_a, :h_m, :h_s, :h_c, :h_h, :h_v, :h_tot, :h_sum - FROM purchase WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) { userlog("ACQ_DAYSTAT FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); - setl(b, T_ARG1, h_a); /* 접수 */ - setl(b, T_ARG2, h_m); /* 대사완료 */ - setl(b, T_ARG3, h_s); /* 정산완료 */ - setl(b, T_ARG4, h_c); /* 취소 */ - setl(b, T_ARG5, h_h); /* 보류 */ - setl(b, T_ARG6, h_v); /* 무효 */ - setl(b, T_COUNT, h_tot); - setl(b, T_GROSS, h_sum); + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } + + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*) FILTER (WHERE status = 'A'), count(*) FILTER (WHERE status = 'M'), count(*) FILTER (WHERE status = 'S'), count(*) FILTER (WHERE status = 'C'), count(*) FILTER (WHERE status = 'H'), count(*) FILTER (WHERE status = 'V') INTO :h_a, :h_b, :h_c, :h_d, :h_e, :h_n FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 상태별건수 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0) INTO :h_cnt, :h_gross, :h_net FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 총계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 미결(접수+대사+보류) 비중으로 일마감 상태를 판정 */ + h_openc = h_a + h_b + h_e; + if (h_cnt > 0) ratio = (h_openc * 10000L) / h_cnt; else ratio = 0; + if (h_cnt == 0) { h_val = 0; } + else if (ratio == 0) { h_val = 1; } + else if (ratio >= 5000) { h_val = 3; } + else { h_val = 2; } + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "일별상태 bizdate=%s 접수=%ld 대사=%ld 정산=%ld 미결=%ld", h_bizdate, h_a, h_b, h_c, h_openc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_DAYSTAT', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_DAYSTAT' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_ARG1, h_a); + setl(b, T_ARG2, h_b); + setl(b, T_ARG3, h_c); + setl(b, T_ARG4, h_d); + setl(b, T_ARG5, h_e); + setl(b, T_ARG6, h_n); + setl(b, T_COUNT, h_cnt); + setl(b, T_GROSS, h_gross); + setl(b, T_NET, h_net); + setl(b, T_ID1, ratio); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_DDC.pgc b/app/src/ac/svc/ACQ_DDC.pgc index a67b73e..972ed98 100644 --- a/app/src/ac/svc/ACQ_DDC.pgc +++ b/app/src/ac/svc/ACQ_DDC.pgc @@ -1,33 +1,170 @@ /* * ACQ_DDC.pgc - ac 모듈 서비스 ACQ_DDC (one service = one file). - * Moved verbatim from the former inline ac_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by ac_svr at tpsvrinit. + * 동적통화환산(DDC): 카드 소지자 통화로 결제 금액을 환산해 제시한다. + * 기준환율에 DDC 마진(bps)을 얹은 적용환율로 원화를 환산해 기록한다. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_DDC.h" void ACQ_DDC(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], ccy[8]; + const char *SVC = "ACQ_DDC"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_id, h_pid, h_fx, h_krw, h_rate; - char h_ccy[8], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; 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; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + userlog("%s 진입: 매입 금액/수수료 조정 요청", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_CCY, ccy, sizeof(ccy)); if (ccy[0] == 0) strcpy(ccy, "USD"); + strncpy(h_ccy, ccy, sizeof(h_ccy)-1); h_ccy[sizeof(h_ccy)-1] = 0; + h_fx = getl(b, T_FX_AMT); + if (h_fx <= 0) { Bchg(b, T_MSG, 0, "DDC 원통화 금액 오류", 0L); setl(b, T_RC, 187); FAIL(b); } + threshold = getl(b, T_ARG1); if (threshold < 0 || threshold > 1000) threshold = 300; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + + /* 3) 업무규칙 분기부: 조정 유형/부호/한도 판정(3+ 분기) */ + /* 기준환율에 DDC 마진을 가산해 적용환율을 만든다 */ + EXEC SQL SELECT coalesce(rate_bps,0) INTO :h_rate2 FROM cm_fx_rate + WHERE ccy = :h_ccy ORDER BY base_date DESC LIMIT 1; + if (sqlca.sqlcode < 0) { + userlog("%s: 기준환율 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_rate2 <= 0) { Bchg(b, T_MSG, 0, "DDC 대상 환율 없음", 0L); setl(b, T_RC, 188); FAIL(b); } + computed = h_rate2 + (h_rate2 * threshold / 10000L); + h_krw = h_fx * computed / 100L; + if (threshold >= 500) { h_val = 3; Bchg(b, T_MSG, 0, "고마진 DDC", 0L); } + else if (threshold >= 200) { h_val = 2; Bchg(b, T_MSG, 0, "표준 DDC", 0L); } + else { h_val = 1; Bchg(b, T_MSG, 0, "저마진 DDC", 0L); } + + /* 4) 본처리부: 조정 원장 기록 및 매입 금액 보정 */ 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); } + if (sqlca.sqlcode < 0) { + userlog("%s: DDC채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_comp = computed; + 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_comp, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: DDC원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_aff = 1; + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "DDC환산 pid=%ld ccy=%s 기준=%ld 적용=%ld 원화=%ld", h_pid, h_ccy, h_rate2, computed, h_krw); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_DDC', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_DDC' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_ID1, h_id); setl(b, T_AMOUNT, h_krw); + setl(b, T_FX_AMT, h_fx); + setl(b, T_ARG2, h_rate2); + setl(b, T_ARG3, computed); + setl(b, T_ARG4, threshold); + setl(b, T_ARG1, h_val); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_DOWSTAT.pgc b/app/src/ac/svc/ACQ_DOWSTAT.pgc index 7f75ee9..f560427 100644 --- a/app/src/ac/svc/ACQ_DOWSTAT.pgc +++ b/app/src/ac/svc/ACQ_DOWSTAT.pgc @@ -1,7 +1,7 @@ /* * ACQ_DOWSTAT.pgc - ac 모듈 서비스 ACQ_DOWSTAT (one service = one file). - * 매입 요일별 통계: 지정 기간(T_STR1~T_STR2)의 매입을 요일(0~6)별로 그룹 - * 집계해 요일번호·건수 쌍을 occurrence 로 내리고 주말 비중을 계산한다. + * 매입 요일별 통계: 지정 기간(T_STR1~T_STR2)의 매입을 평일/주말로 분해 + * 집계해 주말 비중과 최다 요일을 계산한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_DOWSTAT.h" @@ -9,33 +9,155 @@ void ACQ_DOWSTAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long total = 0, weekend = 0; + const char *SVC = "ACQ_DOWSTAT"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_dow, h_n; - char h_from[16], h_to[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_STR1, h_from, sizeof(h_from)); - gets_(b, T_STR2, h_to, sizeof(h_to)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL DECLARE dow_cur CURSOR FOR - SELECT extract(dow from biz_date)::int, count(*) FROM purchase - WHERE biz_date BETWEEN :h_from AND :h_to GROUP BY 1 ORDER BY 1; - EXEC SQL OPEN dow_cur; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH dow_cur INTO :h_dow, :h_n; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE dow_cur; FAIL(b); } - Badd(b, T_ARG5, (char *)&h_dow, 0L); - Badd(b, T_ARG6, (char *)&h_n, 0L); - total += h_n; - if (h_dow == 0 || h_dow == 6) weekend += h_n; + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_STR1, bizdate, sizeof(bizdate)); + strncpy(h_d1, bizdate, sizeof(h_d1)-1); h_d1[sizeof(h_d1)-1] = 0; + gets_(b, T_STR2, bizdate, sizeof(bizdate)); + strncpy(h_d2, bizdate, sizeof(h_d2)-1); h_d2[sizeof(h_d2)-1] = 0; + if (h_d1[0] == 0 || h_d2[0] == 0) { Bchg(b, T_MSG, 0, "기간 지정 누락", 0L); setl(b, T_RC, 106); FAIL(b); } + bizdate[0] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); } - EXEC SQL CLOSE dow_cur; + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } - setl(b, T_COUNT, total); - setl(b, T_ARG1, weekend); - setl(b, T_RC, total > 0 ? 0 : 1); + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*) FILTER (WHERE extract(dow from biz_date) IN (0,6)), count(*) FILTER (WHERE extract(dow from biz_date) NOT IN (0,6)), count(*), coalesce(sum(amount),0) INTO :h_a, :h_b, :h_cnt, :h_gross FROM purchase WHERE biz_date BETWEEN :h_d1::date AND :h_d2::date; + if (sqlca.sqlcode < 0) { + userlog("%s: 요일분해 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(extract(dow from biz_date),0)::bigint, count(*) INTO :h_top, :h_c FROM purchase WHERE biz_date BETWEEN :h_d1::date AND :h_d2::date GROUP BY extract(dow from biz_date) ORDER BY count(*) DESC LIMIT 1; + if (sqlca.sqlcode < 0) { + userlog("%s: 최다요일 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 주말 비중으로 영업 패턴을 분류 */ + if (h_cnt > 0) ratio = (h_a * 10000L) / h_cnt; else ratio = 0; + if (h_cnt == 0) { h_val = 0; } + else if (ratio >= 4000) { h_val = 3; } + else if (ratio >= 2000) { h_val = 2; } + else { h_val = 1; } + h_avg = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "요일통계 %s~%s 주말=%ld 평일=%ld 최다요일=%ld", h_d1, h_d2, h_a, h_b, h_top); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_DOWSTAT', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_DOWSTAT' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_ARG1, h_a); + setl(b, T_ARG2, h_b); + setl(b, T_ARG3, h_top); + setl(b, T_ARG4, h_c); + setl(b, T_ARG5, ratio); + setl(b, T_ARG6, h_val); + setl(b, T_COUNT, h_cnt); + setl(b, T_GROSS, h_gross); + setl(b, T_AMT1, h_avg); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_DUPCHK.pgc b/app/src/ac/svc/ACQ_DUPCHK.pgc index 92790d7..122baa8 100644 --- a/app/src/ac/svc/ACQ_DUPCHK.pgc +++ b/app/src/ac/svc/ACQ_DUPCHK.pgc @@ -1,26 +1,169 @@ /* * ACQ_DUPCHK.pgc - ac 모듈 서비스 ACQ_DUPCHK (one service = one file). - * Moved verbatim from the former inline ac_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by ac_svr at tpsvrinit. + * 중복매입 단건 점검: 가맹점·영업일·금액이 모두 같은 매입이 몇 건 존재하는지 + * 세어 중복 접수 여부를 판정한다. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_DUPCHK.h" void ACQ_DUPCHK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; + const char *SVC = "ACQ_DUPCHK"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_amt; - char h_merch[64], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; + + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ gets_(b, T_MERCHANT, merch, sizeof(merch)); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - h_amt = getl(b, T_AMOUNT); + if (merch[0] == 0) { Bchg(b, T_MSG, 0, "가맹점 ID 누락", 0L); setl(b, T_RC, 101); FAIL(b); } strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + h_amt = getl(b, T_AMOUNT); + if (h_amt <= 0) { Bchg(b, T_MSG, 0, "점검 금액 오류", 0L); setl(b, T_RC, 104); FAIL(b); } + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT count(*) INTO :h_cnt FROM purchase - WHERE merchant_id = :h_merch AND amount = :h_amt AND biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } + + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + EXEC SQL SELECT name, status, daily_limit INTO :h_str1, :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (strcmp(h_status, "ACTIVE") != 0) + userlog("%s 주의: 가맹점 %s 상태=%s", SVC, h_merch, h_status); + /* 가맹점 MDR(bps) 및 상태분포 컨텍스트 로드 */ + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + EXEC SQL SELECT count(*) FILTER (WHERE status IN ('A','M')), count(*) FILTER (WHERE status = 'S') INTO :h_openc, :h_setc FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 상태분포 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산 진행 컨텍스트(기 정산 순액) */ + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_ctx FROM settlement WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*), coalesce(min(purchase_id),0), coalesce(max(purchase_id),0) INTO :h_cnt, :h_a, :h_b FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate AND amount = :h_amt; + if (sqlca.sqlcode < 0) { + userlog("%s: 동일금액 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_c FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate AND amount = :h_amt AND created_at >= now() - interval '10 minutes'; + if (sqlca.sqlcode < 0) { + userlog("%s: 단시간중복 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 건수와 단시간 반복 여부로 중복 위험도를 나눈다 */ + if (h_cnt <= 1) { h_val = 0; Bchg(b, T_MSG, 0, "중복 없음", 0L); } + else if (h_c >= 2) { h_val = 3; Bchg(b, T_MSG, 0, "단시간 중복 의심", 0L); } + else if (h_cnt >= 3) { h_val = 2; Bchg(b, T_MSG, 0, "다중 중복", 0L); } + else { h_val = 1; Bchg(b, T_MSG, 0, "중복 1건", 0L); } + h_gross = h_amt * h_cnt; + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :h_val2 FROM merchant_summary WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 누적집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_pdgross FROM purchase WHERE merchant_id = :h_merch AND biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_val2 > 0 && h_gross > 0) ratio = (h_gross * 10000L) / (h_val2 + h_gross); + else ratio = 0; + h_share = (h_pdgross > 0) ? (h_gross * 100L / h_pdgross) : 0; + userlog("%s 추이: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_pdgross, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "중복점검 merch=%s 금액=%ld 건수=%ld 등급=%ld", h_merch, h_amt, h_cnt, h_val); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_DUPCHK', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_DUPCHK' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ setl(b, T_COUNT, h_cnt); + setl(b, T_ARG1, h_c); + setl(b, T_ARG2, h_val); + setl(b, T_ID1, h_a); + setl(b, T_ID2, h_b); + setl(b, T_GROSS, h_gross); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_DUPSCAN.pgc b/app/src/ac/svc/ACQ_DUPSCAN.pgc index f022969..0883b42 100644 --- a/app/src/ac/svc/ACQ_DUPSCAN.pgc +++ b/app/src/ac/svc/ACQ_DUPSCAN.pgc @@ -9,25 +9,150 @@ void ACQ_DUPSCAN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_DUPSCAN"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_groups = 0, h_rows = 0, h_amt = 0; - char h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL SELECT count(*), coalesce(sum(dup_n), 0), coalesce(sum(dup_amt), 0) - INTO :h_groups, :h_rows, :h_amt - FROM (SELECT count(*) AS dup_n, sum(amount) AS dup_amt - FROM purchase - WHERE biz_date = :h_bizdate AND status NOT IN ('C', 'V') - GROUP BY merchant_id, amount - HAVING count(*) > 1) g; - if (sqlca.sqlcode < 0) { userlog("ACQ_DUPSCAN FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); - setl(b, T_COUNT, h_groups); /* 의심 그룹 수 */ - setl(b, T_ARG1, h_rows); /* 연루 매입 건수 */ - setl(b, T_AMT1, h_amt); /* 연루 총액 */ - setl(b, T_RC, h_groups > 0 ? 1 : 0); + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } + + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*), coalesce(sum(c),0), coalesce(max(c),0) INTO :h_a, :h_b, :h_max FROM (SELECT count(*) AS c FROM purchase WHERE biz_date = :h_bizdate GROUP BY merchant_id, amount HAVING count(*) > 1) g; + if (sqlca.sqlcode < 0) { + userlog("%s: 중복그룹 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(sum(s),0) INTO :h_gross FROM (SELECT sum(amount) AS s FROM purchase WHERE biz_date = :h_bizdate GROUP BY merchant_id, amount HAVING count(*) > 1) g2; + if (sqlca.sqlcode < 0) { + userlog("%s: 연루금액 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_cnt FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 모집단 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 연루 건수 비중과 최대 반복수로 스캔 결과를 등급화 */ + if (h_cnt > 0) ratio = (h_b * 10000L) / h_cnt; else ratio = 0; + if (h_a == 0) { h_val = 0; } + else if (h_max >= 5) { h_val = 3; } + else if (ratio >= 1000) { h_val = 2; } + else { h_val = 1; } + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "중복스캔 bizdate=%s 그룹=%ld 연루=%ld 최대반복=%ld", h_bizdate, h_a, h_b, h_max); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_DUPSCAN', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_DUPSCAN' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_COUNT, h_a); + setl(b, T_ARG1, h_b); + setl(b, T_ARG2, h_max); + setl(b, T_ARG3, ratio); + setl(b, T_ARG4, h_val); + setl(b, T_GROSS, h_gross); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_EDC.pgc b/app/src/ac/svc/ACQ_EDC.pgc index 5f62cda..dac4244 100644 --- a/app/src/ac/svc/ACQ_EDC.pgc +++ b/app/src/ac/svc/ACQ_EDC.pgc @@ -1,25 +1,159 @@ /* * ACQ_EDC.pgc - ac 모듈 서비스 ACQ_EDC (one service = one file). - * Moved verbatim from the former inline ac_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by ac_svr at tpsvrinit. + * EDC 단말 매입 접수: EDC 채널로 유입된 매입을 현행 MDR 로 수수료 산출 후 + * 접수(A) 상태로 생성하고 승인 원장을 함께 남긴다. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_EDC.h" void ACQ_EDC(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; - long amount, bps = 25, fee, net, pid; + const char *SVC = "ACQ_EDC"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; + EXEC SQL BEGIN DECLARE SECTION; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; + EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 신규 매입 접수 요청", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ gets_(b, T_MERCHANT, merch, sizeof(merch)); - amount = getl(b, T_AMOUNT); + if (merch[0] == 0) { Bchg(b, T_MSG, 0, "가맹점 ID 누락", 0L); setl(b, T_RC, 101); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + h_amt = getl(b, T_AMOUNT); + if (h_amt < 1L) { Bchg(b, T_MSG, 0, "금액 범위 오류", 0L); setl(b, T_RC, 104); FAIL(b); } 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); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + + /* 3) 업무규칙 분기부: 가맹점 상태/금액 구간/채널 정책(3+ 분기) */ + if (strcmp(h_status, "ACTIVE") != 0) { Bchg(b, T_MSG, 0, "비활성 가맹점", 0L); setl(b, T_RC, 121); FAIL(b); } + /* EDC 단말 매입은 금액 구간별로 승인 경로가 다르다 */ + if (h_amt >= 5000000) { h_val = 3; Bchg(b, T_MSG, 0, "고액 EDC 매입", 0L); } + else if (h_amt >= 500000) { h_val = 2; Bchg(b, T_MSG, 0, "중액 EDC 매입", 0L); } + else { h_val = 1; Bchg(b, T_MSG, 0, "소액 EDC 매입", 0L); } + /* 일한도 초과분은 접수 자체를 거절 */ + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_sum FROM purchase + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate + AND status NOT IN ('C','V'); + if (sqlca.sqlcode < 0) { + userlog("%s: 한도확인 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_sum + h_amt > h_lim) { Bchg(b, T_MSG, 0, "일한도 초과로 접수 불가", 0L); setl(b, T_RC, 160); FAIL(b); } + + /* 4) 본처리부: 수수료 산출 및 매입/승인 원장 생성 */ + bps = h_rate; h_fee = acq_fee(h_amt, bps); h_net = h_amt - h_fee; + /* EDC 는 단말 수수료를 별도 가산하지 않고 MDR 만 적용 */ + computed = h_amt - h_fee; + h_net = computed; + h_new = acdb_next_purchase_id(); + if (h_new < 0) { Bchg(b, T_MSG, 0, "매입 채번 실패", 0L); setl(b, T_RC, 93); FAIL(b); } + if (acdb_insert_purchase(h_new, h_merch, h_amt, h_fee, h_net, "A", "EDC", h_bizdate) < 0) + { Bchg(b, T_MSG, 0, "매입 생성 실패", 0L); setl(b, T_RC, 94); FAIL(b); } + if (acdb_insert_approval(h_new, 1) < 0) { Bchg(b, T_MSG, 0, "승인 적재 실패", 0L); setl(b, T_RC, 95); FAIL(b); } + h_aff = 1; h_pid = h_new; + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "EDC매입 merch=%s 금액=%ld 수수료=%ld 등급=%ld", h_merch, h_amt, h_fee, h_val); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_EDC', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_EDC' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_PURCHASE_ID, h_new); + setl(b, T_FEE, h_fee); + setl(b, T_NET, h_net); + setl(b, T_ARG1, h_val); + setl(b, T_AMT1, h_sum); + Bchg(b, T_STATUS, 0, "A", 0L); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_EDI.pgc b/app/src/ac/svc/ACQ_EDI.pgc index 1d161bf..0d07f3c 100644 --- a/app/src/ac/svc/ACQ_EDI.pgc +++ b/app/src/ac/svc/ACQ_EDI.pgc @@ -1,31 +1,167 @@ /* * ACQ_EDI.pgc - ac 모듈 서비스 ACQ_EDI (one service = one file). - * Moved verbatim from the former inline ac_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by ac_svr at tpsvrinit. + * EDI 청구 매입 접수: EDI 문서로 청구된 매입을 접수하고 동시에 ac_edi + * 문서 원장에 청구 문서 행을 남겨 원문 추적이 가능하게 한다. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_EDI.h" void ACQ_EDI(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], docno[32], bizdate[16]; + const char *SVC = "ACQ_EDI"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_id, h_amount; - char h_merch[64], h_doc[32], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; 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; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + userlog("%s 진입: 신규 매입 접수 요청", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + if (merch[0] == 0) { Bchg(b, T_MSG, 0, "가맹점 ID 누락", 0L); setl(b, T_RC, 101); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + h_amt = getl(b, T_AMOUNT); + if (h_amt < 1L) { Bchg(b, T_MSG, 0, "금액 범위 오류", 0L); setl(b, T_RC, 104); FAIL(b); } + gets_(b, T_DOCNO, docno, sizeof(docno)); + if (docno[0] == 0) snprintf(docno, sizeof(docno), "EDI%ld", acq_seq_next()); + strncpy(h_docno, docno, sizeof(h_docno)-1); h_docno[sizeof(h_docno)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + + /* 3) 업무규칙 분기부: 가맹점 상태/금액 구간/채널 정책(3+ 분기) */ + if (strcmp(h_status, "ACTIVE") != 0) { Bchg(b, T_MSG, 0, "비활성 가맹점", 0L); setl(b, T_RC, 121); FAIL(b); } + /* 동일 문서번호 중복 청구를 차단한다 */ + EXEC SQL SELECT count(*) INTO :h_cnt FROM ac_edi WHERE doc_no = :h_docno; + if (sqlca.sqlcode < 0) { + userlog("%s: 문서중복 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_cnt > 0) { Bchg(b, T_MSG, 0, "중복 EDI 문서번호", 0L); setl(b, T_RC, 161); FAIL(b); } + if (h_amt >= 10000000) { h_val = 3; } + else if (h_amt >= 1000000) { h_val = 2; } + else { h_val = 1; } + + /* 4) 본처리부: 수수료 산출 및 매입/승인 원장 생성 */ + bps = h_rate; h_fee = acq_fee(h_amt, bps); h_net = h_amt - h_fee; + /* EDI 청구 문서를 먼저 채번해 원장에 남긴다 */ 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); } + if (sqlca.sqlcode < 0) { + userlog("%s: EDI채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO ac_edi (edi_id, merchant_id, doc_no, amount, biz_date) VALUES (:h_id, :h_merch, :h_docno, :h_amt, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 청구문서 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_new = acdb_next_purchase_id(); + if (h_new < 0) { Bchg(b, T_MSG, 0, "매입 채번 실패", 0L); setl(b, T_RC, 93); FAIL(b); } + if (acdb_insert_purchase(h_new, h_merch, h_amt, h_fee, h_net, "A", "EDI", h_bizdate) < 0) + { Bchg(b, T_MSG, 0, "매입 생성 실패", 0L); setl(b, T_RC, 94); FAIL(b); } + if (acdb_insert_approval(h_new, 1) < 0) { Bchg(b, T_MSG, 0, "승인 적재 실패", 0L); setl(b, T_RC, 95); FAIL(b); } + h_aff = 1; h_pid = h_new; + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "EDI매입 merch=%s doc=%s 금액=%ld 등급=%ld", h_merch, h_docno, h_amt, h_val); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_EDI', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_EDI' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_PURCHASE_ID, h_new); + setl(b, T_ID1, h_id); + Bchg(b, T_DOCNO, 0, h_docno, 0L); + setl(b, T_FEE, h_fee); + setl(b, T_NET, h_net); + setl(b, T_ARG1, h_val); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_EDIQRY.pgc b/app/src/ac/svc/ACQ_EDIQRY.pgc index ba999cb..f9531e6 100644 --- a/app/src/ac/svc/ACQ_EDIQRY.pgc +++ b/app/src/ac/svc/ACQ_EDIQRY.pgc @@ -1,7 +1,7 @@ /* * ACQ_EDIQRY.pgc - ac 모듈 서비스 ACQ_EDIQRY (one service = one file). - * EDI 조회: 지정 가맹점(T_MERCHANT)의 영업일 EDI 접수 문서 건수와 금액 - * 합계를 조회해 응답한다. 접수분이 없으면 결과코드 1. + * EDI 조회: 지정 가맹점(T_MERCHANT)의 영업일 EDI 접수 문서 건수와 + * 금액 합계를 조회해 응답한다. 접수분이 없으면 결과코드로 표시한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_EDIQRY.h" @@ -9,20 +9,161 @@ void ACQ_EDIQRY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_EDIQRY"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_n = 0, h_sum = 0; - char h_merch[64], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_n, :h_sum - FROM ac_edi WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); - setl(b, T_COUNT, h_n); - setl(b, T_GROSS, h_sum); - setl(b, T_RC, h_n > 0 ? 0 : 1); + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + if (merch[0] == 0) { Bchg(b, T_MSG, 0, "가맹점 ID 누락", 0L); setl(b, T_RC, 101); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } + + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + EXEC SQL SELECT name, status, daily_limit INTO :h_str1, :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (strcmp(h_status, "ACTIVE") != 0) + userlog("%s 주의: 가맹점 %s 상태=%s", SVC, h_merch, h_status); + /* 가맹점 MDR(bps) 및 상태분포 컨텍스트 로드 */ + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + EXEC SQL SELECT count(*) FILTER (WHERE status IN ('A','M')), count(*) FILTER (WHERE status = 'S') INTO :h_openc, :h_setc FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 상태분포 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산 진행 컨텍스트(기 정산 순액) */ + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_ctx FROM settlement WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(max(edi_id),0) INTO :h_cnt, :h_gross, :h_top FROM ac_edi WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: EDI집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_a FROM ac_edi WHERE merchant_id = :h_merch AND biz_date = :h_bizdate AND doc_no LIKE '%-R'; + if (sqlca.sqlcode < 0) { + userlog("%s: 재전송분 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 재전송 비중으로 EDI 수신 품질을 판정 */ + if (h_cnt > 0) ratio = (h_a * 10000L) / h_cnt; else ratio = 0; + if (h_cnt == 0) { h_val = 1; Bchg(b, T_MSG, 0, "EDI 접수분 없음", 0L); } + else if (ratio >= 3000) { h_val = 3; Bchg(b, T_MSG, 0, "재전송 과다", 0L); } + else if (h_a > 0) { h_val = 2; Bchg(b, T_MSG, 0, "일부 재전송", 0L); } + else { h_val = 0; Bchg(b, T_MSG, 0, "정상 수신", 0L); } + h_avg = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :h_val2 FROM merchant_summary WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 누적집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_pdgross FROM purchase WHERE merchant_id = :h_merch AND biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_val2 > 0 && h_gross > 0) ratio = (h_gross * 10000L) / (h_val2 + h_gross); + else ratio = 0; + h_share = (h_pdgross > 0) ? (h_gross * 100L / h_pdgross) : 0; + userlog("%s 추이: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_pdgross, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "EDI조회 merch=%s 문서=%ld 금액=%ld 재전송=%ld", h_merch, h_cnt, h_gross, h_a); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_EDIQRY', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_EDIQRY' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_GROSS, h_gross); + setl(b, T_ARG1, h_a); + setl(b, T_ARG2, ratio); + setl(b, T_ARG3, h_val); + setl(b, T_ID1, h_top); + setl(b, T_AMT1, h_avg); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_EDIRESEND.pgc b/app/src/ac/svc/ACQ_EDIRESEND.pgc index 86f96d2..a65e4c7 100644 --- a/app/src/ac/svc/ACQ_EDIRESEND.pgc +++ b/app/src/ac/svc/ACQ_EDIRESEND.pgc @@ -1,7 +1,7 @@ /* * ACQ_EDIRESEND.pgc - ac 모듈 서비스 ACQ_EDIRESEND (one service = one file). - * EDI 재전송: 문서번호의 최신 EDI 원장을 찾아 재전송 사본(-R 접미 문서번호) - * 을 발행한다. 이미 재전송본이면 다시 만들지 않는다. + * EDI 재전송: 문서번호의 최신 EDI 원장을 찾아 재전송 사본(-R 접미 문서번호)을 + * 발행한다. 이미 재전송본이면 다시 만들지 않는다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_EDIRESEND.h" @@ -9,38 +9,161 @@ void ACQ_EDIRESEND(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - size_t dlen; + const char *SVC = "ACQ_EDIRESEND"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_new, h_amt = 0; - char h_doc[64], h_redoc[72], h_merch[64], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; - gets_(b, T_DOCNO, h_doc, sizeof(h_doc)); - if (h_doc[0] == 0) { setl(b, T_RC, 8); FAIL(b); } + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); - /* 재전송본(-R 접미) 자체는 재전송 금지 */ - dlen = strlen(h_doc); - if (dlen >= 2 && strcmp(h_doc + dlen - 2, "-R") == 0) { setl(b, T_RC, 2); FAIL(b); } + userlog("%s 진입: 매입 상태 전이 요청 접수", SVC); - /* 최신 원본 EDI 행 조회 */ - EXEC SQL SELECT merchant_id, amount, to_char(biz_date, 'YYYY-MM-DD') - INTO :h_merch, :h_amt, :h_bizdate - FROM ac_edi WHERE doc_no = :h_doc - ORDER BY edi_id DESC LIMIT 1; - if (sqlca.sqlcode == 100) { setl(b, T_RC, 4); FAIL(b); } - if (sqlca.sqlcode < 0) FAIL(b); + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_DOCNO, docno, sizeof(docno)); + if (docno[0] == 0) { Bchg(b, T_MSG, 0, "문서번호 누락", 0L); setl(b, T_RC, 108); FAIL(b); } + strncpy(h_docno, docno, sizeof(h_docno)-1); h_docno[sizeof(h_docno)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - snprintf(h_redoc, sizeof(h_redoc), "%s-R", h_doc); + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; - EXEC SQL SELECT nextval('ac_edi_seq') INTO :h_new; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL INSERT INTO ac_edi (edi_id, merchant_id, doc_no, amount, biz_date) - VALUES (:h_new, :h_merch, :h_redoc, :h_amt, :h_bizdate); - if (sqlca.sqlcode < 0) { userlog("ACQ_EDIRESEND FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + /* 3) 업무규칙 분기부: 상태 전이 가능여부/유형 판정(3+ 분기) */ + if (!(strcmp(h_prev, "A") == 0 || strcmp(h_prev, "M") == 0 || strcmp(h_prev, "S") == 0 || strcmp(h_prev, "C") == 0 || strcmp(h_prev, "H") == 0 || strcmp(h_prev, "V") == 0 || strcmp(h_prev, "P") == 0)) { + userlog("%s 거절: 상태 %s 에서 전이 불가", SVC, h_prev); + Bchg(b, T_MSG, 0, "상태 전이 불가", 0L); setl(b, T_RC, 120); FAIL(b); + } + /* 원본 문서를 찾고 재전송 여부를 확인 */ + EXEC SQL SELECT count(*), coalesce(max(amount),0) INTO :h_cnt, :h_amt + FROM ac_edi WHERE doc_no = :h_docno; + if (sqlca.sqlcode < 0) { + userlog("%s: 원문서 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_cnt == 0) { Bchg(b, T_MSG, 0, "원 EDI 문서 없음", 0L); setl(b, T_RC, 140); FAIL(b); } + if (strstr(h_docno, "-R") != NULL) { Bchg(b, T_MSG, 0, "이미 재전송본", 0L); setl(b, T_RC, 141); FAIL(b); } + if (h_amt >= 1000000) { h_val = 3; } + else if (h_amt >= 100000) { h_val = 2; } + else { h_val = 1; } + snprintf(logmsg, sizeof(logmsg), "%s-R", h_docno); + strncpy(h_str1, logmsg, sizeof(h_str1)-1); h_str1[sizeof(h_str1)-1] = 0; - Bchg(b, T_DOCNO, 0, h_redoc, 0L); - setl(b, T_ID1, h_new); + /* 4) 본처리부: 부수행 기록 및 상태 갱신 */ + EXEC SQL SELECT nextval('ac_edi_seq') INTO :h_id; + if (sqlca.sqlcode < 0) { + userlog("%s: EDI채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO ac_edi (edi_id, merchant_id, doc_no, amount, biz_date) VALUES (:h_id, :h_merch, :h_str1, :h_amt, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 재전송본 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_aff = 1; + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "EDI재전송 doc=%s -> %s 금액=%ld", h_docno, h_str1, h_amt); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_EDIRESEND', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_EDIRESEND' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_ID1, h_id); + Bchg(b, T_DOCNO, 0, h_str1, 0L); setl(b, T_AMOUNT, h_amt); + setl(b, T_ARG1, h_val); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_EDIRSNDCH.pgc b/app/src/ac/svc/ACQ_EDIRSNDCH.pgc index e9e09bd..27f9c2c 100644 --- a/app/src/ac/svc/ACQ_EDIRSNDCH.pgc +++ b/app/src/ac/svc/ACQ_EDIRSNDCH.pgc @@ -1,7 +1,7 @@ /* * ACQ_EDIRSNDCH.pgc - ac 모듈 서비스 ACQ_EDIRSNDCH (one service = one file). - * EDI 재전송 채널별: 영업일 미수신 EDI 문서를 가맹점별로 커서 조회해 - * 문서번호·금액 쌍을 occurrence 로 내리고 재전송 대상 건수를 집계한다. + * EDI 재전송 채널별: 영업일 미수신 EDI 문서를 가맹점별로 집계해 재전송 + * 대상 건수와 최다 대상 가맹점을 뽑아 응답한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_EDIRSNDCH.h" @@ -9,29 +9,146 @@ void ACQ_EDIRSNDCH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long n = 0, tot = 0; + const char *SVC = "ACQ_EDIRSNDCH"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_amt; - char h_doc[32], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL DECLARE edr_cur CURSOR FOR - SELECT doc_no, amount FROM ac_edi WHERE biz_date = :h_bizdate ORDER BY doc_no; - EXEC SQL OPEN edr_cur; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH edr_cur INTO :h_doc, :h_amt; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE edr_cur; FAIL(b); } - if (n < 8) { Badd(b, T_DOCNO, h_doc, 0L); Badd(b, T_AMT1, (char *)&h_amt, 0L); } - n++; tot += h_amt; + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); } - EXEC SQL CLOSE edr_cur; + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } - setl(b, T_COUNT, n); - setl(b, T_GROSS, tot); - setl(b, T_RC, n > 0 ? 0 : 1); + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), count(DISTINCT merchant_id) INTO :h_cnt, :h_gross, :h_n FROM ac_edi WHERE biz_date = :h_bizdate AND doc_no NOT LIKE '%-R'; + if (sqlca.sqlcode < 0) { + userlog("%s: 재전송대상 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(merchant_id,''), count(*) INTO :h_merch, :h_a FROM ac_edi WHERE biz_date = :h_bizdate AND doc_no NOT LIKE '%-R' GROUP BY merchant_id ORDER BY count(*) DESC LIMIT 1; + if (sqlca.sqlcode < 0) { + userlog("%s: 최다대상 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 대상 규모로 재전송 배치 편성 여부를 판정 */ + if (h_cnt > 0) ratio = (h_a * 10000L) / h_cnt; else ratio = 0; + if (h_cnt == 0) { h_val = 0; Bchg(b, T_MSG, 0, "재전송 대상 없음", 0L); } + else if (h_cnt >= 50) { h_val = 3; Bchg(b, T_MSG, 0, "대량 재전송 필요", 0L); } + else if (h_cnt >= 10) { h_val = 2; Bchg(b, T_MSG, 0, "배치 재전송 권고", 0L); } + else { h_val = 1; Bchg(b, T_MSG, 0, "개별 재전송", 0L); } + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "EDI재전송 bizdate=%s 대상=%ld 가맹점=%ld 최다=%s", h_bizdate, h_cnt, h_n, h_merch); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_EDIRSNDCH', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_EDIRSNDCH' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_GROSS, h_gross); + setl(b, T_ARG1, h_n); + setl(b, T_ARG2, h_a); + setl(b, T_ARG3, ratio); + setl(b, T_ARG4, h_val); + Bchg(b, T_MERCHANT, 0, h_merch, 0L); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_EDISTAT.pgc b/app/src/ac/svc/ACQ_EDISTAT.pgc index b53aaaf..e57e6f5 100644 --- a/app/src/ac/svc/ACQ_EDISTAT.pgc +++ b/app/src/ac/svc/ACQ_EDISTAT.pgc @@ -9,22 +9,149 @@ void ACQ_EDISTAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_EDISTAT"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_lo = 0, h_mid = 0, h_hi = 0, h_sum = 0; - char h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL SELECT count(*) FILTER (WHERE amount < 100000), - count(*) FILTER (WHERE amount >= 100000 AND amount < 1000000), - count(*) FILTER (WHERE amount >= 1000000), coalesce(sum(amount),0) - INTO :h_lo, :h_mid, :h_hi, :h_sum FROM ac_edi WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); - setl(b, T_ARG1, h_lo); - setl(b, T_ARG2, h_mid); - setl(b, T_ARG3, h_hi); - setl(b, T_GROSS, h_sum); + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } + + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*) FILTER (WHERE amount < 100000), count(*) FILTER (WHERE amount BETWEEN 100000 AND 999999), count(*) FILTER (WHERE amount >= 1000000) INTO :h_a, :h_b, :h_c FROM ac_edi WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 금액대분해 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), count(DISTINCT merchant_id) INTO :h_cnt, :h_gross, :h_n FROM ac_edi WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 총계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 고액 문서 비중으로 EDI 청구 성격을 분류 */ + if (h_cnt > 0) ratio = (h_c * 10000L) / h_cnt; else ratio = 0; + if (h_cnt == 0) { h_val = 0; } + else if (ratio >= 3000) { h_val = 3; } + else if (h_c > 0) { h_val = 2; } + else { h_val = 1; } + h_avg = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "EDI통계 bizdate=%s 소액=%ld 중액=%ld 고액=%ld 가맹점=%ld", h_bizdate, h_a, h_b, h_c, h_n); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_EDISTAT', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_EDISTAT' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_ARG1, h_a); + setl(b, T_ARG2, h_b); + setl(b, T_ARG3, h_c); + setl(b, T_ARG4, ratio); + setl(b, T_ARG5, h_val); + setl(b, T_COUNT, h_cnt); + setl(b, T_GROSS, h_gross); + setl(b, T_ID1, h_n); + setl(b, T_AMT1, h_avg); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_ERRCLASS.pgc b/app/src/ac/svc/ACQ_ERRCLASS.pgc index ce4c541..83ea459 100644 --- a/app/src/ac/svc/ACQ_ERRCLASS.pgc +++ b/app/src/ac/svc/ACQ_ERRCLASS.pgc @@ -1,46 +1,158 @@ /* * ACQ_ERRCLASS.pgc - ac 모듈 서비스 ACQ_ERRCLASS (one service = one file). - * 매입 오류유형별 분류: 미매입 원장(ac_unmatch)의 사유 접두어를 읽어 - * 중복/한도/환율/기타 4개 오류군으로 분류하고 군별 건수를 집계한다. + * 매입 오류유형별 분류: 미매입 원장의 사유 접두어를 읽어 중복/한도/환율/기타 + * 4개 오류군으로 분류하고 군별 건수를 집계한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_ERRCLASS.h" -/* 사유 접두어 → 오류군 (0=중복 1=한도 2=환율 3=기타) */ -static int ac_errkind(const char *r) -{ - if (strncmp(r, "DUP", 3) == 0) return 0; - if (strncmp(r, "LIMIT", 5) == 0) return 1; - if (strncmp(r, "FX", 2) == 0) return 2; - return 3; -} - void ACQ_ERRCLASS(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long cls[4] = {0, 0, 0, 0}; + const char *SVC = "ACQ_ERRCLASS"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_reason[64], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL DECLARE erc_cur CURSOR FOR - SELECT reason FROM ac_unmatch WHERE biz_date = :h_bizdate; - EXEC SQL OPEN erc_cur; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH erc_cur INTO :h_reason; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE erc_cur; FAIL(b); } - cls[ac_errkind(h_reason)]++; + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); } - EXEC SQL CLOSE erc_cur; + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } - setl(b, T_ARG1, cls[0]); - setl(b, T_ARG2, cls[1]); - setl(b, T_ARG3, cls[2]); - setl(b, T_ARG4, cls[3]); - setl(b, T_COUNT, cls[0] + cls[1] + cls[2] + cls[3]); + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*) FILTER (WHERE reason LIKE 'DUP%'), count(*) FILTER (WHERE reason LIKE 'LIMIT%'), count(*) FILTER (WHERE reason LIKE 'FX%'), count(*) INTO :h_a, :h_b, :h_c, :h_cnt FROM ac_unmatch WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 오류군분해 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(DISTINCT purchase_id) INTO :h_n FROM ac_unmatch WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 연루매입 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 기타 오류군을 역산하고 최대 오류군을 판정 */ + h_d = h_cnt - (h_a + h_b + h_c); + h_max = h_a; h_top = 1; + if (h_b > h_max) { h_max = h_b; h_top = 2; } + if (h_c > h_max) { h_max = h_c; h_top = 3; } + if (h_d > h_max) { h_max = h_d; h_top = 4; } + if (h_cnt > 0) ratio = (h_max * 10000L) / h_cnt; else ratio = 0; + h_val = (h_cnt == 0) ? 0 : ((ratio >= 6000) ? 3 : 1); + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "오류분류 bizdate=%s 중복=%ld 한도=%ld 환율=%ld 기타=%ld", h_bizdate, h_a, h_b, h_c, h_d); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_ERRCLASS', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_ERRCLASS' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_ARG1, h_a); + setl(b, T_ARG2, h_b); + setl(b, T_ARG3, h_c); + setl(b, T_ARG4, h_d); + setl(b, T_ARG5, h_top); + setl(b, T_ARG6, ratio); + setl(b, T_COUNT, h_cnt); + setl(b, T_ID1, h_n); + setl(b, T_ARG7, h_val); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_ERRQRY.pgc b/app/src/ac/svc/ACQ_ERRQRY.pgc index a469274..658d422 100644 --- a/app/src/ac/svc/ACQ_ERRQRY.pgc +++ b/app/src/ac/svc/ACQ_ERRQRY.pgc @@ -1,7 +1,7 @@ /* * ACQ_ERRQRY.pgc - ac 모듈 서비스 ACQ_ERRQRY (one service = one file). * 매입 오류유형별 조회: 사유 접두어(T_REASON)로 시작하는 미매입 건을 - * 패턴 조회해 해당 오류유형의 건수를 응답한다. + * 패턴 조회해 해당 오류유형의 건수와 연루 금액을 응답한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_ERRQRY.h" @@ -9,20 +9,153 @@ void ACQ_ERRQRY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char pat[64]; + const char *SVC = "ACQ_ERRQRY"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_n = 0; - char h_like[70]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_REASON, pat, sizeof(pat)); - if (pat[0] == 0) strcpy(pat, "DUP"); - snprintf(h_like, sizeof(h_like), "%s%%", pat); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL SELECT count(*) INTO :h_n FROM ac_unmatch WHERE reason LIKE :h_like; - if (sqlca.sqlcode < 0) FAIL(b); + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); - setl(b, T_COUNT, h_n); - setl(b, T_RC, h_n > 0 ? 0 : 1); + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "DUP"); + snprintf(logmsg, sizeof(logmsg), "%s%%", reason); + strncpy(h_reason, logmsg, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } + + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*), count(DISTINCT purchase_id), coalesce(max(unmatch_id),0) INTO :h_cnt, :h_n, :h_top FROM ac_unmatch WHERE biz_date = :h_bizdate AND reason LIKE :h_reason; + if (sqlca.sqlcode < 0) { + userlog("%s: 유형조회 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(sum(p.amount),0) INTO :h_gross FROM ac_unmatch u JOIN purchase p ON p.purchase_id = u.purchase_id WHERE u.biz_date = :h_bizdate AND u.reason LIKE :h_reason; + if (sqlca.sqlcode < 0) { + userlog("%s: 연루금액 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_a FROM ac_unmatch WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 전체오류 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 지정 유형의 전체 대비 비중으로 대응 등급을 판정 */ + if (h_a > 0) ratio = (h_cnt * 10000L) / h_a; else ratio = 0; + if (h_cnt == 0) { h_val = 0; } + else if (ratio >= 5000) { h_val = 3; } + else if (ratio >= 2000) { h_val = 2; } + else { h_val = 1; } + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "오류조회 bizdate=%s 패턴=%s 건수=%ld 비중=%ld", h_bizdate, h_reason, h_cnt, ratio); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_ERRQRY', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_ERRQRY' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_ARG1, h_n); + setl(b, T_ARG2, ratio); + setl(b, T_ARG3, h_val); + setl(b, T_GROSS, h_gross); + setl(b, T_ID1, h_top); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_ERRRETRY.pgc b/app/src/ac/svc/ACQ_ERRRETRY.pgc index ab3aecb..6eb7b1a 100644 --- a/app/src/ac/svc/ACQ_ERRRETRY.pgc +++ b/app/src/ac/svc/ACQ_ERRRETRY.pgc @@ -1,7 +1,7 @@ /* * ACQ_ERRRETRY.pgc - ac 모듈 서비스 ACQ_ERRRETRY (one service = one file). * 매입 오류 재처리: 보류(H) 상태로 묶여 있던 오류 매입을 접수(A)로 되돌려 - * 재처리 큐에 태우고 반영 건수를 결과코드로 응답한다. + * 재처리 큐에 태우고 미매입 오류 이력을 정리한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_ERRRETRY.h" @@ -9,17 +9,154 @@ void ACQ_ERRRETRY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_ERRRETRY"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_aff; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); - EXEC SQL UPDATE purchase SET status = 'A', updated_at = now() - WHERE purchase_id = :h_pid AND status = 'H'; - if (sqlca.sqlcode < 0) { userlog("ACQ_ERRRETRY FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + userlog("%s 진입: 매입 상태 전이 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + + /* 3) 업무규칙 분기부: 상태 전이 가능여부/유형 판정(3+ 분기) */ + if (!(strcmp(h_prev, "H") == 0)) { + userlog("%s 거절: 상태 %s 에서 전이 불가", SVC, h_prev); + Bchg(b, T_MSG, 0, "상태 전이 불가", 0L); setl(b, T_RC, 120); FAIL(b); + } + /* 남아 있는 오류 사유 건수로 재처리 방식을 결정 */ + EXEC SQL SELECT count(*) INTO :h_cnt FROM ac_unmatch WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 오류이력 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_cnt == 0) { h_val = 1; Bchg(b, T_MSG, 0, "오류이력 없이 해제", 0L); } + else if (h_cnt >= 3) { h_val = 3; Bchg(b, T_MSG, 0, "다중 오류 재처리", 0L); } + else { h_val = 2; Bchg(b, T_MSG, 0, "단순 오류 재처리", 0L); } + + /* 4) 본처리부: 부수행 기록 및 상태 갱신 */ + EXEC SQL DELETE FROM ac_unmatch WHERE purchase_id = :h_pid AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 오류정리 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_rem = sqlca.sqlerrd[2]; + EXEC SQL UPDATE purchase SET status = 'A', updated_at = now() WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 상태갱신 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } h_aff = sqlca.sqlerrd[2]; if (h_aff > 0) Bchg(b, T_STATUS, 0, "A", 0L); - setl(b, T_RC, h_aff > 0 ? 0 : 1); + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "오류재처리 pid=%ld 오류=%ld 정리=%ld 등급=%ld", h_pid, h_cnt, h_rem, h_val); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_ERRRETRY', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_ERRRETRY' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_ARG1, h_rem); + setl(b, T_ARG2, h_val); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_ERRSTAT.pgc b/app/src/ac/svc/ACQ_ERRSTAT.pgc index de3dc78..f784d7a 100644 --- a/app/src/ac/svc/ACQ_ERRSTAT.pgc +++ b/app/src/ac/svc/ACQ_ERRSTAT.pgc @@ -1,7 +1,7 @@ /* * ACQ_ERRSTAT.pgc - ac 모듈 서비스 ACQ_ERRSTAT (one service = one file). - * 매입 오류유형별 통계: 미매입 원장을 사유 접두어 기준으로 한 번의 집계에서 - * 중복/한도/환율 오류 건수를 FILTER 로 분해해 동시에 응답한다. + * 매입 오류유형별 통계: 미매입 원장을 사유 접두어 기준으로 중복/한도/환율 + * 오류 건수를 FILTER 로 분해해 동시에 응답하고 해소율을 계산한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_ERRSTAT.h" @@ -9,22 +9,148 @@ void ACQ_ERRSTAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_ERRSTAT"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_dup = 0, h_lim = 0, h_fx = 0, h_all = 0; - char h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL SELECT count(*) FILTER (WHERE reason LIKE 'DUP%'), - count(*) FILTER (WHERE reason LIKE 'LIMIT%'), - count(*) FILTER (WHERE reason LIKE 'FX%'), count(*) - INTO :h_dup, :h_lim, :h_fx, :h_all FROM ac_unmatch WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); - setl(b, T_ARG1, h_dup); - setl(b, T_ARG2, h_lim); - setl(b, T_ARG3, h_fx); - setl(b, T_COUNT, h_all); + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } + + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*) FILTER (WHERE reason LIKE 'DUP%'), count(*) FILTER (WHERE reason LIKE 'LIMIT%'), count(*) FILTER (WHERE reason LIKE 'FX%'), count(*) INTO :h_a, :h_b, :h_c, :h_cnt FROM ac_unmatch WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 유형통계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_d FROM ac_unmatch u JOIN purchase p ON p.purchase_id = u.purchase_id WHERE u.biz_date = :h_bizdate AND p.status IN ('M','S'); + if (sqlca.sqlcode < 0) { + userlog("%s: 해소건수 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 해소율(원매입이 대사/정산으로 진행된 비율)로 잔존 위험 판정 */ + if (h_cnt > 0) ratio = (h_d * 10000L) / h_cnt; else ratio = 0; + h_e = h_cnt - h_d; + if (h_cnt == 0) { h_val = 0; } + else if (ratio >= 8000) { h_val = 1; } + else if (ratio >= 4000) { h_val = 2; } + else { h_val = 3; } + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "오류통계 bizdate=%s 총=%ld 해소=%ld 잔존=%ld 해소율=%ld", h_bizdate, h_cnt, h_d, h_e, ratio); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_ERRSTAT', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_ERRSTAT' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_ARG1, h_a); + setl(b, T_ARG2, h_b); + setl(b, T_ARG3, h_c); + setl(b, T_ARG4, h_d); + setl(b, T_ARG5, h_e); + setl(b, T_ARG6, ratio); + setl(b, T_ARG7, h_val); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_ESCROW.pgc b/app/src/ac/svc/ACQ_ESCROW.pgc index 8447a8d..63fd242 100644 --- a/app/src/ac/svc/ACQ_ESCROW.pgc +++ b/app/src/ac/svc/ACQ_ESCROW.pgc @@ -1,7 +1,7 @@ /* * ACQ_ESCROW.pgc - ac 모듈 서비스 ACQ_ESCROW (one service = one file). * 매입 에스크로 예치: 분쟁 소지 매입 금액을 정산 전 유보하기 위해 원장에 - * ESCROW 항목으로 예치액을 적립한다. 금액이 0 이하면 거절. + * ESCROW 항목으로 예치액을 적립한다. 금액이 0 이하면 거절한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_ESCROW.h" @@ -9,23 +9,152 @@ void ACQ_ESCROW(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_ESCROW"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_amt, h_lid; - char h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 원장 예치/보증/연계 요청", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } h_amt = getl(b, T_AMOUNT); - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - if (h_amt <= 0) { setl(b, T_RC, 3); FAIL(b); } + if (h_amt < 1L) { Bchg(b, T_MSG, 0, "금액 범위 오류", 0L); setl(b, T_RC, 104); FAIL(b); } + gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "일반"); + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - EXEC SQL SELECT nextval('ledger_seq') INTO :h_lid; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) - VALUES (:h_lid, :h_pid, 'ESCROW', :h_amt, :h_bizdate); - if (sqlca.sqlcode < 0) { userlog("ACQ_ESCROW FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; - setl(b, T_SETTLE_ID, h_lid); + /* 3) 업무규칙 분기부: 금액/유형/잔액 판정(3+ 분기) */ + /* 기존 예치 잔액을 확인해 과다 예치를 막는다 */ + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_sum FROM ledger + WHERE purchase_id = :h_pid AND entry_type = 'ESCROW'; + if (sqlca.sqlcode < 0) { + userlog("%s: 기예치 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_sum + h_amt > h_gross) { Bchg(b, T_MSG, 0, "예치 누계가 매입금액 초과", 0L); setl(b, T_RC, 196); FAIL(b); } + if (h_amt * 2 >= h_gross) { h_val = 3; Bchg(b, T_MSG, 0, "과반 예치", 0L); } + else if (h_sum > 0) { h_val = 2; Bchg(b, T_MSG, 0, "추가 예치", 0L); } + else { h_val = 1; Bchg(b, T_MSG, 0, "최초 예치", 0L); } + + /* 4) 본처리부: 원장 분개 기록 */ + EXEC SQL SELECT nextval('ledger_seq') INTO :h_ledid; + if (sqlca.sqlcode < 0) { + userlog("%s: 예치채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) VALUES (:h_ledid, :h_pid, 'ESCROW', :h_amt, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 예치분개 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_aff = 1; + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_ctx FROM ledger WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 원장재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "에스크로예치 pid=%ld 예치=%ld 누계=%ld 등급=%ld", h_pid, h_amt, h_sum + h_amt, h_val); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_ESCROW', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_ESCROW' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_ID1, h_ledid); + setl(b, T_AMOUNT, h_amt); + setl(b, T_AMT1, h_sum + h_amt); + setl(b, T_ARG1, h_val); + setl(b, T_GROSS, h_gross); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_ESCROWREL.pgc b/app/src/ac/svc/ACQ_ESCROWREL.pgc index 98e08c5..6324248 100644 --- a/app/src/ac/svc/ACQ_ESCROWREL.pgc +++ b/app/src/ac/svc/ACQ_ESCROWREL.pgc @@ -9,28 +9,154 @@ void ACQ_ESCROWREL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_ESCROWREL"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_esc = 0, h_rel = 0, h_lid, h_bal; - char h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 원장 예치/보증/연계 요청", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ h_pid = getl(b, T_PURCHASE_ID); - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "일반"); + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + + /* 3) 업무규칙 분기부: 금액/유형/잔액 판정(3+ 분기) */ + /* 예치/해제 원장을 차감해 잔여 예치금을 구한다 */ EXEC SQL SELECT coalesce(sum(amount) FILTER (WHERE entry_type = 'ESCROW'),0), - coalesce(sum(amount) FILTER (WHERE entry_type = 'ESCROW_REL'),0) - INTO :h_esc, :h_rel FROM ledger WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - h_bal = h_esc - h_rel; - if (h_bal <= 0) { setl(b, T_GROSS, 0); setl(b, T_RC, 1); OK(b); } + coalesce(sum(amount) FILTER (WHERE entry_type = 'ESCROW_REL'),0) + INTO :h_sum, :h_val2 FROM ledger WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 예치잔액 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_rem = h_sum - h_val2; + if (h_rem <= 0) { Bchg(b, T_MSG, 0, "해제할 예치 잔액 없음", 0L); setl(b, T_RC, 197); FAIL(b); } + if (h_rem >= h_gross) { h_val = 3; Bchg(b, T_MSG, 0, "전액 예치 해제", 0L); } + else if (h_val2 > 0) { h_val = 2; Bchg(b, T_MSG, 0, "부분 추가 해제", 0L); } + else { h_val = 1; Bchg(b, T_MSG, 0, "최초 해제", 0L); } + h_amt = h_rem; - EXEC SQL SELECT nextval('ledger_seq') INTO :h_lid; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) - VALUES (:h_lid, :h_pid, 'ESCROW_REL', :h_bal, :h_bizdate); - if (sqlca.sqlcode < 0) { userlog("ACQ_ESCROWREL FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + /* 4) 본처리부: 원장 분개 기록 */ + EXEC SQL SELECT nextval('ledger_seq') INTO :h_ledid; + if (sqlca.sqlcode < 0) { + userlog("%s: 해제채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) VALUES (:h_ledid, :h_pid, 'ESCROW_REL', :h_amt, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 해제분개 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_aff = 1; - setl(b, T_GROSS, h_bal); + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_ctx FROM ledger WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 원장재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "에스크로해제 pid=%ld 예치=%ld 기해제=%ld 해제=%ld", h_pid, h_sum, h_val2, h_amt); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_ESCROWREL', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_ESCROWREL' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_ID1, h_ledid); + setl(b, T_AMOUNT, h_amt); + setl(b, T_AMT1, h_sum); + setl(b, T_AMT2, h_val2); + setl(b, T_ARG1, 0); + setl(b, T_ARG2, h_val); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_EVENTBUY.pgc b/app/src/ac/svc/ACQ_EVENTBUY.pgc index f00a85a..4385ba1 100644 --- a/app/src/ac/svc/ACQ_EVENTBUY.pgc +++ b/app/src/ac/svc/ACQ_EVENTBUY.pgc @@ -9,33 +9,151 @@ void ACQ_EVENTBUY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long fee, disc; + const char *SVC = "ACQ_EVENTBUY"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_amt, h_bps = 0, h_disc, h_fee, h_net; - char h_merch[64], h_mst[16], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 신규 매입 접수 요청", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + if (merch[0] == 0) { Bchg(b, T_MSG, 0, "가맹점 ID 누락", 0L); setl(b, T_RC, 101); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; h_amt = getl(b, T_AMOUNT); - disc = getl(b, T_ARG1); if (disc < 0) disc = 0; - gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - if (h_amt <= 0) { setl(b, T_RC, 3); FAIL(b); } + if (h_amt < 1L) { Bchg(b, T_MSG, 0, "금액 범위 오류", 0L); setl(b, T_RC, 104); FAIL(b); } + threshold = getl(b, T_ARG1); + if (threshold < 0 || threshold > 10000) { Bchg(b, T_MSG, 0, "할인율 범위 오류(0~10000bps)", 0L); setl(b, T_RC, 163); FAIL(b); } + if (threshold == 0) threshold = 2000; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - EXEC SQL SELECT status, mdr_bps INTO :h_mst, :h_bps FROM merchant WHERE merchant_id = :h_merch; - if (sqlca.sqlcode == 100) { setl(b, T_RC, 5); FAIL(b); } - if (sqlca.sqlcode < 0) FAIL(b); - if (strcmp(h_mst, "ACTIVE") != 0) { setl(b, T_RC, 6); FAIL(b); } - h_disc = (h_bps > disc) ? h_bps - disc : 0; - fee = h_amt * h_disc / 10000; h_fee = fee; h_net = h_amt - fee; + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; - EXEC SQL SELECT nextval('purchase_seq') INTO :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL INSERT INTO purchase (purchase_id, merchant_id, amount, fee, net, status, biz_date, channel, category) - VALUES (:h_pid, :h_merch, :h_amt, :h_fee, :h_net, 'A', :h_bizdate, 'EVENT', 'PROMO'); - if (sqlca.sqlcode < 0) { userlog("ACQ_EVENTBUY FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + /* 3) 업무규칙 분기부: 가맹점 상태/금액 구간/채널 정책(3+ 분기) */ + if (strcmp(h_status, "ACTIVE") != 0) { Bchg(b, T_MSG, 0, "비활성 가맹점", 0L); setl(b, T_RC, 121); FAIL(b); } + /* 할인율 구간별로 승인 등급을 나눈다 */ + if (threshold >= 8000) { h_val = 3; Bchg(b, T_MSG, 0, "고할인 이벤트", 0L); } + else if (threshold >= 4000) { h_val = 2; Bchg(b, T_MSG, 0, "중할인 이벤트", 0L); } + else { h_val = 1; Bchg(b, T_MSG, 0, "일반 이벤트", 0L); } - setl(b, T_PURCHASE_ID, h_pid); + /* 4) 본처리부: 수수료 산출 및 매입/승인 원장 생성 */ + bps = h_rate; h_fee = acq_fee(h_amt, bps); h_net = h_amt - h_fee; + /* 이벤트 할인율만큼 수수료를 깎아 가맹점 정산금을 올린다 */ + h_delta = h_fee * threshold / 10000L; + h_fee = h_fee - h_delta; + h_net = h_amt - h_fee; + h_new = acdb_next_purchase_id(); + if (h_new < 0) { Bchg(b, T_MSG, 0, "매입 채번 실패", 0L); setl(b, T_RC, 93); FAIL(b); } + if (acdb_insert_purchase(h_new, h_merch, h_amt, h_fee, h_net, "A", "EVENT", h_bizdate) < 0) + { Bchg(b, T_MSG, 0, "매입 생성 실패", 0L); setl(b, T_RC, 94); FAIL(b); } + if (acdb_insert_approval(h_new, 1) < 0) { Bchg(b, T_MSG, 0, "승인 적재 실패", 0L); setl(b, T_RC, 95); FAIL(b); } + h_aff = 1; h_pid = h_new; + EXEC SQL UPDATE purchase SET category = 'EVENT' WHERE purchase_id = :h_new; + if (sqlca.sqlcode < 0) { + userlog("%s: 분류표시 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "이벤트매입 merch=%s 금액=%ld 할인율=%ld 감면=%ld", h_merch, h_amt, threshold, h_delta); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_EVENTBUY', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_EVENTBUY' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_PURCHASE_ID, h_new); setl(b, T_FEE, h_fee); + setl(b, T_NET, h_net); + setl(b, T_DELTA, h_delta); + setl(b, T_ARG2, threshold); + setl(b, T_ARG1, h_val); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_EXCHANGE.pgc b/app/src/ac/svc/ACQ_EXCHANGE.pgc index 93a52d7..e79dc14 100644 --- a/app/src/ac/svc/ACQ_EXCHANGE.pgc +++ b/app/src/ac/svc/ACQ_EXCHANGE.pgc @@ -1,6 +1,6 @@ /* * ACQ_EXCHANGE.pgc - ac 모듈 서비스 ACQ_EXCHANGE (one service = one file). - * 매입 교환 처리: 반품 후 동일가 재구매를 한 번에 처리한다. 원매입에 대해 + * 매입 교환 처리: 반품 후 동일가 재구매를 한 번에 처리한다. 원매입에 * EXCHANGE 취소 행을 남기고 원매입 상태를 대사완료(M)로 유지한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ @@ -9,30 +9,169 @@ void ACQ_EXCHANGE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char why[80]; + const char *SVC = "ACQ_EXCHANGE"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_id, h_pid, h_amt, h_orig = 0; - char h_reason[100], h_st[4], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 매입 상태 전이 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ h_pid = getl(b, T_PURCHASE_ID); - h_amt = getl(b, T_AMOUNT); - gets_(b, T_REASON, why, sizeof(why)); - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "일반"); + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - EXEC SQL SELECT status, amount INTO :h_st, :h_orig FROM purchase WHERE purchase_id = :h_pid; - if (sqlca.sqlcode == 100) { setl(b, T_RC, 5); FAIL(b); } - if (sqlca.sqlcode < 0) FAIL(b); - if (h_amt <= 0 || h_amt > h_orig) { setl(b, T_RC, 3); FAIL(b); } - snprintf(h_reason, sizeof(h_reason), "EXCHANGE:%s", why[0] ? why : "SWAP"); + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; - 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, 'EXCHANGE', :h_amt, :h_reason, :h_bizdate); - if (sqlca.sqlcode < 0) { userlog("ACQ_EXCHANGE FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + /* 3) 업무규칙 분기부: 상태 전이 가능여부/유형 판정(3+ 분기) */ + if (!(strcmp(h_prev, "A") == 0 || strcmp(h_prev, "M") == 0)) { + userlog("%s 거절: 상태 %s 에서 전이 불가", SVC, h_prev); + Bchg(b, T_MSG, 0, "상태 전이 불가", 0L); setl(b, T_RC, 120); FAIL(b); + } + /* 교환 금액과 원금 차이로 차액 정산 여부를 결정 */ + h_amt = getl(b, T_AMOUNT); if (h_amt <= 0) h_amt = h_gross; + h_delta = h_amt - h_gross; + if (h_delta > 0) { h_val = 3; Bchg(b, T_MSG, 0, "상위 교환(추가징수)", 0L); } + else if (h_delta < 0) { h_val = 2; Bchg(b, T_MSG, 0, "하위 교환(차액환급)", 0L); } + else { h_val = 1; Bchg(b, T_MSG, 0, "동일가 교환", 0L); } - setl(b, T_CANCEL_ID, h_id); + /* 4) 본처리부: 부수행 기록 및 상태 갱신 */ + EXEC SQL SELECT nextval('ac_cancel_seq') INTO :h_canid; + if (sqlca.sqlcode < 0) { + userlog("%s: 취소채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO ac_cancel (cancel_id, purchase_id, cancel_type, amount, reason, biz_date) VALUES (:h_canid, :h_pid, 'EXCHANGE', :h_gross, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 교환행 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT nextval('ledger_seq') INTO :h_ledid; + if (sqlca.sqlcode < 0) { + userlog("%s: 원장채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) VALUES (:h_ledid, :h_pid, 'EXCHANGE', :h_delta, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 차액분개 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL UPDATE purchase SET status = 'M', updated_at = now() WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 상태갱신 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_aff = sqlca.sqlerrd[2]; + if (h_aff > 0) Bchg(b, T_STATUS, 0, "M", 0L); + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "매입교환 pid=%ld 원금=%ld 교환가=%ld 차액=%ld", h_pid, h_gross, h_amt, h_delta); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_EXCHANGE', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_EXCHANGE' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_CANCEL_ID, h_canid); + setl(b, T_DELTA, h_delta); + setl(b, T_AMOUNT, h_amt); + setl(b, T_ARG1, h_val); + setl(b, T_ID1, h_ledid); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_FEEADJ.pgc b/app/src/ac/svc/ACQ_FEEADJ.pgc index 2ea194d..26841b9 100644 --- a/app/src/ac/svc/ACQ_FEEADJ.pgc +++ b/app/src/ac/svc/ACQ_FEEADJ.pgc @@ -1,30 +1,155 @@ /* * ACQ_FEEADJ.pgc - ac 모듈 서비스 ACQ_FEEADJ (one service = one file). - * Moved verbatim from the former inline ac_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by ac_svr at tpsvrinit. + * 수수료 조정: 요청 증감액(T_DELTA)만큼 수수료 조정 이력을 남기고 + * purchase 의 fee 를 가산, net 을 차감해 정산금에 반영한다. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_FEEADJ.h" void ACQ_FEEADJ(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], reason[128]; - long pid, delta, adj; + const char *SVC = "ACQ_FEEADJ"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long e_pid, e_delta; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; 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); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); - 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); + userlog("%s 진입: 매입 금액/수수료 조정 요청", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "일반"); + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + + /* 3) 업무규칙 분기부: 조정 유형/부호/한도 판정(3+ 분기) */ + /* 조정 폭이 원 수수료 대비 과도하면 거절 */ + h_delta = getl(b, T_DELTA); + if (h_delta == 0) { Bchg(b, T_MSG, 0, "조정액 0", 0L); setl(b, T_RC, 170); FAIL(b); } + if (h_fee + h_delta < 0) { Bchg(b, T_MSG, 0, "조정 후 수수료 음수", 0L); setl(b, T_RC, 171); FAIL(b); } + if (h_delta > 0 && h_delta > h_fee) { h_val = 3; } + else if (h_delta > 0) { h_val = 2; } + else { h_val = 1; } + + /* 4) 본처리부: 조정 원장 기록 및 매입 금액 보정 */ + h_adjid = acdb_insert_fee_adj(h_pid, h_delta, h_reason, h_bizdate); + if (h_adjid < 0) { Bchg(b, T_MSG, 0, "조정 이력 적재 실패", 0L); setl(b, T_RC, 98); FAIL(b); } + EXEC SQL UPDATE purchase SET fee = fee + :h_delta, net = net - :h_delta, updated_at = now() WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 수수료반영 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_aff = sqlca.sqlerrd[2]; + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "수수료조정 pid=%ld 증감=%ld 조정후=%ld 사유=%s", h_pid, h_delta, h_fee + h_delta, h_reason); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_FEEADJ', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_FEEADJ' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_ADJ, h_adjid); + setl(b, T_DELTA, h_delta); + setl(b, T_FEE, h_fee + h_delta); + setl(b, T_NET, h_net - h_delta); + setl(b, T_ARG1, h_val); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_FEEHIST.pgc b/app/src/ac/svc/ACQ_FEEHIST.pgc index dd40009..1dd6b25 100644 --- a/app/src/ac/svc/ACQ_FEEHIST.pgc +++ b/app/src/ac/svc/ACQ_FEEHIST.pgc @@ -1,7 +1,7 @@ /* * ACQ_FEEHIST.pgc - ac 모듈 서비스 ACQ_FEEHIST (one service = one file). * 수수료 조정 이력 집계: 매입 건의 조정 건수, 순증감 합계, 증액분/감액분을 - * FILTER 집계로 한 번에 산출한다. 순증감이 0 이 아니면 T_RC=1 로 표시. + * FILTER 집계로 한 번에 산출하고 조정 여부를 결과코드로 표시한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_FEEHIST.h" @@ -9,24 +9,151 @@ void ACQ_FEEHIST(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_FEEHIST"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_n = 0, h_net = 0, h_up = 0, h_dn = 0; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } - EXEC SQL SELECT count(*), - coalesce(sum(delta), 0), - coalesce(sum(delta) FILTER (WHERE delta > 0), 0), - coalesce(sum(delta) FILTER (WHERE delta < 0), 0) - INTO :h_n, :h_net, :h_up, :h_dn - FROM ac_fee_adj WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0) { userlog("ACQ_FEEHIST FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - setl(b, T_COUNT, h_n); - setl(b, T_DELTA, h_net); /* 순증감 */ - setl(b, T_AMT1, h_up); /* 증액 합계 */ - setl(b, T_AMT2, h_dn); /* 감액 합계(음수) */ - setl(b, T_RC, h_net != 0 ? 1 : 0); + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*), coalesce(sum(delta),0), coalesce(sum(delta) FILTER (WHERE delta > 0),0), coalesce(sum(delta) FILTER (WHERE delta < 0),0) INTO :h_cnt, :h_a, :h_b, :h_c FROM ac_fee_adj WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 조정집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT amount, fee, net, status INTO :h_amt, :h_fee, :h_net, :h_status FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입현황 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 순증감 방향과 원수수료 대비 조정폭으로 등급 판정 */ + if (h_fee > 0) ratio = (h_a * 10000L) / h_fee; else ratio = 0; + if (h_cnt == 0) { h_val = 0; } + else if (h_a > 0) { h_val = 1; } + else if (h_a < 0) { h_val = 2; } + else { h_val = 3; } + h_gross = h_amt; + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "수수료이력 pid=%ld 건수=%ld 순증감=%ld 증액=%ld 감액=%ld", h_pid, h_cnt, h_a, h_b, h_c); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_FEEHIST', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_FEEHIST' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_DELTA, h_a); + setl(b, T_AMT1, h_b); + setl(b, T_AMT2, h_c); + setl(b, T_FEE, h_fee); + setl(b, T_NET, h_net); + setl(b, T_ARG1, ratio); + setl(b, T_ARG2, h_val); + Bchg(b, T_STATUS, 0, h_status, 0L); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_FEERECALC.pgc b/app/src/ac/svc/ACQ_FEERECALC.pgc index 2de429c..aa1731e 100644 --- a/app/src/ac/svc/ACQ_FEERECALC.pgc +++ b/app/src/ac/svc/ACQ_FEERECALC.pgc @@ -1,6 +1,6 @@ /* * ACQ_FEERECALC.pgc - ac 모듈 서비스 ACQ_FEERECALC (one service = one file). - * 수수료 재산정: 가맹점의 현행 MDR 로 수수료를 다시 계산해 기존 수수료와의 + * 수수료 재산정: 가맹점의 현행 MDR 로 수수료를 다시 계산해 기존 수수료와 * 차액이 있으면 조정 이력을 남기고 purchase 의 fee/net 을 보정한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ @@ -9,36 +9,143 @@ void ACQ_FEERECALC(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long bps = 0, newfee, delta, adj; - char bizdate[16]; + const char *SVC = "ACQ_FEERECALC"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_amt = 0, h_fee = 0, h_newfee; - char h_merch[64]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 매입 금액/수수료 조정 요청", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - EXEC SQL SELECT merchant_id, amount, fee INTO :h_merch, :h_amt, :h_fee - FROM purchase WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; - if (acdb_merchant_mdr(h_merch, &bps) < 0) FAIL(b); - newfee = acq_fee(h_amt, bps); - delta = newfee - h_fee; + /* 3) 업무규칙 분기부: 조정 유형/부호/한도 판정(3+ 분기) */ + /* 현행 MDR 로 재계산해 차액 유무를 판정 */ + computed = acq_fee(h_gross, h_rate); + h_delta = computed - h_fee; + if (h_delta == 0) { Bchg(b, T_MSG, 0, "재산정 차액 없음", 0L); setl(b, T_RC, 172); FAIL(b); } + else if (h_delta > 0) { h_val = 3; Bchg(b, T_MSG, 0, "수수료 과소 징수분 추징", 0L); } + else { h_val = 2; Bchg(b, T_MSG, 0, "수수료 과다 징수분 환급", 0L); } - if (delta != 0) { - adj = acdb_insert_fee_adj(h_pid, delta, "RECALC", bizdate); - if (adj < 0) FAIL(b); - h_newfee = newfee; - EXEC SQL UPDATE purchase SET fee = :h_newfee, net = amount - :h_newfee, updated_at = now() - WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0) { userlog("ACQ_FEERECALC FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } - setl(b, T_ADJ, adj); + /* 4) 본처리부: 조정 원장 기록 및 매입 금액 보정 */ + h_adjid = acdb_insert_fee_adj(h_pid, h_delta, "RECALC", h_bizdate); + if (h_adjid < 0) { Bchg(b, T_MSG, 0, "재산정 이력 실패", 0L); setl(b, T_RC, 98); FAIL(b); } + h_fee = computed; h_net = h_gross - h_fee; + if (acdb_update_amount(h_pid, h_gross, h_fee, h_net) < 0) + { Bchg(b, T_MSG, 0, "재산정 반영 실패", 0L); setl(b, T_RC, 97); FAIL(b); } + h_aff = 1; + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "수수료재산정 pid=%ld mdr=%ld 차액=%ld 신수수료=%ld", h_pid, h_rate, h_delta, h_fee); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_FEERECALC', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); } - setl(b, T_FEE, newfee); - setl(b, T_DELTA, delta); - setl(b, T_RC, delta == 0 ? 1 : 0); /* 1=차액 없음 */ + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_FEERECALC' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_ADJ, h_adjid); + setl(b, T_FEE, h_fee); + setl(b, T_NET, h_net); + setl(b, T_DELTA, h_delta); + setl(b, T_ARG1, h_rate); + setl(b, T_ARG2, h_val); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_FEEWAIVE.pgc b/app/src/ac/svc/ACQ_FEEWAIVE.pgc index a414413..1ae6379 100644 --- a/app/src/ac/svc/ACQ_FEEWAIVE.pgc +++ b/app/src/ac/svc/ACQ_FEEWAIVE.pgc @@ -1,7 +1,7 @@ /* * ACQ_FEEWAIVE.pgc - ac 모듈 서비스 ACQ_FEEWAIVE (one service = one file). * 수수료 감면: 요청 감면율(bps)만큼 수수료를 깎아 가맹점 정산금에 돌려준다. - * 음수 delta 조정 이력을 남기고 purchase 의 fee 차감 / net 증액을 반영. + * 음수 delta 조정 이력을 남기고 purchase 의 fee 차감 / net 증액을 반영한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_FEEWAIVE.h" @@ -9,35 +9,151 @@ void ACQ_FEEWAIVE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long rate, waive, adj; - char bizdate[16]; + const char *SVC = "ACQ_FEEWAIVE"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_fee = 0, h_w; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 매입 금액/수수료 조정 요청", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ h_pid = getl(b, T_PURCHASE_ID); - rate = getl(b, T_ARG1); /* 감면율 bps (1~10000) */ + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + threshold = getl(b, T_ARG1); + if (threshold <= 0 || threshold > 10000) { Bchg(b, T_MSG, 0, "감면율 범위 오류", 0L); setl(b, T_RC, 173); FAIL(b); } + gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "일반"); + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (rate < 1 || rate > 10000) { setl(b, T_RC, 9); FAIL(b); } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - EXEC SQL SELECT fee INTO :h_fee FROM purchase - WHERE purchase_id = :h_pid AND status NOT IN ('C', 'V'); - if (sqlca.sqlcode == 100) { setl(b, T_RC, 5); FAIL(b); } - if (sqlca.sqlcode < 0) FAIL(b); + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; - waive = acq_fee(h_fee, rate); /* fee * rate / 10000 */ - if (waive <= 0) { setl(b, T_RC, 1); setl(b, T_AMT1, 0); OK(b); } + /* 3) 업무규칙 분기부: 조정 유형/부호/한도 판정(3+ 분기) */ + /* 감면율 구간별 승인 등급과 감면액 산출 */ + computed = h_fee * threshold / 10000L; + if (computed <= 0) { Bchg(b, T_MSG, 0, "감면액 없음", 0L); setl(b, T_RC, 174); FAIL(b); } + if (threshold >= 10000) { h_val = 3; Bchg(b, T_MSG, 0, "전액 감면", 0L); } + else if (threshold >= 5000) { h_val = 2; Bchg(b, T_MSG, 0, "과반 감면", 0L); } + else { h_val = 1; Bchg(b, T_MSG, 0, "부분 감면", 0L); } + h_delta = -computed; - adj = acdb_insert_fee_adj(h_pid, -waive, "WAIVE", bizdate); - if (adj < 0) FAIL(b); + /* 4) 본처리부: 조정 원장 기록 및 매입 금액 보정 */ + h_adjid = acdb_insert_fee_adj(h_pid, h_delta, h_reason, h_bizdate); + if (h_adjid < 0) { Bchg(b, T_MSG, 0, "감면 이력 실패", 0L); setl(b, T_RC, 98); FAIL(b); } + h_comp = computed; + EXEC SQL UPDATE purchase SET fee = fee - :h_comp, net = net + :h_comp, updated_at = now() WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 감면반영 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_aff = sqlca.sqlerrd[2]; - h_w = waive; - EXEC SQL UPDATE purchase SET fee = fee - :h_w, net = net + :h_w, updated_at = now() - WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0) { userlog("ACQ_FEEWAIVE FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); - setl(b, T_AMT1, waive); - setl(b, T_ADJ, adj); + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "수수료감면 pid=%ld 감면율=%ld 감면액=%ld 잔여수수료=%ld", h_pid, threshold, computed, h_fee - computed); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_FEEWAIVE', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_FEEWAIVE' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_ADJ, h_adjid); + setl(b, T_DELTA, h_delta); + setl(b, T_FEE, h_fee - computed); + setl(b, T_NET, h_net + computed); + setl(b, T_ARG2, threshold); + setl(b, T_ARG3, h_val); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_FOREIGN.pgc b/app/src/ac/svc/ACQ_FOREIGN.pgc index d788547..da4fe35 100644 --- a/app/src/ac/svc/ACQ_FOREIGN.pgc +++ b/app/src/ac/svc/ACQ_FOREIGN.pgc @@ -1,38 +1,174 @@ /* * ACQ_FOREIGN.pgc - ac 모듈 서비스 ACQ_FOREIGN (one service = one file). - * Moved verbatim from the former inline ac_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by ac_svr at tpsvrinit. + * 해외 매입 접수: 해외 카드 매입을 FOREIGN 채널로 접수한다. 원통화 금액과 + * 적용 환율로 원화를 환산해 매입 금액을 확정하고 ac_fx 원장에 남긴다. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_FOREIGN.h" void ACQ_FOREIGN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16], ccy[8]; - long amount, bps = 30, fee, net, pid; + const char *SVC = "ACQ_FOREIGN"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_id, h_pid, h_fx, h_krw, h_rate; - char h_ccy[8], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; 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); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); - h_pid = pid; h_fx = amount; h_rate = 1350; h_krw = amount; + userlog("%s 진입: 신규 매입 접수 요청", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + if (merch[0] == 0) { Bchg(b, T_MSG, 0, "가맹점 ID 누락", 0L); setl(b, T_RC, 101); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + gets_(b, T_CCY, ccy, sizeof(ccy)); if (ccy[0] == 0) strcpy(ccy, "USD"); strncpy(h_ccy, ccy, sizeof(h_ccy)-1); h_ccy[sizeof(h_ccy)-1] = 0; + h_fx = getl(b, T_FX_AMT); + if (h_fx <= 0) { Bchg(b, T_MSG, 0, "원통화 금액 오류", 0L); setl(b, T_RC, 167); FAIL(b); } + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + + /* 3) 업무규칙 분기부: 가맹점 상태/금액 구간/채널 정책(3+ 분기) */ + if (strcmp(h_status, "ACTIVE") != 0) { Bchg(b, T_MSG, 0, "비활성 가맹점", 0L); setl(b, T_RC, 121); FAIL(b); } + /* 기준환율을 읽어 원화 환산액을 확정한다 */ + EXEC SQL SELECT coalesce(rate_bps,0) INTO :h_rate2 FROM cm_fx_rate + WHERE ccy = :h_ccy ORDER BY base_date DESC LIMIT 1; + if (sqlca.sqlcode < 0) { + userlog("%s: 기준환율 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_rate2 <= 0) { Bchg(b, T_MSG, 0, "환율 미등록 통화", 0L); setl(b, T_RC, 168); FAIL(b); } + h_krw = h_fx * h_rate2 / 100L; + h_amt = h_krw; + if (h_krw >= 5000000) { h_val = 3; } + else if (h_krw >= 500000) { h_val = 2; } + else { h_val = 1; } + + /* 4) 본처리부: 수수료 산출 및 매입/승인 원장 생성 */ + bps = h_rate; h_fee = acq_fee(h_amt, bps); h_net = h_amt - h_fee; + /* 환산 내역을 해외매입 원장에 기록 */ 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); + if (sqlca.sqlcode < 0) { + userlog("%s: 환산채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO ac_fx (fx_id, purchase_id, ccy, fx_amount, krw_amount, rate_bps, biz_date) VALUES (:h_id, 0, :h_ccy, :h_fx, :h_krw, :h_rate2, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 환산원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 환산 확정 후 수수료를 원화 기준으로 재산출 */ + h_fee = acq_fee(h_amt, h_rate); h_net = h_amt - h_fee; + h_new = acdb_next_purchase_id(); + if (h_new < 0) { Bchg(b, T_MSG, 0, "매입 채번 실패", 0L); setl(b, T_RC, 93); FAIL(b); } + if (acdb_insert_purchase(h_new, h_merch, h_amt, h_fee, h_net, "A", "FOREIGN", h_bizdate) < 0) + { Bchg(b, T_MSG, 0, "매입 생성 실패", 0L); setl(b, T_RC, 94); FAIL(b); } + if (acdb_insert_approval(h_new, 1) < 0) { Bchg(b, T_MSG, 0, "승인 적재 실패", 0L); setl(b, T_RC, 95); FAIL(b); } + h_aff = 1; h_pid = h_new; + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "해외매입 merch=%s ccy=%s 원통화=%ld 원화=%ld", h_merch, h_ccy, h_fx, h_krw); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_FOREIGN', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_FOREIGN' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_PURCHASE_ID, h_new); + setl(b, T_AMOUNT, h_krw); + setl(b, T_FX_AMT, h_fx); + setl(b, T_ARG2, h_rate2); + setl(b, T_FEE, h_fee); + setl(b, T_NET, h_net); + setl(b, T_ID1, h_id); + setl(b, T_ARG1, h_val); + Bchg(b, T_CCY, 0, h_ccy, 0L); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_FXCONVERT.pgc b/app/src/ac/svc/ACQ_FXCONVERT.pgc index 6671ded..8afdb82 100644 --- a/app/src/ac/svc/ACQ_FXCONVERT.pgc +++ b/app/src/ac/svc/ACQ_FXCONVERT.pgc @@ -9,35 +9,167 @@ void ACQ_FXCONVERT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long krw; + const char *SVC = "ACQ_FXCONVERT"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_fxid, h_pid, h_fx, h_rate, h_krw; - char h_ccy[8], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 매입 금액/수수료 조정 요청", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_CCY, ccy, sizeof(ccy)); + if (ccy[0] == 0) { Bchg(b, T_MSG, 0, "통화코드 누락", 0L); setl(b, T_RC, 103); FAIL(b); } + strncpy(h_ccy, ccy, sizeof(h_ccy)-1); h_ccy[sizeof(h_ccy)-1] = 0; h_fx = getl(b, T_FX_AMT); - h_rate = getl(b, T_ARG1); /* 환율 bps: KRW = fx * rate / 10000 */ - gets_(b, T_CCY, h_ccy, sizeof(h_ccy)); - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - if (h_fx <= 0 || h_rate <= 0 || h_ccy[0] == 0) { setl(b, T_RC, 3); FAIL(b); } + if (h_fx <= 0) { Bchg(b, T_MSG, 0, "원통화 금액 오류", 0L); setl(b, T_RC, 167); FAIL(b); } + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - krw = (h_fx * h_rate) / 10000L; - h_krw = krw; + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; - EXEC SQL SELECT nextval('ac_fx_seq') INTO :h_fxid; - 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_fxid, :h_pid, :h_ccy, :h_fx, :h_krw, :h_rate, :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); + /* 3) 업무규칙 분기부: 조정 유형/부호/한도 판정(3+ 분기) */ + /* 요청 환율이 없으면 기준환율을 채택한다 */ + h_rate2 = getl(b, T_ARG1); + if (h_rate2 <= 0) { + EXEC SQL SELECT coalesce(rate_bps,0) INTO :h_rate2 FROM cm_fx_rate + WHERE ccy = :h_ccy ORDER BY base_date DESC LIMIT 1; + if (sqlca.sqlcode < 0) { + userlog("%s: 기준환율 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + } + if (h_rate2 <= 0) { Bchg(b, T_MSG, 0, "적용 환율 없음", 0L); setl(b, T_RC, 168); FAIL(b); } + h_krw = (h_fx * h_rate2) / 10000L; + if (h_krw >= 5000000) { h_val = 3; } + else if (h_krw >= 500000) { h_val = 2; } + else { h_val = 1; } - /* 해외매입 채널 표시 (존재하는 매입에만 반영) */ - EXEC SQL UPDATE purchase SET channel = 'FOREIGN', updated_at = now() - WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0) { userlog("ACQ_FXCONVERT FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + /* 4) 본처리부: 조정 원장 기록 및 매입 금액 보정 */ + EXEC SQL SELECT nextval('ac_fx_seq') INTO :h_id; + if (sqlca.sqlcode < 0) { + userlog("%s: 환산채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); 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_rate2, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 환산원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL UPDATE purchase SET channel = 'FOREIGN', updated_at = now() WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 채널표시 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_aff = sqlca.sqlerrd[2]; - setl(b, T_ID1, h_fxid); - setl(b, T_AMOUNT, krw); + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "통화환산 pid=%ld ccy=%s 원통화=%ld 원화=%ld", h_pid, h_ccy, h_fx, h_krw); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_FXCONVERT', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_FXCONVERT' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_ID1, h_id); + setl(b, T_AMOUNT, h_krw); + setl(b, T_FX_AMT, h_fx); + setl(b, T_ARG2, h_rate2); + setl(b, T_ARG1, h_val); + Bchg(b, T_CCY, 0, h_ccy, 0L); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_FXRATE.pgc b/app/src/ac/svc/ACQ_FXRATE.pgc index 9fe4715..779ee4a 100644 --- a/app/src/ac/svc/ACQ_FXRATE.pgc +++ b/app/src/ac/svc/ACQ_FXRATE.pgc @@ -1,7 +1,7 @@ /* * ACQ_FXRATE.pgc - ac 모듈 서비스 ACQ_FXRATE (one service = one file). - * 통화 적용환율 조회: 해외매입 원장(ac_fx)에서 특정 통화의 최근 영업일 - * 평균/최저/최고 환율(bps)을 조회한다. 건수가 없으면 T_RC=1. + * 통화 적용환율 조회: 해외매입 원장에서 특정 통화의 최근 영업일 평균/최저/ + * 최고 환율(bps)을 조회하고 기준환율과의 괴리를 계산한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_FXRATE.h" @@ -9,27 +9,157 @@ void ACQ_FXRATE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_FXRATE"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_n = 0, h_avg = 0, h_min = 0, h_max = 0; - char h_ccy[8], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_CCY, h_ccy, sizeof(h_ccy)); - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - if (h_ccy[0] == 0) { setl(b, T_RC, 8); FAIL(b); } + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL SELECT count(*), - coalesce(avg(rate_bps)::bigint, 0), - coalesce(min(rate_bps), 0), - coalesce(max(rate_bps), 0) - INTO :h_n, :h_avg, :h_min, :h_max - FROM ac_fx WHERE ccy = :h_ccy AND biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) { userlog("ACQ_FXRATE FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); - setl(b, T_ARG1, h_avg); + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_CCY, ccy, sizeof(ccy)); + if (ccy[0] == 0) { Bchg(b, T_MSG, 0, "통화코드 누락", 0L); setl(b, T_RC, 103); FAIL(b); } + strncpy(h_ccy, ccy, sizeof(h_ccy)-1); h_ccy[sizeof(h_ccy)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } + + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*), coalesce(avg(rate_bps),0)::bigint, coalesce(min(rate_bps),0), coalesce(max(rate_bps),0) INTO :h_cnt, :h_rate, :h_min, :h_max FROM ac_fx WHERE ccy = :h_ccy AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 적용환율 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(rate_bps,0) INTO :h_a FROM cm_fx_rate WHERE ccy = :h_ccy ORDER BY base_date DESC LIMIT 1; + if (sqlca.sqlcode < 0) { + userlog("%s: 기준환율 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(sum(krw_amount),0) INTO :h_gross FROM ac_fx WHERE ccy = :h_ccy AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 환산총액 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 적용-기준 환율 괴리와 당일 변동폭으로 환리스크 판정 */ + h_e = h_max - h_min; + h_b = h_rate - h_a; + if (h_cnt == 0) { h_val = 1; Bchg(b, T_MSG, 0, "환율 적용 실적 없음", 0L); } + else if (h_a > 0 && (h_b > h_a / 50 || h_b < -(h_a / 50))) { h_val = 3; Bchg(b, T_MSG, 0, "기준환율 괴리 과다", 0L); } + else if (h_e > 0) { h_val = 2; Bchg(b, T_MSG, 0, "당일 환율 변동", 0L); } + else { h_val = 0; Bchg(b, T_MSG, 0, "환율 안정", 0L); } + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "환율조회 ccy=%s 평균=%ld 기준=%ld 괴리=%ld", h_ccy, h_rate, h_a, h_b); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_FXRATE', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_FXRATE' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_ARG1, h_rate); setl(b, T_ARG2, h_min); setl(b, T_ARG3, h_max); - setl(b, T_COUNT, h_n); - setl(b, T_RC, h_n > 0 ? 0 : 1); + setl(b, T_ARG4, h_a); + setl(b, T_ARG5, h_b); + setl(b, T_ARG6, h_e); + setl(b, T_GROSS, h_gross); + setl(b, T_ID1, h_val); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_GRADESTAT.pgc b/app/src/ac/svc/ACQ_GRADESTAT.pgc index 0bd3e8e..b95c910 100644 --- a/app/src/ac/svc/ACQ_GRADESTAT.pgc +++ b/app/src/ac/svc/ACQ_GRADESTAT.pgc @@ -1,47 +1,159 @@ /* * ACQ_GRADESTAT.pgc - ac 모듈 서비스 ACQ_GRADESTAT (one service = one file). - * 매입 가맹점등급별 통계: 가맹점 MDR(bps)을 프리미엄/일반/기본 등급으로 나눠 - * 등급별 매출 합계를 버킷 집계한다. 등급 경계는 C 분류기로 판정한다. + * 매입 가맹점등급별 통계: 가맹점 MDR(bps)을 프리미엄/일반/기본 등급으로 + * 나눠 등급별 매출 합계를 버킷 집계한다. 등급 경계는 C 분류기로 판정한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_GRADESTAT.h" -/* MDR(bps) → 등급 (0=프리미엄 1=일반 2=기본) */ -static int ac_grade(long bps) -{ - if (bps <= 20) return 0; - if (bps <= 28) return 1; - return 2; -} - void ACQ_GRADESTAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long g[3] = {0, 0, 0}; + const char *SVC = "ACQ_GRADESTAT"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_bps, h_sum; - char h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL DECLARE grd_cur CURSOR FOR - SELECT m.mdr_bps, coalesce(sum(p.amount),0) FROM merchant m - JOIN purchase p ON p.merchant_id = m.merchant_id AND p.biz_date = :h_bizdate - GROUP BY m.merchant_id, m.mdr_bps; - EXEC SQL OPEN grd_cur; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH grd_cur INTO :h_bps, :h_sum; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE grd_cur; FAIL(b); } - g[ac_grade(h_bps)] += h_sum; + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); } - EXEC SQL CLOSE grd_cur; + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } - setl(b, T_ARG1, g[0]); - setl(b, T_ARG2, g[1]); - setl(b, T_ARG3, g[2]); - setl(b, T_GROSS, g[0] + g[1] + g[2]); + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT coalesce(sum(p.amount) FILTER (WHERE m.mdr_bps <= 20),0), coalesce(sum(p.amount) FILTER (WHERE m.mdr_bps BETWEEN 21 AND 28),0), coalesce(sum(p.amount) FILTER (WHERE m.mdr_bps > 28),0) INTO :h_a, :h_b, :h_c FROM purchase p JOIN merchant m ON m.merchant_id = p.merchant_id WHERE p.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 등급별매출 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), count(DISTINCT merchant_id) INTO :h_cnt, :h_gross, :h_n FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 총계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 최대 등급군을 골라 포트폴리오 성격을 판정 */ + h_max = h_a; h_top = 1; + if (h_b > h_max) { h_max = h_b; h_top = 2; } + if (h_c > h_max) { h_max = h_c; h_top = 3; } + if (h_gross > 0) ratio = (h_a * 10000L) / h_gross; else ratio = 0; + if (h_gross == 0) { h_val = 0; } + else if (ratio >= 5000) { h_val = 3; } + else if (h_c > h_a) { h_val = 2; } + else { h_val = 1; } + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "등급통계 bizdate=%s 프리미엄=%ld 일반=%ld 기본=%ld", h_bizdate, h_a, h_b, h_c); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_GRADESTAT', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_GRADESTAT' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_AMT1, h_a); + setl(b, T_AMT2, h_b); + setl(b, T_AMT3, h_c); + setl(b, T_ARG1, h_top); + setl(b, T_ARG2, ratio); + setl(b, T_ARG3, h_val); + setl(b, T_COUNT, h_cnt); + setl(b, T_GROSS, h_gross); + setl(b, T_ID1, h_n); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_GUARANTEE.pgc b/app/src/ac/svc/ACQ_GUARANTEE.pgc index 86dca04..aad1bda 100644 --- a/app/src/ac/svc/ACQ_GUARANTEE.pgc +++ b/app/src/ac/svc/ACQ_GUARANTEE.pgc @@ -1,7 +1,7 @@ /* * ACQ_GUARANTEE.pgc - ac 모듈 서비스 ACQ_GUARANTEE (one service = one file). * 매입 보증 등록: 대형 매입에 대한 이행보증금을 원장에 GUARANTEE 항목으로 - * 적립한다. 보증률(T_ARG1, bps)로 보증금을 산출하며 기본 10%. + * 적립한다. 보증률(T_ARG1, bps)로 보증금을 산출하며 기본 10% 를 적용한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_GUARANTEE.h" @@ -9,26 +9,153 @@ void ACQ_GUARANTEE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_GUARANTEE"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_amt, h_bps, h_lid, h_guar; - char h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 원장 예치/보증/연계 요청", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ h_pid = getl(b, T_PURCHASE_ID); - h_amt = getl(b, T_AMOUNT); - h_bps = getl(b, T_ARG1); if (h_bps <= 0) h_bps = 1000; - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - h_guar = h_amt * h_bps / 10000; - if (h_guar <= 0) { setl(b, T_RC, 3); FAIL(b); } + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + threshold = getl(b, T_ARG1); + if (threshold < 0 || threshold > 10000) { Bchg(b, T_MSG, 0, "보증률 범위 오류", 0L); setl(b, T_RC, 198); FAIL(b); } + if (threshold == 0) threshold = 1000; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - EXEC SQL SELECT nextval('ledger_seq') INTO :h_lid; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) - VALUES (:h_lid, :h_pid, 'GUARANTEE', :h_guar, :h_bizdate); - if (sqlca.sqlcode < 0) { userlog("ACQ_GUARANTEE FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; - setl(b, T_AMT1, h_guar); - setl(b, T_SETTLE_ID, h_lid); + /* 3) 업무규칙 분기부: 금액/유형/잔액 판정(3+ 분기) */ + /* 보증률로 보증금을 산출하고 기존 보증 누계를 확인 */ + h_amt = h_gross * threshold / 10000L; + if (h_amt <= 0) { Bchg(b, T_MSG, 0, "보증금 산출 불가", 0L); setl(b, T_RC, 199); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0), count(*) INTO :h_sum, :h_cnt + FROM ledger WHERE purchase_id = :h_pid AND entry_type = 'GUARANTEE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 기보증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_cnt > 0) { Bchg(b, T_MSG, 0, "이미 보증 등록됨", 0L); setl(b, T_RC, 200); FAIL(b); } + if (threshold >= 5000) { h_val = 3; } + else if (threshold >= 2000) { h_val = 2; } + else { h_val = 1; } + + /* 4) 본처리부: 원장 분개 기록 */ + EXEC SQL SELECT nextval('ledger_seq') INTO :h_ledid; + if (sqlca.sqlcode < 0) { + userlog("%s: 보증채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) VALUES (:h_ledid, :h_pid, 'GUARANTEE', :h_amt, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 보증분개 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_aff = 1; + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_ctx FROM ledger WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 원장재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "보증등록 pid=%ld 보증률=%ld 보증금=%ld 매입=%ld", h_pid, threshold, h_amt, h_gross); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_GUARANTEE', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_GUARANTEE' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_ID1, h_ledid); + setl(b, T_AMOUNT, h_amt); + setl(b, T_ARG2, threshold); + setl(b, T_GROSS, h_gross); + setl(b, T_ARG1, h_val); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_GUARQRY.pgc b/app/src/ac/svc/ACQ_GUARQRY.pgc index 297e6cc..7a0272f 100644 --- a/app/src/ac/svc/ACQ_GUARQRY.pgc +++ b/app/src/ac/svc/ACQ_GUARQRY.pgc @@ -1,7 +1,7 @@ /* * ACQ_GUARQRY.pgc - ac 모듈 서비스 ACQ_GUARQRY (one service = one file). - * 매입 보증 조회: 한 매입에 적립된 보증금(GUARANTEE) 총액과 건수를 원장에서 - * 조회해 응답한다. 보증 이력이 없으면 결과코드 1. + * 매입 보증 조회: 한 매입에 적립된 보증금(GUARANTEE) 총액과 건수를 + * 원장에서 조회하고 매입금액 대비 보증 커버율을 계산한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_GUARQRY.h" @@ -9,18 +9,148 @@ void ACQ_GUARQRY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_GUARQRY"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_sum = 0, h_n = 0; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } - EXEC SQL SELECT coalesce(sum(amount),0), count(*) INTO :h_sum, :h_n - FROM ledger WHERE purchase_id = :h_pid AND entry_type = 'GUARANTEE'; - if (sqlca.sqlcode < 0) FAIL(b); + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - setl(b, T_AMT1, h_sum); - setl(b, T_COUNT, h_n); - setl(b, T_RC, h_n > 0 ? 0 : 1); + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(max(ledger_id),0) INTO :h_cnt, :h_gross, :h_top FROM ledger WHERE purchase_id = :h_pid AND entry_type = 'GUARANTEE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 보증집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT amount, status INTO :h_amt, :h_status FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 원매입 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 매입금액 대비 보증 커버율로 담보 충분성을 판정 */ + if (h_amt > 0) ratio = (h_gross * 10000L) / h_amt; else ratio = 0; + if (h_cnt == 0) { h_val = 1; Bchg(b, T_MSG, 0, "보증 이력 없음", 0L); } + else if (ratio >= 1000) { h_val = 0; Bchg(b, T_MSG, 0, "보증 충분", 0L); } + else if (ratio >= 500) { h_val = 2; Bchg(b, T_MSG, 0, "보증 보통", 0L); } + else { h_val = 3; Bchg(b, T_MSG, 0, "보증 부족", 0L); } + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "보증조회 pid=%ld 건수=%ld 보증금=%ld 커버=%ld", h_pid, h_cnt, h_gross, ratio); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_GUARQRY', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_GUARQRY' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_GROSS, h_gross); + setl(b, T_AMOUNT, h_amt); + setl(b, T_ARG1, ratio); + setl(b, T_ARG2, h_val); + setl(b, T_ID1, h_top); + Bchg(b, T_STATUS, 0, h_status, 0L); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_HALFSTAT.pgc b/app/src/ac/svc/ACQ_HALFSTAT.pgc index dbbd9bf..043f277 100644 --- a/app/src/ac/svc/ACQ_HALFSTAT.pgc +++ b/app/src/ac/svc/ACQ_HALFSTAT.pgc @@ -9,24 +9,154 @@ void ACQ_HALFSTAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long share = 0, total; + const char *SVC = "ACQ_HALFSTAT"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_h1 = 0, h_h2 = 0, h_n = 0; - int h_yr; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - h_yr = (int)getl(b, T_ARG1); if (h_yr < 2000) h_yr = 2026; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL SELECT coalesce(sum(amount) FILTER (WHERE extract(month from biz_date) <= 6),0), - coalesce(sum(amount) FILTER (WHERE extract(month from biz_date) > 6),0), count(*) - INTO :h_h1, :h_h2, :h_n FROM purchase WHERE extract(year from biz_date) = :h_yr; - if (sqlca.sqlcode < 0) FAIL(b); - total = h_h1 + h_h2; - if (total > 0) share = h_h1 * 100 / total; + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); - setl(b, T_ARG2, h_h1); - setl(b, T_ARG3, h_h2); - setl(b, T_COUNT, h_n); - setl(b, T_RC, share); + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + k = getl(b, T_ARG1); if (k < 2000 || k > 2999) k = 2026; + snprintf(logmsg, sizeof(logmsg), "%ld-01-01", k); + strncpy(h_d1, logmsg, sizeof(h_d1)-1); h_d1[sizeof(h_d1)-1] = 0; + snprintf(logmsg, sizeof(logmsg), "%ld-12-31", k); + strncpy(h_d2, logmsg, sizeof(h_d2)-1); h_d2[sizeof(h_d2)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } + + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT coalesce(sum(amount) FILTER (WHERE extract(month from biz_date) <= 6),0), coalesce(sum(amount) FILTER (WHERE extract(month from biz_date) > 6),0), count(*) FILTER (WHERE extract(month from biz_date) <= 6), count(*) FILTER (WHERE extract(month from biz_date) > 6) INTO :h_a, :h_b, :h_c, :h_d FROM purchase WHERE biz_date BETWEEN :h_d1::date AND :h_d2::date; + if (sqlca.sqlcode < 0) { + userlog("%s: 반기분해 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_gross FROM purchase WHERE biz_date BETWEEN :h_d1::date AND :h_d2::date; + if (sqlca.sqlcode < 0) { + userlog("%s: 연간계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 상반기 비중으로 연간 실적 쏠림을 판정 */ + if (h_gross > 0) ratio = (h_a * 10000L) / h_gross; else ratio = 0; + if (h_gross == 0) { h_val = 0; } + else if (ratio >= 7000) { h_val = 3; } + else if (ratio <= 3000) { h_val = 2; } + else { h_val = 1; } + h_e = h_a - h_b; + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "반기통계 연도=%ld 상반기=%ld 하반기=%ld 비중=%ld", k, h_a, h_b, ratio); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_HALFSTAT', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_HALFSTAT' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_AMT1, h_a); + setl(b, T_AMT2, h_b); + setl(b, T_ARG1, h_c); + setl(b, T_ARG2, h_d); + setl(b, T_ARG3, ratio); + setl(b, T_ARG4, h_val); + setl(b, T_AMT3, h_e); + setl(b, T_COUNT, h_cnt); + setl(b, T_GROSS, h_gross); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_HOLD.pgc b/app/src/ac/svc/ACQ_HOLD.pgc index dc607e4..c930fc7 100644 --- a/app/src/ac/svc/ACQ_HOLD.pgc +++ b/app/src/ac/svc/ACQ_HOLD.pgc @@ -1,6 +1,9 @@ /* * ACQ_HOLD.pgc - ac 모듈 서비스 ACQ_HOLD (one service = one file). - * 매입 보류: 접수/대사(A,M) 상태만 보류로 전이, 반영 건수를 T_RC 로. + * 매입 보류: 접수/대사(A,M) 상태만 보류로 전이한다. 보류 사유를 미매입 원장에 + * 남기고 가맹점 보류 적체 현황을 함께 회신한다. + * 입출력 계약 보존: 입력 T_PURCHASE_ID, 출력 T_STATUS='H' 및 + * T_RC (반영 0 / 미반영 1, 미반영도 정상 응답). * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_HOLD.h" @@ -8,17 +11,130 @@ void ACQ_HOLD(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_HOLD"; + char reason[128], logmsg[256]; + long grade = 0; EXEC SQL BEGIN DECLARE SECTION; long h_pid, h_aff; + long h_amt = 0, h_cnt = 0, h_sum = 0, h_id = 0, h_logid = 0, h_ctx = 0, h_unm = 0; + char h_prev[8], h_merch[64], h_bizdate[16], h_reason[128], h_logmsg[256]; EXEC SQL END DECLARE SECTION; + userlog("%s 진입: 매입 보류 요청", SVC); + + /* 1) 입력 검증부 */ h_pid = getl(b, T_PURCHASE_ID); - /* only an approved(A)/matched(M) purchase may be put on hold */ + if (h_pid <= 0) { + Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); + } + gets_(b, T_REASON, reason, sizeof(reason)); + if (reason[0] == 0) strcpy(reason, "HOLD"); + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + + /* 2) 마스터/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount INTO :h_prev, :h_merch, :h_amt + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 조회 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { + Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); + } + userlog("%s 컨텍스트: pid=%ld merch=%s 상태=%s 금액=%ld", SVC, h_pid, h_merch, h_prev, h_amt); + + /* 3) 업무규칙 분기부: 금액/상태 구간별 보류 등급 (3+ 분기) */ + if (h_amt >= 10000000L) { + grade = 3; Bchg(b, T_MSG, 0, "고액 매입 보류", 0L); + } else if (strcmp(h_prev, "M") == 0) { + grade = 2; Bchg(b, T_MSG, 0, "대사완료건 보류", 0L); + } else if (strcmp(h_prev, "A") == 0) { + grade = 1; Bchg(b, T_MSG, 0, "접수건 보류", 0L); + } else { + grade = 0; Bchg(b, T_MSG, 0, "보류 불가 상태", 0L); + } + + /* 4) 본처리부: only an approved(A)/matched(M) purchase may be put on hold */ EXEC SQL UPDATE purchase SET status = 'H', updated_at = now() WHERE purchase_id = :h_pid AND status IN ('A', 'M'); - if (sqlca.sqlcode < 0) { userlog("ACQ_HOLD FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + if (sqlca.sqlcode < 0) { + userlog("ACQ_HOLD FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } h_aff = sqlca.sqlerrd[2]; + + /* 반영된 경우에만 보류 사유를 미매입 원장에 남긴다 */ + if (h_aff > 0) { + EXEC SQL SELECT nextval('ac_unmatch_seq') INTO :h_id; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); } + EXEC SQL INSERT INTO ac_unmatch (unmatch_id, purchase_id, reason, biz_date) + VALUES (:h_id, :h_pid, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 보류사유 적재 실패 [%d]", SVC, sqlca.sqlcode); + setl(b, T_RC, 90); FAIL(b); + } + } + + /* 6) 집계/요약 갱신부: 가맹점 보류 적체 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM purchase WHERE merchant_id = :h_merch AND status = 'H'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); } + + /* 누적 보류 사유 건수 확인 */ + EXEC SQL SELECT count(*) INTO :h_unm FROM ac_unmatch WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); } + + /* 7) 예외/보상부 */ + if (h_aff == 0) + userlog("%s 미반영: pid=%ld 상태=%s (보류 대상 아님)", SVC, h_pid, h_prev); + if (h_cnt >= 10) + userlog("%s 경보: 가맹점 %s 보류 적체 %ld 건", SVC, h_merch, h_cnt); + if (h_unm >= 5) + userlog("%s 경보: pid=%ld 보류 사유 %ld 건 누적", SVC, h_pid, h_unm); + if (h_sum > 0 && h_amt > 0 && h_sum / h_amt >= 20) + userlog("%s 경보: 보류 총액이 단건의 20배 초과 merch=%s", SVC, h_merch); + + /* 5) 이력/감사 적재부 */ + snprintf(logmsg, sizeof(logmsg), "매입보류 pid=%ld 직전=%s 반영=%ld 적체=%ld 등급=%ld", + h_pid, h_prev, h_aff, h_cnt, grade); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_HOLD', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_HOLD' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); } + if (h_ctx <= 0) { + userlog("%s 경보: 보류 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 보류 처리 %ld 회로 과다", SVC, h_ctx); + + /* 8) 응답부 (계약 보존: 반영 시 T_STATUS='H', T_RC 는 0/1) */ if (h_aff > 0) Bchg(b, T_STATUS, 0, "H", 0L); + else Bchg(b, T_STATUS, 0, h_prev, 0L); + Bchg(b, T_MERCHANT, 0, h_merch, 0L); + setl(b, T_AMOUNT, h_amt); + setl(b, T_COUNT, h_cnt); + setl(b, T_GROSS, h_sum); + setl(b, T_ARG1, grade); + setl(b, T_ARG2, h_aff); + setl(b, T_ARG3, h_unm); + setl(b, T_ARG4, h_ctx); + setl(b, T_ID1, h_id); setl(b, T_RC, h_aff > 0 ? 0 : 1); + userlog("%s 완료: pid=%ld 반영=%ld 적체=%ld", SVC, h_pid, h_aff, h_cnt); OK(b); } diff --git a/app/src/ac/svc/ACQ_HOLDQRY.pgc b/app/src/ac/svc/ACQ_HOLDQRY.pgc index 9f31e19..d136435 100644 --- a/app/src/ac/svc/ACQ_HOLDQRY.pgc +++ b/app/src/ac/svc/ACQ_HOLDQRY.pgc @@ -1,7 +1,7 @@ /* * ACQ_HOLDQRY.pgc - ac 모듈 서비스 ACQ_HOLDQRY (one service = one file). - * 보류 매입 조회: 가맹점의 보류(H) 건을 최근 갱신순 커서로 훑어 건수·총액을 - * 집계하고 상위 5건의 매입 ID/금액을 occurrence 로 내려준다. + * 보류 매입 조회: 가맹점의 보류(H) 건을 집계해 건수·총액과 최고 보류액, + * 가장 오래된 보류 매입 ID를 응답한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_HOLDQRY.h" @@ -9,35 +9,162 @@ void ACQ_HOLDQRY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long cnt = 0, total = 0, top = 0; + const char *SVC = "ACQ_HOLDQRY"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_amt; - char h_merch[64]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL DECLARE hlq_cur CURSOR FOR - SELECT purchase_id, amount FROM purchase - WHERE merchant_id = :h_merch AND status = 'H' - ORDER BY updated_at DESC; - EXEC SQL OPEN hlq_cur; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH hlq_cur INTO :h_pid, :h_amt; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE hlq_cur; FAIL(b); } - cnt++; total += h_amt; - if (top < 5) { - Badd(b, T_ID2, (char *)&h_pid, 0L); - Badd(b, T_AMT4, (char *)&h_amt, 0L); - top++; - } + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + if (merch[0] == 0) { Bchg(b, T_MSG, 0, "가맹점 ID 누락", 0L); setl(b, T_RC, 101); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); } - EXEC SQL CLOSE hlq_cur; + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } - setl(b, T_COUNT, cnt); - setl(b, T_GROSS, total); - setl(b, T_RC, cnt > 0 ? 1 : 0); /* 1=보류 존재 */ + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + EXEC SQL SELECT name, status, daily_limit INTO :h_str1, :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (strcmp(h_status, "ACTIVE") != 0) + userlog("%s 주의: 가맹점 %s 상태=%s", SVC, h_merch, h_status); + /* 가맹점 MDR(bps) 및 상태분포 컨텍스트 로드 */ + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + EXEC SQL SELECT count(*) FILTER (WHERE status IN ('A','M')), count(*) FILTER (WHERE status = 'S') INTO :h_openc, :h_setc FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 상태분포 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산 진행 컨텍스트(기 정산 순액) */ + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_ctx FROM settlement WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(max(amount),0), coalesce(min(purchase_id),0) INTO :h_cnt, :h_gross, :h_max, :h_top FROM purchase WHERE merchant_id = :h_merch AND status = 'H'; + if (sqlca.sqlcode < 0) { + userlog("%s: 보류집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_a FROM purchase WHERE merchant_id = :h_merch AND status = 'H' AND biz_date < :h_bizdate::date; + if (sqlca.sqlcode < 0) { + userlog("%s: 이월보류 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 이월 보류 비중으로 적체 심각도를 판정 */ + if (h_cnt > 0) ratio = (h_a * 10000L) / h_cnt; else ratio = 0; + if (h_cnt == 0) { h_val = 0; Bchg(b, T_MSG, 0, "보류 없음", 0L); } + else if (ratio >= 5000) { h_val = 3; Bchg(b, T_MSG, 0, "이월 보류 적체", 0L); } + else if (h_cnt >= 5) { h_val = 2; Bchg(b, T_MSG, 0, "보류 다수", 0L); } + else { h_val = 1; Bchg(b, T_MSG, 0, "보류 소수", 0L); } + h_avg = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :h_val2 FROM merchant_summary WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 누적집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_pdgross FROM purchase WHERE merchant_id = :h_merch AND biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_val2 > 0 && h_gross > 0) ratio = (h_gross * 10000L) / (h_val2 + h_gross); + else ratio = 0; + h_share = (h_pdgross > 0) ? (h_gross * 100L / h_pdgross) : 0; + userlog("%s 추이: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_pdgross, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "보류조회 merch=%s 건수=%ld 총액=%ld 이월=%ld", h_merch, h_cnt, h_gross, h_a); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_HOLDQRY', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_HOLDQRY' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_GROSS, h_gross); + setl(b, T_AMOUNT, h_max); + setl(b, T_PURCHASE_ID, h_top); + setl(b, T_ARG1, h_a); + setl(b, T_ARG2, ratio); + setl(b, T_ARG3, h_val); + setl(b, T_AMT1, h_avg); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_HOURSTAT.pgc b/app/src/ac/svc/ACQ_HOURSTAT.pgc index b0e7669..71704d7 100644 --- a/app/src/ac/svc/ACQ_HOURSTAT.pgc +++ b/app/src/ac/svc/ACQ_HOURSTAT.pgc @@ -1,7 +1,7 @@ /* * ACQ_HOURSTAT.pgc - ac 모듈 서비스 ACQ_HOURSTAT (one service = one file). - * 매입 시간대별 통계: 접수시각을 새벽/오전/오후/야간 네 구간으로 나눠 구간별 - * 매입 건수를 한 번의 집계로 분해하고 피크 구간 인덱스를 판정한다. + * 매입 시간대별 통계: 접수시각을 새벽/오전/오후/야간 네 구간으로 나눠 + * 구간별 건수를 분해하고 피크 구간 인덱스를 판정한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_HOURSTAT.h" @@ -9,27 +9,151 @@ void ACQ_HOURSTAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long v[4]; int i, peak = 0; + const char *SVC = "ACQ_HOURSTAT"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_d = 0, h_m = 0, h_a = 0, h_n = 0; - char h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL SELECT count(*) FILTER (WHERE extract(hour from created_at) < 6), - count(*) FILTER (WHERE extract(hour from created_at) BETWEEN 6 AND 11), - count(*) FILTER (WHERE extract(hour from created_at) BETWEEN 12 AND 17), - count(*) FILTER (WHERE extract(hour from created_at) >= 18) - INTO :h_d, :h_m, :h_a, :h_n FROM purchase WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); - v[0] = h_d; v[1] = h_m; v[2] = h_a; v[3] = h_n; - for (i = 1; i < 4; i++) if (v[i] > v[peak]) peak = i; - setl(b, T_ARG1, h_d); - setl(b, T_ARG2, h_m); - setl(b, T_ARG3, h_a); - setl(b, T_ARG4, h_n); - setl(b, T_RC, peak); + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } + + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*) FILTER (WHERE extract(hour from created_at) < 6), count(*) FILTER (WHERE extract(hour from created_at) BETWEEN 6 AND 11), count(*) FILTER (WHERE extract(hour from created_at) BETWEEN 12 AND 17), count(*) FILTER (WHERE extract(hour from created_at) >= 18) INTO :h_a, :h_b, :h_c, :h_d FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 시간대분해 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_gross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 총계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 네 구간 중 피크 구간을 고르고 집중도를 계산 */ + h_max = h_a; h_top = 1; + if (h_b > h_max) { h_max = h_b; h_top = 2; } + if (h_c > h_max) { h_max = h_c; h_top = 3; } + if (h_d > h_max) { h_max = h_d; h_top = 4; } + if (h_cnt > 0) ratio = (h_max * 10000L) / h_cnt; else ratio = 0; + if (h_a > 0 && h_top == 1) { h_val = 3; } + else if (ratio >= 6000) { h_val = 2; } + else { h_val = 1; } + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "시간대통계 bizdate=%s 새벽=%ld 오전=%ld 오후=%ld 야간=%ld", h_bizdate, h_a, h_b, h_c, h_d); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_HOURSTAT', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_HOURSTAT' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_ARG1, h_a); + setl(b, T_ARG2, h_b); + setl(b, T_ARG3, h_c); + setl(b, T_ARG4, h_d); + setl(b, T_ARG5, h_top); + setl(b, T_ARG6, ratio); + setl(b, T_COUNT, h_cnt); + setl(b, T_GROSS, h_gross); + setl(b, T_ARG7, h_val); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_INSTALL.pgc b/app/src/ac/svc/ACQ_INSTALL.pgc index c427152..9fd5f1a 100644 --- a/app/src/ac/svc/ACQ_INSTALL.pgc +++ b/app/src/ac/svc/ACQ_INSTALL.pgc @@ -1,34 +1,171 @@ /* * ACQ_INSTALL.pgc - ac 모듈 서비스 ACQ_INSTALL (one service = one file). - * Moved verbatim from the former inline ac_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by ac_svr at tpsvrinit. + * 할부 등록: 요청 개월수로 매입금액을 균등 분할해 ac_install 에 회차별 + * 스케줄을 적재한다. 잔액은 마지막 회차에 가산해 원금 정합을 맞춘다. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_INSTALL.h" void ACQ_INSTALL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long months, amount, i; + const char *SVC = "ACQ_INSTALL"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_id, h_pid, h_seq, h_months, h_mamt; - char h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 할부/분할 스케줄 요청", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ h_pid = getl(b, T_PURCHASE_ID); - months = getl(b, T_INSTALL_N); if (months < 1) months = 3; - amount = getl(b, T_AMOUNT); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + h_months = getl(b, T_INSTALL_N); + if (h_months < 2 || h_months > 36) { Bchg(b, T_MSG, 0, "할부 개월 범위 오류(2~36)", 0L); setl(b, T_RC, 189); FAIL(b); } 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); + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); } - setl(b, T_COUNT, months); + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + + /* 3) 업무규칙 분기부: 개월/회차/금액 구간 판정(3+ 분기) */ + /* 기존 스케줄 중복을 막고 개월 구간별 등급을 정한다 */ + EXEC SQL SELECT count(*) INTO :h_cnt FROM ac_install WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 기존스케줄 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_cnt > 0) { Bchg(b, T_MSG, 0, "이미 할부 등록됨", 0L); setl(b, T_RC, 190); FAIL(b); } + if (h_months >= 12) { h_val = 3; } + else if (h_months >= 6) { h_val = 2; } + else { h_val = 1; } + h_mamt = h_gross / h_months; + h_rem = h_gross - (h_mamt * h_months); + + /* 4) 본처리부: 회차별 스케줄 원장 루프 적재 */ + for (i = 1; i <= h_months; i++) { + h_seq = i; + /* 마지막 회차에 단수 잔액을 가산해 원금과 일치시킨다 */ + h_amt = (i == h_months) ? (h_mamt + h_rem) : h_mamt; + EXEC SQL SELECT nextval('ac_install_seq') INTO :h_id; + if (sqlca.sqlcode < 0) { + userlog("%s: 회차채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); 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_amt, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 회차적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + } + h_aff = h_months; + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "할부등록 pid=%ld 개월=%ld 회차금액=%ld 단수=%ld", h_pid, h_months, h_mamt, h_rem); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_INSTALL', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_INSTALL' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_COUNT, h_months); + setl(b, T_INSTALL_N, h_months); + setl(b, T_AMT1, h_mamt); + setl(b, T_ARG1, h_rem); + setl(b, T_ARG2, h_val); + setl(b, T_GROSS, h_gross); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_INSTCXL.pgc b/app/src/ac/svc/ACQ_INSTCXL.pgc index 16f1b0c..ac1dbb8 100644 --- a/app/src/ac/svc/ACQ_INSTCXL.pgc +++ b/app/src/ac/svc/ACQ_INSTCXL.pgc @@ -1,7 +1,7 @@ /* * ACQ_INSTCXL.pgc - ac 모듈 서비스 ACQ_INSTCXL (one service = one file). - * 할부 취소: 한 매입의 할부 스케줄 전 회차를 원장에서 삭제해 할부를 해지한다. - * 삭제된 회차수를 건수로 응답하며, 없으면 결과코드 1. + * 할부 취소: 한 매입의 할부 스케줄 전 회차를 원장에서 삭제해 할부를 + * 해지한다. 삭제된 회차수를 응답하며 해지 분개를 남긴다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_INSTCXL.h" @@ -9,16 +9,164 @@ void ACQ_INSTCXL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_INSTCXL"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_del; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + userlog("%s 진입: 매입 상태 전이 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "일반"); + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + + /* 3) 업무규칙 분기부: 상태 전이 가능여부/유형 판정(3+ 분기) */ + if (!(strcmp(h_prev, "A") == 0 || strcmp(h_prev, "M") == 0 || strcmp(h_prev, "S") == 0 || strcmp(h_prev, "C") == 0 || strcmp(h_prev, "H") == 0 || strcmp(h_prev, "V") == 0 || strcmp(h_prev, "P") == 0)) { + userlog("%s 거절: 상태 %s 에서 전이 불가", SVC, h_prev); + Bchg(b, T_MSG, 0, "상태 전이 불가", 0L); setl(b, T_RC, 120); FAIL(b); + } + /* 해지 대상 회차와 잔여 금액을 먼저 확보 */ + EXEC SQL SELECT count(*), coalesce(sum(month_amount),0), coalesce(max(months),0) + INTO :h_cnt, :h_sum, :h_months FROM ac_install WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 할부조회 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_cnt == 0) { Bchg(b, T_MSG, 0, "할부 스케줄 없음", 0L); setl(b, T_RC, 139); FAIL(b); } + if (h_months >= 12) { h_val = 3; } + else if (h_months >= 6) { h_val = 2; } + else { h_val = 1; } + + /* 4) 본처리부: 부수행 기록 및 상태 갱신 */ EXEC SQL DELETE FROM ac_install WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0) { userlog("ACQ_INSTCXL FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } - h_del = sqlca.sqlerrd[2]; - setl(b, T_COUNT, h_del); - setl(b, T_RC, h_del > 0 ? 0 : 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 할부삭제 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_rem = sqlca.sqlerrd[2]; + EXEC SQL SELECT nextval('ledger_seq') INTO :h_ledid; + if (sqlca.sqlcode < 0) { + userlog("%s: 원장채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) VALUES (:h_ledid, :h_pid, 'INSTCXL', :h_sum, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 해지분개 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_aff = 1; + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "할부취소 pid=%ld 회차=%ld 삭제=%ld 개월=%ld", h_pid, h_cnt, h_rem, h_months); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_INSTCXL', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_INSTCXL' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_COUNT, h_rem); + setl(b, T_INSTALL_N, h_months); + setl(b, T_GROSS, h_sum); + setl(b, T_ARG1, h_val); + setl(b, T_ID1, h_ledid); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_INSTPLAN.pgc b/app/src/ac/svc/ACQ_INSTPLAN.pgc index 6c97b3c..7ba6fce 100644 --- a/app/src/ac/svc/ACQ_INSTPLAN.pgc +++ b/app/src/ac/svc/ACQ_INSTPLAN.pgc @@ -1,7 +1,7 @@ /* * ACQ_INSTPLAN.pgc - ac 모듈 서비스 ACQ_INSTPLAN (one service = one file). - * 할부 계획 조회: 한 매입의 약정 개월수와 회차 합계, 회차당 금액을 단건 - * 조회해 계획 개요를 응답한다. 할부가 없으면 결과코드 1. + * 할부 계획 조회: 한 매입의 약정 개월수와 회차 합계, 회차당 금액을 조회해 + * 계획 개요와 원금 대비 스케줄 정합을 응답한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_INSTPLAN.h" @@ -9,20 +9,150 @@ void ACQ_INSTPLAN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_INSTPLAN"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_months = 0, h_sum = 0, h_first = 0; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } - EXEC SQL SELECT coalesce(max(months),0), coalesce(sum(month_amount),0), - coalesce(min(month_amount),0) - INTO :h_months, :h_sum, :h_first FROM ac_install WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*), coalesce(max(months),0), coalesce(sum(month_amount),0), coalesce(max(month_amount),0) INTO :h_cnt, :h_months, :h_sum, :h_max FROM ac_install WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 할부계획 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT amount, status INTO :h_amt, :h_status FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 원매입 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 스케줄 합계와 원금 차이로 계획 정합을 판정 */ + h_e = h_amt - h_sum; + if (h_cnt == 0) { h_val = 1; Bchg(b, T_MSG, 0, "할부 없음", 0L); } + else if (h_e == 0) { h_val = 0; Bchg(b, T_MSG, 0, "계획 정합", 0L); } + else if (h_e > 0) { h_val = 2; Bchg(b, T_MSG, 0, "스케줄 부족", 0L); } + else { h_val = 3; Bchg(b, T_MSG, 0, "스케줄 초과", 0L); } + h_avg = (h_cnt > 0) ? (h_sum / h_cnt) : 0; + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "할부계획 pid=%ld 회차=%ld 개월=%ld 합계=%ld 차이=%ld", h_pid, h_cnt, h_months, h_sum, h_e); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_INSTPLAN', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_INSTPLAN' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_COUNT, h_cnt); setl(b, T_INSTALL_N, h_months); setl(b, T_GROSS, h_sum); - setl(b, T_AMT1, h_first); - setl(b, T_RC, h_months > 0 ? 0 : 1); + setl(b, T_AMOUNT, h_amt); + setl(b, T_ARG1, h_avg); + setl(b, T_ARG2, h_e); + setl(b, T_ARG3, h_val); + setl(b, T_AMT1, h_max); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_INSTPREPAY.pgc b/app/src/ac/svc/ACQ_INSTPREPAY.pgc index fc0be1c..fbb1c97 100644 --- a/app/src/ac/svc/ACQ_INSTPREPAY.pgc +++ b/app/src/ac/svc/ACQ_INSTPREPAY.pgc @@ -9,28 +9,163 @@ void ACQ_INSTPREPAY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_INSTPREPAY"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_from, h_left = 0, h_cnt = 0, h_lid; - char h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 할부/분할 스케줄 요청", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ h_pid = getl(b, T_PURCHASE_ID); - h_from = getl(b, T_ARG1); - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + h_seq = getl(b, T_ARG1); + if (h_seq < 1) { Bchg(b, T_MSG, 0, "기준 회차 오류", 0L); setl(b, T_RC, 194); FAIL(b); } + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - EXEC SQL SELECT coalesce(sum(month_amount),0), count(*) INTO :h_left, :h_cnt - FROM ac_install WHERE purchase_id = :h_pid AND seq_no > :h_from; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_cnt == 0) { setl(b, T_RC, 1); OK(b); } + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; - EXEC SQL SELECT nextval('ledger_seq') INTO :h_lid; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) - VALUES (:h_lid, :h_pid, 'PREPAY', :h_left, :h_bizdate); - if (sqlca.sqlcode < 0) { userlog("ACQ_INSTPREPAY FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + /* 3) 업무규칙 분기부: 개월/회차/금액 구간 판정(3+ 분기) */ + /* 기준 회차 이후 잔여 할부금을 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(month_amount),0), coalesce(max(months),0) + INTO :h_cnt, :h_sum, :h_months FROM ac_install + WHERE purchase_id = :h_pid AND seq_no > :h_seq; + if (sqlca.sqlcode < 0) { + userlog("%s: 잔여회차 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_cnt == 0) { Bchg(b, T_MSG, 0, "조기상환 잔여 회차 없음", 0L); setl(b, T_RC, 195); FAIL(b); } + if (h_cnt >= 6) { h_val = 3; Bchg(b, T_MSG, 0, "장기 잔여 조기상환", 0L); } + else if (h_cnt >= 3) { h_val = 2; Bchg(b, T_MSG, 0, "중기 잔여 조기상환", 0L); } + else { h_val = 1; Bchg(b, T_MSG, 0, "소액 잔여 조기상환", 0L); } - setl(b, T_GROSS, h_left); + /* 4) 본처리부: 회차별 스케줄 원장 루프 적재 */ + /* 선지급 분개를 남기고 잔여 회차를 정리한다 */ + EXEC SQL SELECT nextval('ledger_seq') INTO :h_ledid; + if (sqlca.sqlcode < 0) { + userlog("%s: 원장채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) VALUES (:h_ledid, :h_pid, 'PREPAY', :h_sum, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 선지급분개 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL DELETE FROM ac_install WHERE purchase_id = :h_pid AND seq_no > :h_seq; + if (sqlca.sqlcode < 0) { + userlog("%s: 잔여회차정리 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_rem = sqlca.sqlerrd[2]; + h_aff = h_rem; + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "할부조기상환 pid=%ld 기준회차=%ld 잔여=%ld 선지급=%ld", h_pid, h_seq, h_cnt, h_sum); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_INSTPREPAY', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_INSTPREPAY' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ setl(b, T_COUNT, h_cnt); + setl(b, T_GROSS, h_sum); + setl(b, T_ARG2, h_seq); + setl(b, T_ARG3, h_rem); + setl(b, T_ID1, h_ledid); + setl(b, T_ARG4, h_val); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_INSTQRY.pgc b/app/src/ac/svc/ACQ_INSTQRY.pgc index dbffaaa..8656d37 100644 --- a/app/src/ac/svc/ACQ_INSTQRY.pgc +++ b/app/src/ac/svc/ACQ_INSTQRY.pgc @@ -1,7 +1,7 @@ /* * ACQ_INSTQRY.pgc - ac 모듈 서비스 ACQ_INSTQRY (one service = one file). - * 할부 회차조회: 한 매입의 할부 스케줄(ac_install)을 회차 순으로 커서 조회해 - * 회차·회차금액 쌍을 occurrence 로 내리고 총 회차수와 합계를 계산한다. + * 할부 회차조회: 한 매입의 할부 스케줄을 집계해 총 회차수와 합계, + * 최소/최대 회차금액 및 최종 회차 번호를 계산한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_INSTQRY.h" @@ -9,30 +9,157 @@ void ACQ_INSTQRY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long n = 0, tot = 0; + const char *SVC = "ACQ_INSTQRY"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_seq, h_mamt; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ h_pid = getl(b, T_PURCHASE_ID); - - EXEC SQL DECLARE isq_cur CURSOR FOR - SELECT seq_no, month_amount FROM ac_install WHERE purchase_id = :h_pid - ORDER BY seq_no; - EXEC SQL OPEN isq_cur; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH isq_cur INTO :h_seq, :h_mamt; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE isq_cur; FAIL(b); } - Badd(b, T_ARG5, (char *)&h_seq, 0L); - Badd(b, T_AMT1, (char *)&h_mamt, 0L); - n++; tot += h_mamt; + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); } - EXEC SQL CLOSE isq_cur; - setl(b, T_INSTALL_N, n); - setl(b, T_GROSS, tot); - setl(b, T_RC, n > 0 ? 0 : 1); + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*), coalesce(sum(month_amount),0), coalesce(min(month_amount),0), coalesce(max(month_amount),0) INTO :h_cnt, :h_sum, :h_min, :h_max FROM ac_install WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 회차집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(max(seq_no),0), coalesce(min(seq_no),0) INTO :h_a, :h_b FROM ac_install WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 회차범위 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT amount INTO :h_amt FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 원금 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 회차금액 편차로 균등/불균등 분할을 구분 */ + h_e = h_max - h_min; + if (h_cnt == 0) { h_val = 1; } + else if (h_e == 0) { h_val = 0; } + else if (h_min > 0 && h_e * 100L / h_min > 10) { h_val = 3; } + else { h_val = 2; } + if (h_amt > 0) ratio = (h_sum * 10000L) / h_amt; else ratio = 0; + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "할부회차 pid=%ld 회차수=%ld 합계=%ld 편차=%ld", h_pid, h_cnt, h_sum, h_e); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_INSTQRY', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_INSTQRY' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_GROSS, h_sum); + setl(b, T_AMT1, h_min); + setl(b, T_AMT2, h_max); + setl(b, T_ARG1, h_a); + setl(b, T_ARG2, h_b); + setl(b, T_ARG3, h_e); + setl(b, T_ARG4, ratio); + setl(b, T_ARG5, h_val); + setl(b, T_AMOUNT, h_amt); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_ISSSTAT.pgc b/app/src/ac/svc/ACQ_ISSSTAT.pgc index 6f3ee90..178a192 100644 --- a/app/src/ac/svc/ACQ_ISSSTAT.pgc +++ b/app/src/ac/svc/ACQ_ISSSTAT.pgc @@ -1,7 +1,7 @@ /* * ACQ_ISSSTAT.pgc - ac 모듈 서비스 ACQ_ISSSTAT (one service = one file). - * 매입 발급사별 통계: 영업일 발급사 그룹 집계에서 1위 발급사(건수·총액)와 - * 전체 발급사 수, 1위의 점유율(bps)을 계산해 응답한다. + * 매입 발급사별 통계: 영업일 발급사 그룹에서 1위 발급사(건수·총액)와 + * 전체 발급사 수, 1위 점유율(bps)을 계산해 응답한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_ISSSTAT.h" @@ -9,36 +9,154 @@ void ACQ_ISSSTAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long share_bps = 0; + const char *SVC = "ACQ_ISSSTAT"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_topcnt = 0, h_topsum = 0, h_kinds = 0, h_total = 0; - char h_top[32], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - h_top[0] = 0; - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - /* 전체 발급사 수 + 총액 */ - EXEC SQL SELECT count(DISTINCT issuer), coalesce(sum(amount), 0) - INTO :h_kinds, :h_total - FROM purchase WHERE biz_date = :h_bizdate AND status <> 'C'; - if (sqlca.sqlcode < 0) FAIL(b); + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); - /* 1위 발급사 */ - EXEC SQL SELECT issuer, count(*), sum(amount) - INTO :h_top, :h_topcnt, :h_topsum - FROM purchase WHERE biz_date = :h_bizdate AND status <> 'C' - GROUP BY issuer ORDER BY sum(amount) DESC, issuer LIMIT 1; - if (sqlca.sqlcode < 0) { userlog("ACQ_ISSSTAT FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } - if (sqlca.sqlcode == 100) { setl(b, T_RC, 1); OK(b); } + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } - if (h_total > 0) share_bps = (h_topsum * 10000) / h_total; + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - Bchg(b, T_ISSUER, 0, h_top, 0L); - setl(b, T_COUNT, h_topcnt); - setl(b, T_GROSS, h_topsum); - setl(b, T_ARG1, h_kinds); - setl(b, T_ARG2, share_bps); /* 1위 점유율 bps */ + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(DISTINCT issuer), count(*), coalesce(sum(amount),0) INTO :h_n, :h_cnt, :h_gross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 발급사모집단 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(issuer,''), count(*), coalesce(sum(amount),0) INTO :h_issuer, :h_a, :h_b FROM purchase WHERE biz_date = :h_bizdate GROUP BY issuer ORDER BY sum(amount) DESC LIMIT 1; + if (sqlca.sqlcode < 0) { + userlog("%s: 1위발급사 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_c FROM purchase WHERE biz_date = :h_bizdate AND issuer = 'UNKNOWN'; + if (sqlca.sqlcode < 0) { + userlog("%s: 미상발급사 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 1위 점유율과 미상 비중으로 데이터 품질/집중도 판정 */ + if (h_gross > 0) ratio = (h_b * 10000L) / h_gross; else ratio = 0; + if (h_cnt > 0) h_share = (h_c * 10000L) / h_cnt; else h_share = 0; + if (h_cnt == 0) { h_val = 0; } + else if (h_share >= 3000) { h_val = 3; } + else if (ratio >= 6000) { h_val = 2; } + else { h_val = 1; } + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "발급사통계 bizdate=%s 발급사=%ld 1위=%s 점유=%ld 미상=%ld", h_bizdate, h_n, h_issuer, ratio, h_c); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_ISSSTAT', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_ISSSTAT' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_COUNT, h_n); + Bchg(b, T_ISSUER, 0, h_issuer, 0L); + setl(b, T_ARG1, h_a); + setl(b, T_AMT1, h_b); + setl(b, T_ARG2, ratio); + setl(b, T_ARG3, h_c); + setl(b, T_ARG4, h_share); + setl(b, T_ARG5, h_val); + setl(b, T_GROSS, h_gross); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_ISSUERCLS.pgc b/app/src/ac/svc/ACQ_ISSUERCLS.pgc index 5914c4b..98e687e 100644 --- a/app/src/ac/svc/ACQ_ISSUERCLS.pgc +++ b/app/src/ac/svc/ACQ_ISSUERCLS.pgc @@ -1,22 +1,165 @@ /* * ACQ_ISSUERCLS.pgc - ac 모듈 서비스 ACQ_ISSUERCLS (one service = one file). - * Moved verbatim from the former inline ac_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by ac_svr at tpsvrinit. + * 발급사 분류 지정: 매입의 발급사(issuer) 코드를 지정 값으로 갱신하고 + * 같은 발급사의 당일 매입 건수와 매출을 재집계해 회신한다. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_ISSUERCLS.h" void ACQ_ISSUERCLS(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char issuer[32]; + const char *SVC = "ACQ_ISSUERCLS"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid; - char h_issuer[32]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; + + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 매입 상태 전이 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } 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; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + + /* 3) 업무규칙 분기부: 상태 전이 가능여부/유형 판정(3+ 분기) */ + if (!(strcmp(h_prev, "A") == 0 || strcmp(h_prev, "M") == 0 || strcmp(h_prev, "S") == 0 || strcmp(h_prev, "C") == 0 || strcmp(h_prev, "H") == 0 || strcmp(h_prev, "V") == 0 || strcmp(h_prev, "P") == 0)) { + userlog("%s 거절: 상태 %s 에서 전이 불가", SVC, h_prev); + Bchg(b, T_MSG, 0, "상태 전이 불가", 0L); setl(b, T_RC, 120); FAIL(b); + } + /* 미상(UNKNOWN) 에서의 확정 여부로 등급을 나눈다 */ + EXEC SQL SELECT issuer INTO :h_str2 FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 기존발급사 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (strcmp(h_str2, "UNKNOWN") == 0) { h_val = 3; Bchg(b, T_MSG, 0, "미상 발급사 확정", 0L); } + else if (strcmp(h_str2, h_issuer) == 0) { h_val = 1; Bchg(b, T_MSG, 0, "동일 발급사", 0L); } + else { h_val = 2; Bchg(b, T_MSG, 0, "발급사 정정", 0L); } + + /* 4) 본처리부: 부수행 기록 및 상태 갱신 */ 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); + if (sqlca.sqlcode < 0) { + userlog("%s: 발급사갱신 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_rem = sqlca.sqlerrd[2]; + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum FROM purchase WHERE issuer = :h_issuer AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 발급사재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_aff = 1; + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "발급사지정 pid=%ld %s -> %s 동일=%ld", h_pid, h_str2, h_issuer, h_cnt); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_ISSUERCLS', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_ISSUERCLS' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + Bchg(b, T_ISSUER, 0, h_issuer, 0L); + setl(b, T_COUNT, h_cnt); + setl(b, T_GROSS, h_sum); + setl(b, T_ARG1, h_rem); + setl(b, T_ARG2, h_val); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_LIMITCHK.pgc b/app/src/ac/svc/ACQ_LIMITCHK.pgc index 730aadc..f1d1855 100644 --- a/app/src/ac/svc/ACQ_LIMITCHK.pgc +++ b/app/src/ac/svc/ACQ_LIMITCHK.pgc @@ -1,28 +1,170 @@ /* * ACQ_LIMITCHK.pgc - ac 모듈 서비스 ACQ_LIMITCHK (one service = one file). - * Moved verbatim from the former inline ac_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by ac_svr at tpsvrinit. + * 매입 한도 점검: 가맹점 일한도와 당일 누적 매입을 비교해 초과 여부를 + * 결과코드로 응답하고 잔여 한도를 함께 계산한다. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_LIMITCHK.h" void ACQ_LIMITCHK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; + const char *SVC = "ACQ_LIMITCHK"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_sum, h_lim; - char h_merch[64], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; + + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ gets_(b, T_MERCHANT, merch, sizeof(merch)); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (merch[0] == 0) { Bchg(b, T_MSG, 0, "가맹점 ID 누락", 0L); setl(b, T_RC, 101); FAIL(b); } strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_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); + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } + + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + EXEC SQL SELECT name, status, daily_limit INTO :h_str1, :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (strcmp(h_status, "ACTIVE") != 0) + userlog("%s 주의: 가맹점 %s 상태=%s", SVC, h_merch, h_status); + /* 가맹점 MDR(bps) 및 상태분포 컨텍스트 로드 */ + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + EXEC SQL SELECT count(*) FILTER (WHERE status IN ('A','M')), count(*) FILTER (WHERE status = 'S') INTO :h_openc, :h_setc FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 상태분포 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산 진행 컨텍스트(기 정산 순액) */ + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_ctx FROM settlement WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT coalesce(sum(amount),0), count(*) INTO :h_gross, :h_cnt FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 당일누적 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_a FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate AND status IN ('C','V'); + if (sqlca.sqlcode < 0) { + userlog("%s: 취소분 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 취소분을 제외한 유효 누적으로 한도 소진을 판정 */ + h_b = h_gross - h_a; + h_e = h_lim - h_b; + if (h_lim > 0) ratio = (h_b * 10000L) / h_lim; else ratio = 0; + if (h_b > h_lim) { h_val = 1; Bchg(b, T_MSG, 0, "일한도 초과", 0L); } + else if (ratio >= 9000) { h_val = 2; Bchg(b, T_MSG, 0, "한도 임박", 0L); } + else if (ratio >= 5000) { h_val = 3; Bchg(b, T_MSG, 0, "한도 절반 소진", 0L); } + else { h_val = 0; Bchg(b, T_MSG, 0, "한도 여유", 0L); } + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :h_val2 FROM merchant_summary WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 누적집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_pdgross FROM purchase WHERE merchant_id = :h_merch AND biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_val2 > 0 && h_gross > 0) ratio = (h_gross * 10000L) / (h_val2 + h_gross); + else ratio = 0; + h_share = (h_pdgross > 0) ? (h_gross * 100L / h_pdgross) : 0; + userlog("%s 추이: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_pdgross, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "한도점검 merch=%s 누적=%ld 한도=%ld 잔여=%ld", h_merch, h_b, h_lim, h_e); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_LIMITCHK', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_LIMITCHK' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_GROSS, h_b); + setl(b, T_ARG1, h_lim); + setl(b, T_ARG2, h_e); + setl(b, T_ARG3, ratio); + setl(b, T_COUNT, h_cnt); + setl(b, T_AMT1, h_a); + setl(b, T_ARG4, h_val); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_LIMITEXC.pgc b/app/src/ac/svc/ACQ_LIMITEXC.pgc index 19f4e79..afb0919 100644 --- a/app/src/ac/svc/ACQ_LIMITEXC.pgc +++ b/app/src/ac/svc/ACQ_LIMITEXC.pgc @@ -1,7 +1,7 @@ /* * ACQ_LIMITEXC.pgc - ac 모듈 서비스 ACQ_LIMITEXC (one service = one file). * 매입 한도 예외처리: 가맹점 일한도와 당일 누적 매입을 비교해 한도 초과 시 - * 미매입 원장에 LIMIT_EXC 예외를 기록하고, 여유 한도를 응답한다. + * 미매입 원장에 LIMIT_EXC 예외를 기록하고 여유 한도를 응답한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_LIMITEXC.h" @@ -9,32 +9,161 @@ void ACQ_LIMITEXC(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long room; + const char *SVC = "ACQ_LIMITEXC"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_id, h_lim = 0, h_used = 0; - char h_merch[64], h_bizdate[16], h_reason[64]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; - gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); - EXEC SQL SELECT daily_limit INTO :h_lim FROM merchant WHERE merchant_id = :h_merch; - if (sqlca.sqlcode == 100) { setl(b, T_RC, 5); FAIL(b); } - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_used FROM purchase - WHERE merchant_id = :h_merch AND biz_date = :h_bizdate AND status <> 'C'; - if (sqlca.sqlcode < 0) FAIL(b); - room = h_lim - h_used; - if (room >= 0) { setl(b, T_DELTA, room); setl(b, T_RC, 0); OK(b); } + userlog("%s 진입: 매입 상태 전이 요청 접수", SVC); - snprintf(h_reason, sizeof(h_reason), "LIMIT_EXC:%ld", -room); - EXEC SQL SELECT nextval('ac_unmatch_seq') INTO :h_id; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL INSERT INTO ac_unmatch (unmatch_id, purchase_id, reason, biz_date) - VALUES (:h_id, 0, :h_reason, :h_bizdate); - if (sqlca.sqlcode < 0) { userlog("ACQ_LIMITEXC FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - setl(b, T_DELTA, room); - setl(b, T_RC, 1); + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + + /* 3) 업무규칙 분기부: 상태 전이 가능여부/유형 판정(3+ 분기) */ + if (!(strcmp(h_prev, "A") == 0 || strcmp(h_prev, "M") == 0 || strcmp(h_prev, "S") == 0 || strcmp(h_prev, "C") == 0 || strcmp(h_prev, "H") == 0 || strcmp(h_prev, "V") == 0 || strcmp(h_prev, "P") == 0)) { + userlog("%s 거절: 상태 %s 에서 전이 불가", SVC, h_prev); + Bchg(b, T_MSG, 0, "상태 전이 불가", 0L); setl(b, T_RC, 120); FAIL(b); + } + /* 당일 유효 누적을 계산해 한도 초과 여부를 판정 */ + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_sum FROM purchase + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate + AND status NOT IN ('C','V'); + if (sqlca.sqlcode < 0) { + userlog("%s: 누적집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_e = h_lim - h_sum; + ratio = (h_lim > 0) ? (h_sum * 10000L / h_lim) : 0; + if (h_e >= 0) { h_val = 0; Bchg(b, T_MSG, 0, "한도 이내", 0L); } + else if (ratio >= 12000) { h_val = 3; Bchg(b, T_MSG, 0, "한도 20% 이상 초과", 0L); } + else { h_val = 2; Bchg(b, T_MSG, 0, "한도 초과", 0L); } + + /* 4) 본처리부: 부수행 기록 및 상태 갱신 */ + if (h_val > 0) { + EXEC SQL SELECT nextval('ac_unmatch_seq') INTO :h_id; + if (sqlca.sqlcode < 0) { + userlog("%s: 예외채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO ac_unmatch (unmatch_id, purchase_id, reason, biz_date) VALUES (:h_id, :h_pid, 'LIMIT_EXC', :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 예외등록 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + } + h_aff = 1; + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "한도예외 pid=%ld 누적=%ld 한도=%ld 여유=%ld", h_pid, h_sum, h_lim, h_e); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_LIMITEXC', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_LIMITEXC' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_GROSS, h_sum); + setl(b, T_ARG1, h_lim); + setl(b, T_ARG2, h_e); + setl(b, T_ARG3, ratio); + setl(b, T_ARG4, h_val); + setl(b, T_ID1, h_id); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_LIMITOVR.pgc b/app/src/ac/svc/ACQ_LIMITOVR.pgc index 2a78a29..47c2aae 100644 --- a/app/src/ac/svc/ACQ_LIMITOVR.pgc +++ b/app/src/ac/svc/ACQ_LIMITOVR.pgc @@ -1,7 +1,7 @@ /* * ACQ_LIMITOVR.pgc - ac 모듈 서비스 ACQ_LIMITOVR (one service = one file). * 매입 한도 초과 승인: 한도 초과로 보류(H)된 매입을 관리자가 예외 승인해 - * 접수(A)로 되살린다. 승인 표식(net 유지)만 남기고 반영 건수를 응답한다. + * 접수(A)로 되살린다. 승인 시 한도 소진 현황을 재확인한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_LIMITOVR.h" @@ -9,18 +9,166 @@ void ACQ_LIMITOVR(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_LIMITOVR"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_aff; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); - EXEC SQL UPDATE purchase SET status = 'A', updated_at = now() - WHERE purchase_id = :h_pid AND status = 'H' AND channel <> 'RSV'; - if (sqlca.sqlcode < 0) { userlog("ACQ_LIMITOVR FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + userlog("%s 진입: 매입 상태 전이 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "일반"); + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + + /* 3) 업무규칙 분기부: 상태 전이 가능여부/유형 판정(3+ 분기) */ + if (!(strcmp(h_prev, "H") == 0)) { + userlog("%s 거절: 상태 %s 에서 전이 불가", SVC, h_prev); + Bchg(b, T_MSG, 0, "상태 전이 불가", 0L); setl(b, T_RC, 120); FAIL(b); + } + /* 현재 소진율을 재확인해 예외 승인 등급을 정한다 */ + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_sum FROM purchase + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate + AND status NOT IN ('C','V'); + if (sqlca.sqlcode < 0) { + userlog("%s: 소진재확인 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_lim > 0) ? (h_sum * 10000L / h_lim) : 0; + if (ratio >= 15000) { Bchg(b, T_MSG, 0, "한도 150% 초과로 승인 불가", 0L); setl(b, T_RC, 123); FAIL(b); } + else if (ratio >= 12000) { h_val = 3; } + else if (ratio >= 10000) { h_val = 2; } + else { h_val = 1; } + + /* 4) 본처리부: 부수행 기록 및 상태 갱신 */ + EXEC SQL SELECT nextval('ledger_seq') INTO :h_ledid; + if (sqlca.sqlcode < 0) { + userlog("%s: 원장채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) VALUES (:h_ledid, :h_pid, 'LIMITOVR', :h_gross, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 예외분개 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL UPDATE purchase SET status = 'A', updated_at = now() WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 상태갱신 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } h_aff = sqlca.sqlerrd[2]; if (h_aff > 0) Bchg(b, T_STATUS, 0, "A", 0L); - setl(b, T_COUNT, h_aff); - setl(b, T_RC, h_aff > 0 ? 0 : 2); + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "한도예외승인 pid=%ld 소진=%ld 한도=%ld 등급=%ld", h_pid, h_sum, h_lim, h_val); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_LIMITOVR', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_LIMITOVR' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_GROSS, h_sum); + setl(b, T_ARG1, h_lim); + setl(b, T_ARG2, ratio); + setl(b, T_ARG3, h_val); + setl(b, T_ID1, h_ledid); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_LIMITUSAGE.pgc b/app/src/ac/svc/ACQ_LIMITUSAGE.pgc index 3632591..6aa4239 100644 --- a/app/src/ac/svc/ACQ_LIMITUSAGE.pgc +++ b/app/src/ac/svc/ACQ_LIMITUSAGE.pgc @@ -1,7 +1,7 @@ /* * ACQ_LIMITUSAGE.pgc - ac 모듈 서비스 ACQ_LIMITUSAGE (one service = one file). * 가맹점 한도소진 조회: 영업일 유효 매입(취소/무효 제외) 합계를 일한도와 - * 비교해 사용액/잔여액/소진율(bps)을 산출한다. 90% 초과 시 경보 표시. + * 비교해 사용액/잔여액/소진율(bps)을 산출하고 90% 초과 시 경보한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_LIMITUSAGE.h" @@ -9,29 +9,163 @@ void ACQ_LIMITUSAGE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long pct_bps; + const char *SVC = "ACQ_LIMITUSAGE"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_limit = 0, h_used = 0; - char h_merch[64], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL SELECT daily_limit INTO :h_limit FROM merchant WHERE merchant_id = :h_merch; - if (sqlca.sqlcode == 100) { setl(b, T_RC, 5); FAIL(b); } - if (sqlca.sqlcode < 0) FAIL(b); + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); - EXEC SQL SELECT coalesce(sum(amount), 0) INTO :h_used FROM purchase - WHERE merchant_id = :h_merch AND biz_date = :h_bizdate AND status NOT IN ('C', 'V'); - if (sqlca.sqlcode < 0) { userlog("ACQ_LIMITUSAGE FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + if (merch[0] == 0) { Bchg(b, T_MSG, 0, "가맹점 ID 누락", 0L); setl(b, T_RC, 101); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } - pct_bps = (h_limit > 0) ? (h_used * 10000) / h_limit : 10000; + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - setl(b, T_AMT1, h_used); - setl(b, T_AMT2, h_limit); - setl(b, T_AMT3, h_limit > h_used ? h_limit - h_used : 0); /* 잔여 한도 */ - setl(b, T_ARG1, pct_bps); - setl(b, T_RC, pct_bps > 9000 ? 1 : 0); /* 90% 초과 경보 */ + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + EXEC SQL SELECT name, status, daily_limit INTO :h_str1, :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (strcmp(h_status, "ACTIVE") != 0) + userlog("%s 주의: 가맹점 %s 상태=%s", SVC, h_merch, h_status); + /* 가맹점 MDR(bps) 및 상태분포 컨텍스트 로드 */ + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + EXEC SQL SELECT count(*) FILTER (WHERE status IN ('A','M')), count(*) FILTER (WHERE status = 'S') INTO :h_openc, :h_setc FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 상태분포 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산 진행 컨텍스트(기 정산 순액) */ + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_ctx FROM settlement WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_gross FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate AND status NOT IN ('C','V'); + if (sqlca.sqlcode < 0) { + userlog("%s: 유효매입 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(max(amount),0), coalesce(avg(amount),0)::bigint INTO :h_max, :h_avg FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate AND status NOT IN ('C','V'); + if (sqlca.sqlcode < 0) { + userlog("%s: 단건규모 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 소진율 구간별 경보 등급을 부여 */ + h_e = h_lim - h_gross; + if (h_lim > 0) ratio = (h_gross * 10000L) / h_lim; else ratio = 0; + if (ratio >= 10000) { h_val = 3; } + else if (ratio >= 9000) { h_val = 2; } + else if (ratio >= 7000) { h_val = 1; } + else { h_val = 0; } + if (h_val >= 2) userlog("%s 경보: 한도 소진율 %ld bps", SVC, ratio); + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :h_val2 FROM merchant_summary WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 누적집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_pdgross FROM purchase WHERE merchant_id = :h_merch AND biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_val2 > 0 && h_gross > 0) ratio = (h_gross * 10000L) / (h_val2 + h_gross); + else ratio = 0; + h_share = (h_pdgross > 0) ? (h_gross * 100L / h_pdgross) : 0; + userlog("%s 추이: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_pdgross, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "한도소진 merch=%s 사용=%ld 잔여=%ld 소진율=%ld", h_merch, h_gross, h_e, ratio); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_LIMITUSAGE', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_LIMITUSAGE' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_GROSS, h_gross); + setl(b, T_ARG1, h_lim); + setl(b, T_ARG2, h_e); + setl(b, T_ARG3, ratio); + setl(b, T_ARG4, h_val); + setl(b, T_COUNT, h_cnt); + setl(b, T_AMT1, h_max); + setl(b, T_AMT2, h_avg); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_MERCHSUM.pgc b/app/src/ac/svc/ACQ_MERCHSUM.pgc index 197fa16..0859c84 100644 --- a/app/src/ac/svc/ACQ_MERCHSUM.pgc +++ b/app/src/ac/svc/ACQ_MERCHSUM.pgc @@ -1,27 +1,120 @@ /* * ACQ_MERCHSUM.pgc - ac 모듈 서비스 ACQ_MERCHSUM (one service = one file). - * Moved verbatim from the former inline ac_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by ac_svr at tpsvrinit. + * 가맹점 일별 집계: 영업일 매입을 재집계해 merchant_summary 에 upsert 하고 + * 건수/총액을 회신한다. 상태별 분해와 직전일 대비 추세도 함께 산출한다. + * 입출력 계약 보존: 입력 T_MERCHANT/T_BIZDATE, 출력 T_COUNT/T_GROSS + * (acdb_upsert_merch_sum 반영). + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_MERCHSUM.h" void ACQ_MERCHSUM(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; + const char *SVC = "ACQ_MERCHSUM"; + char merch[64], bizdate[16], logmsg[256]; + long trend = 0, ratio = 0; EXEC SQL BEGIN DECLARE SECTION; long h_cnt, h_gross, h_fee, h_net; - char h_merch[64], h_bizdate[16]; + long h_a = 0, h_m = 0, h_s = 0, h_c = 0, h_pd = 0, h_lim = 0, h_logid = 0; + char h_merch[64], h_bizdate[16], h_status[8], h_logmsg[256]; EXEC SQL END DECLARE SECTION; + + userlog("%s 진입: 가맹점 일별 집계 요청", SVC); + + /* 1) 입력 검증부 */ gets_(b, T_MERCHANT, merch, sizeof(merch)); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (merch[0] == 0) { + Bchg(b, T_MSG, 0, "가맹점 ID 누락", 0L); setl(b, T_RC, 101); FAIL(b); + } strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { + Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + userlog("%s 컨텍스트: merch=%s bizdate=%s", SVC, h_merch, h_bizdate); + + /* 2) 마스터 조회부: 가맹점 존재/상태 확인 */ + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점 조회 실패 [%d]", SVC, sqlca.sqlcode); setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { + Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); + } + if (strcmp(h_status, "ACTIVE") != 0) + userlog("%s 주의: 가맹점 %s 상태=%s", SVC, h_merch, h_status); + + /* 4) 본처리부: 영업일 실적 재집계 (계약 보존 질의) */ 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); + if (sqlca.sqlcode < 0) { + userlog("%s: 실적 집계 실패 [%d]", SVC, sqlca.sqlcode); setl(b, T_RC, 90); FAIL(b); + } + + /* 상태별 분해 및 직전일 실적 */ + EXEC SQL SELECT count(*) FILTER (WHERE status = 'A'), count(*) FILTER (WHERE status = 'M'), + count(*) FILTER (WHERE status = 'S'), count(*) FILTER (WHERE status = 'C') + INTO :h_a, :h_m, :h_s, :h_c FROM purchase + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); } + + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_pd FROM purchase + WHERE merchant_id = :h_merch AND biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); } + + /* 3) 업무규칙 분기부: 실적 규모/추세/한도 소진 판정 (3+ 분기) */ + trend = (h_pd > 0) ? (h_gross * 100L / h_pd) : 0; + ratio = (h_lim > 0) ? (h_gross * 10000L / h_lim) : 0; + if (h_cnt == 0) { + Bchg(b, T_MSG, 0, "당일 실적 없음", 0L); + } else if (ratio >= 9000) { + Bchg(b, T_MSG, 0, "한도 임박 실적", 0L); + } else if (trend >= 200) { + Bchg(b, T_MSG, 0, "직전일 대비 급증", 0L); + } else if (trend > 0 && trend <= 50) { + Bchg(b, T_MSG, 0, "직전일 대비 급감", 0L); + } else { + Bchg(b, T_MSG, 0, "정상 실적", 0L); + } + + /* 6) 집계/요약 갱신부 (계약 보존: upsert 수행) */ + if (acdb_upsert_merch_sum(merch, bizdate, h_gross, h_fee, h_net, h_cnt) < 0) { + userlog("%s: 요약 upsert 실패 merch=%s", SVC, h_merch); + Bchg(b, T_MSG, 0, "요약 갱신 실패", 0L); setl(b, T_RC, 92); FAIL(b); + } + + /* 5) 이력/감사 적재부 */ + snprintf(logmsg, sizeof(logmsg), "가맹점집계 merch=%s 건수=%ld 총액=%ld 수수료=%ld 추세=%ld", + h_merch, h_cnt, h_gross, h_fee, trend); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_MERCHSUM', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); } + + /* 8) 응답부 (계약 보존: T_COUNT / T_GROSS) */ + setl(b, T_COUNT, h_cnt); + setl(b, T_GROSS, h_gross); + setl(b, T_FEE, h_fee); + setl(b, T_NET, h_net); + setl(b, T_ARG1, h_a); + setl(b, T_ARG2, h_m); + setl(b, T_ARG3, h_s); + setl(b, T_ARG4, h_c); + setl(b, T_ARG5, trend); + setl(b, T_ARG6, ratio); + setl(b, T_RC, 0); + userlog("%s 완료: merch=%s 건수=%ld 총액=%ld", SVC, h_merch, h_cnt, h_gross); OK(b); } diff --git a/app/src/ac/svc/ACQ_MONSTAT.pgc b/app/src/ac/svc/ACQ_MONSTAT.pgc index 72ccef1..223aaab 100644 --- a/app/src/ac/svc/ACQ_MONSTAT.pgc +++ b/app/src/ac/svc/ACQ_MONSTAT.pgc @@ -1,7 +1,7 @@ /* * ACQ_MONSTAT.pgc - ac 모듈 서비스 ACQ_MONSTAT (one service = one file). - * 매입 월간 통계: 기준일이 속한 달 전체의 건수/총액/평균 거래액과 영업일수 - * (거래 발생일 기준)를 date_trunc 월 경계로 집계한다. + * 매입 월간 통계: 기준일이 속한 달 전체의 건수/총액/평균 거래액과 + * 영업일수(거래 발생일 기준)를 date_trunc 월 경계로 집계한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_MONSTAT.h" @@ -9,27 +9,149 @@ void ACQ_MONSTAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_MONSTAT"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt = 0, h_amt = 0, h_avg = 0, h_days = 0; - char h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL SELECT count(*), - coalesce(sum(amount), 0), - coalesce(avg(amount)::bigint, 0), - count(DISTINCT biz_date) - INTO :h_cnt, :h_amt, :h_avg, :h_days - FROM purchase - WHERE date_trunc('month', biz_date) = date_trunc('month', CAST(:h_bizdate AS date)) - AND status NOT IN ('C', 'V'); - if (sqlca.sqlcode < 0) { userlog("ACQ_MONSTAT FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } + + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(avg(amount),0)::bigint, count(DISTINCT biz_date) INTO :h_cnt, :h_gross, :h_avg, :h_n FROM purchase WHERE date_trunc('month', biz_date) = date_trunc('month', :h_bizdate::date); + if (sqlca.sqlcode < 0) { + userlog("%s: 월간집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_fee, :h_net FROM purchase WHERE date_trunc('month', biz_date) = date_trunc('month', :h_bizdate::date); + if (sqlca.sqlcode < 0) { + userlog("%s: 월간수수료 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 영업일수 대비 일평균 실적으로 월 진척을 판정 */ + h_share = (h_n > 0) ? (h_gross / h_n) : 0; + if (h_gross > 0) ratio = (h_fee * 10000L) / h_gross; else ratio = 0; + if (h_cnt == 0) { h_val = 0; } + else if (h_n >= 20) { h_val = 3; } + else if (h_n >= 10) { h_val = 2; } + else { h_val = 1; } + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "월간통계 기준=%s 건수=%ld 총액=%ld 영업일=%ld", h_bizdate, h_cnt, h_gross, h_n); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_MONSTAT', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_MONSTAT' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_amt); - setl(b, T_AMT1, h_avg); /* 평균 거래액 */ - setl(b, T_ARG1, h_days); /* 거래 발생 영업일수 */ - setl(b, T_RC, h_cnt > 0 ? 0 : 1); + setl(b, T_GROSS, h_gross); + setl(b, T_ARG1, h_avg); + setl(b, T_ARG2, h_n); + setl(b, T_AMT1, h_share); + setl(b, T_FEE, h_fee); + setl(b, T_NET, h_net); + setl(b, T_ARG3, ratio); + setl(b, T_ARG4, h_val); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_NETCALC.pgc b/app/src/ac/svc/ACQ_NETCALC.pgc index dba08e7..e33c130 100644 --- a/app/src/ac/svc/ACQ_NETCALC.pgc +++ b/app/src/ac/svc/ACQ_NETCALC.pgc @@ -1,7 +1,7 @@ /* * ACQ_NETCALC.pgc - ac 모듈 서비스 ACQ_NETCALC (one service = one file). * 정산금 재계산: 매입금액에서 수수료와 원천세 누계를 차감한 실정산금을 - * 다시 구해 purchase.net 을 보정한다. 변경 없으면 T_RC=1. + * 다시 구해 purchase.net 을 보정한다. 변경이 없으면 결과코드로 알린다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_NETCALC.h" @@ -9,38 +9,151 @@ void ACQ_NETCALC(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long newnet; + const char *SVC = "ACQ_NETCALC"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_amt = 0, h_fee = 0, h_oldnet = 0, h_wht = 0, h_newnet; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 매입 금액/수수료 조정 요청", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - EXEC SQL SELECT amount, fee, net INTO :h_amt, :h_fee, :h_oldnet - FROM purchase WHERE purchase_id = :h_pid; - if (sqlca.sqlcode == 100) { setl(b, T_RC, 5); FAIL(b); } - if (sqlca.sqlcode < 0) FAIL(b); + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; - /* 원천세 누계 반영 */ - EXEC SQL SELECT coalesce(sum(tax_amount), 0) INTO :h_wht - FROM ac_wht WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); + /* 3) 업무규칙 분기부: 조정 유형/부호/한도 판정(3+ 분기) */ + /* 원천세 누계를 반영한 실정산금을 계산 */ + EXEC SQL SELECT coalesce(sum(tax_amount),0) INTO :h_tax FROM ac_wht + WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 원천세누계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + computed = h_gross - h_fee - h_tax; + h_delta = computed - h_net; + if (h_delta == 0) { Bchg(b, T_MSG, 0, "정산금 변동 없음", 0L); setl(b, T_RC, 179); FAIL(b); } + else if (h_tax > 0) { h_val = 3; Bchg(b, T_MSG, 0, "원천세 반영 보정", 0L); } + else if (h_delta < 0) { h_val = 2; Bchg(b, T_MSG, 0, "정산금 하향 보정", 0L); } + else { h_val = 1; Bchg(b, T_MSG, 0, "정산금 상향 보정", 0L); } - newnet = h_amt - h_fee - h_wht; - if (newnet == h_oldnet) { - setl(b, T_NET, newnet); - setl(b, T_RC, 1); /* 이미 정합 */ - OK(b); + /* 4) 본처리부: 조정 원장 기록 및 매입 금액 보정 */ + h_comp = computed; + EXEC SQL UPDATE purchase SET net = :h_comp, updated_at = now() WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산금보정 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_aff = sqlca.sqlerrd[2]; + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "정산금재계산 pid=%ld 기존=%ld 신규=%ld 원천세=%ld", h_pid, h_net, computed, h_tax); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_NETCALC', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); } - h_newnet = newnet; - EXEC SQL UPDATE purchase SET net = :h_newnet, updated_at = now() - WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0) { userlog("ACQ_NETCALC FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_NETCALC' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); - setl(b, T_NET, newnet); - setl(b, T_TAX, h_wht); - setl(b, T_DELTA, newnet - h_oldnet); + /* 8) 응답부 */ + setl(b, T_NET, computed); + setl(b, T_DELTA, h_delta); + setl(b, T_TAX, h_tax); + setl(b, T_FEE, h_fee); + setl(b, T_AMOUNT, h_gross); + setl(b, T_ARG1, h_val); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_NOINTSHARE.pgc b/app/src/ac/svc/ACQ_NOINTSHARE.pgc index 1cbd854..d2721e4 100644 --- a/app/src/ac/svc/ACQ_NOINTSHARE.pgc +++ b/app/src/ac/svc/ACQ_NOINTSHARE.pgc @@ -6,39 +6,157 @@ */ #include "ACQ_NOINTSHARE.h" -#define AC_NOINT_BPS_PER_MONTH 15L /* 발급사 분담률: 월당 15bps */ - void ACQ_NOINTSHARE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long share, adj; - char bizdate[16]; + const char *SVC = "ACQ_NOINTSHARE"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_months = 0, h_amt = 0; - char h_issuer[32]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 매입 금액/수수료 조정 요청", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - /* 할부 스케줄에서 개월수 확인 (스케줄 없으면 무이자 대상 아님) */ - EXEC SQL SELECT coalesce(max(months), 0) INTO :h_months + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + + /* 3) 업무규칙 분기부: 조정 유형/부호/한도 판정(3+ 분기) */ + /* 할부 개월수를 확인하고 분담률을 산정 */ + EXEC SQL SELECT coalesce(max(months),0), count(*) INTO :h_months, :h_cnt FROM ac_install WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_months < 2) { setl(b, T_RC, 4); FAIL(b); } + if (sqlca.sqlcode < 0) { + userlog("%s: 할부개월 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_months <= 0) { Bchg(b, T_MSG, 0, "할부 매입이 아님", 0L); setl(b, T_RC, 180); FAIL(b); } + threshold = h_months * 15L; + computed = h_gross * threshold / 10000L; + if (h_months >= 12) { h_val = 3; Bchg(b, T_MSG, 0, "장기 무이자 분담", 0L); } + else if (h_months >= 6) { h_val = 2; Bchg(b, T_MSG, 0, "중기 무이자 분담", 0L); } + else { h_val = 1; Bchg(b, T_MSG, 0, "단기 무이자 분담", 0L); } + h_delta = -computed; - EXEC SQL SELECT amount, issuer INTO :h_amt, :h_issuer - FROM purchase WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); + /* 4) 본처리부: 조정 원장 기록 및 매입 금액 보정 */ + h_adjid = acdb_insert_fee_adj(h_pid, h_delta, "NOINT", h_bizdate); + if (h_adjid < 0) { Bchg(b, T_MSG, 0, "분담금 이력 실패", 0L); setl(b, T_RC, 98); FAIL(b); } + h_comp = computed; + EXEC SQL UPDATE purchase SET fee = fee - :h_comp, net = net + :h_comp, updated_at = now() WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 분담반영 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_aff = sqlca.sqlerrd[2]; - share = acq_fee(h_amt, h_months * AC_NOINT_BPS_PER_MONTH); - adj = acdb_insert_fee_adj(h_pid, -share, "NOINT", bizdate); - if (adj < 0) { userlog("ACQ_NOINTSHARE adj FAIL pid=%ld", h_pid); FAIL(b); } + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); - Bchg(b, T_ISSUER, 0, h_issuer, 0L); - setl(b, T_AMT1, share); /* 발급사 분담금 */ + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "무이자분담 pid=%ld 개월=%ld 분담률=%ld 분담금=%ld", h_pid, h_months, threshold, computed); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_NOINTSHARE', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_NOINTSHARE' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_ADJ, h_adjid); setl(b, T_INSTALL_N, h_months); - setl(b, T_ADJ, adj); + setl(b, T_ARG2, threshold); + setl(b, T_DELTA, h_delta); + setl(b, T_FEE, h_fee - computed); + setl(b, T_ARG1, h_val); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_PARTCXL.pgc b/app/src/ac/svc/ACQ_PARTCXL.pgc index 0fcbfd5..c9cb6cb 100644 --- a/app/src/ac/svc/ACQ_PARTCXL.pgc +++ b/app/src/ac/svc/ACQ_PARTCXL.pgc @@ -1,7 +1,7 @@ /* * ACQ_PARTCXL.pgc - ac 모듈 서비스 ACQ_PARTCXL (one service = one file). - * 부분취소: 취소액이 잔여 매입금액 미만인 경우에만 허용. 수수료를 비례 - * 배분해 환급하고 purchase 잔액(amount/fee/net)을 차감, PARTIAL 취소행 생성. + * 부분취소: 취소액이 잔여 매입금액 미만인 경우에만 허용한다. 수수료를 + * 비례 배분해 환급하고 purchase 잔액(amount/fee/net)을 차감한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_PARTCXL.h" @@ -9,43 +9,167 @@ void ACQ_PARTCXL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long feeback; + const char *SVC = "ACQ_PARTCXL"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_cid, h_pid, h_cxl, h_amt = 0, h_fee = 0, h_fb; - char h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 매입 상태 전이 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ h_pid = getl(b, T_PURCHASE_ID); - h_cxl = getl(b, T_AMOUNT); - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + h_amt = getl(b, T_AMOUNT); + if (h_amt < 1L) { Bchg(b, T_MSG, 0, "금액 범위 오류", 0L); setl(b, T_RC, 104); FAIL(b); } + gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "일반"); + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - EXEC SQL SELECT amount, fee INTO :h_amt, :h_fee FROM purchase - WHERE purchase_id = :h_pid AND status IN ('A', 'M'); - if (sqlca.sqlcode == 100) { setl(b, T_RC, 5); FAIL(b); } - if (sqlca.sqlcode < 0) FAIL(b); - if (h_cxl <= 0 || h_cxl >= h_amt) { setl(b, T_RC, 3); FAIL(b); } + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; - /* 수수료 비례 환급분: fee * 취소액 / 원금액 */ - feeback = (h_fee * h_cxl) / h_amt; - h_fb = feeback; - - EXEC SQL SELECT nextval('ac_cancel_seq') INTO :h_cid; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL INSERT INTO ac_cancel (cancel_id, purchase_id, cancel_type, amount, reason, biz_date) - VALUES (:h_cid, :h_pid, 'PARTIAL', :h_cxl, 'PARTCXL', :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); - - EXEC SQL UPDATE purchase - SET amount = amount - :h_cxl, - fee = fee - :h_fb, - net = net - (:h_cxl - :h_fb), - updated_at = now() + /* 3) 업무규칙 분기부: 상태 전이 가능여부/유형 판정(3+ 분기) */ + if (!(strcmp(h_prev, "A") == 0 || strcmp(h_prev, "M") == 0)) { + userlog("%s 거절: 상태 %s 에서 전이 불가", SVC, h_prev); + Bchg(b, T_MSG, 0, "상태 전이 불가", 0L); setl(b, T_RC, 120); FAIL(b); + } + /* 기취소 누계를 반영한 잔액으로 부분취소 가능성을 검증 */ + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_sum FROM ac_cancel WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0) { userlog("ACQ_PARTCXL FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + if (sqlca.sqlcode < 0) { + userlog("%s: 기취소 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_rem = h_gross - h_sum; + if (h_amt >= h_rem) { Bchg(b, T_MSG, 0, "취소액이 잔액 이상(전액취소 대상)", 0L); setl(b, T_RC, 146); FAIL(b); } + h_delta = (h_gross > 0) ? (h_fee * h_amt / h_gross) : 0; + if (h_amt * 2 >= h_rem) { h_val = 3; } + else if (h_amt * 4 >= h_rem) { h_val = 2; } + else { h_val = 1; } - setl(b, T_CANCEL_ID, h_cid); - setl(b, T_AMT1, h_amt - h_cxl); /* 잔여 매입금액 */ - setl(b, T_FEE, feeback); + /* 4) 본처리부: 부수행 기록 및 상태 갱신 */ + EXEC SQL SELECT nextval('ac_cancel_seq') INTO :h_canid; + if (sqlca.sqlcode < 0) { + userlog("%s: 취소채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO ac_cancel (cancel_id, purchase_id, cancel_type, amount, reason, biz_date) VALUES (:h_canid, :h_pid, 'PARTIAL', :h_amt, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 부분취소행 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL UPDATE purchase SET amount = amount - :h_amt, fee = fee - :h_delta, net = net - (:h_amt - :h_delta), updated_at = now() WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 잔액차감 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_aff = 1; + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "부분취소 pid=%ld 취소=%ld 잔액=%ld 수수료환급=%ld", h_pid, h_amt, h_rem - h_amt, h_delta); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_PARTCXL', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_PARTCXL' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_CANCEL_ID, h_canid); + setl(b, T_AMOUNT, h_amt); + setl(b, T_FEE, h_delta); + setl(b, T_ARG1, h_rem - h_amt); + setl(b, T_ARG2, h_val); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_PARTIAL.pgc b/app/src/ac/svc/ACQ_PARTIAL.pgc index 2f34ca9..aa7d3b8 100644 --- a/app/src/ac/svc/ACQ_PARTIAL.pgc +++ b/app/src/ac/svc/ACQ_PARTIAL.pgc @@ -1,26 +1,162 @@ /* * ACQ_PARTIAL.pgc - ac 모듈 서비스 ACQ_PARTIAL (one service = one file). - * Moved verbatim from the former inline ac_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by ac_svr at tpsvrinit. + * 부분처리 등록: 매입의 일부 금액만 확정 처리하고 나머지는 미확정으로 + * 남긴다. PARTIAL 취소행으로 미확정분을 표시한다. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_PARTIAL.h" void ACQ_PARTIAL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + const char *SVC = "ACQ_PARTIAL"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_id, h_pid, h_amt; - char h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); h_amt = getl(b, T_AMOUNT); + + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 매입 상태 전이 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + h_amt = getl(b, T_AMOUNT); + if (h_amt < 1L) { Bchg(b, T_MSG, 0, "금액 범위 오류", 0L); setl(b, T_RC, 104); FAIL(b); } 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); + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + + /* 3) 업무규칙 분기부: 상태 전이 가능여부/유형 판정(3+ 분기) */ + if (!(strcmp(h_prev, "A") == 0 || strcmp(h_prev, "M") == 0)) { + userlog("%s 거절: 상태 %s 에서 전이 불가", SVC, h_prev); + Bchg(b, T_MSG, 0, "상태 전이 불가", 0L); setl(b, T_RC, 120); FAIL(b); + } + /* 확정 금액이 원금 범위인지 확인하고 확정 비율을 산정 */ + if (h_amt > h_gross) { Bchg(b, T_MSG, 0, "확정액이 원금 초과", 0L); setl(b, T_RC, 147); FAIL(b); } + h_rem = h_gross - h_amt; + ratio = (h_gross > 0) ? (h_amt * 10000L / h_gross) : 0; + if (ratio >= 9000) { h_val = 1; Bchg(b, T_MSG, 0, "대부분 확정", 0L); } + else if (ratio >= 5000) { h_val = 2; Bchg(b, T_MSG, 0, "과반 확정", 0L); } + else { h_val = 3; Bchg(b, T_MSG, 0, "소액 확정", 0L); } + + /* 4) 본처리부: 부수행 기록 및 상태 갱신 */ + EXEC SQL SELECT nextval('ac_cancel_seq') INTO :h_canid; + if (sqlca.sqlcode < 0) { + userlog("%s: 취소채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO ac_cancel (cancel_id, purchase_id, cancel_type, amount, reason, biz_date) VALUES (:h_canid, :h_pid, 'PARTIAL', :h_rem, 'PARTIAL', :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 미확정행 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_aff = 1; + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "부분처리 pid=%ld 확정=%ld 미확정=%ld 확정률=%ld", h_pid, h_amt, h_rem, ratio); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_PARTIAL', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_PARTIAL' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_CANCEL_ID, h_canid); + setl(b, T_AMOUNT, h_amt); + setl(b, T_ARG1, h_rem); + setl(b, T_ARG2, ratio); + setl(b, T_ARG3, h_val); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_PARTRFND.pgc b/app/src/ac/svc/ACQ_PARTRFND.pgc index 947658c..22a79ba 100644 --- a/app/src/ac/svc/ACQ_PARTRFND.pgc +++ b/app/src/ac/svc/ACQ_PARTRFND.pgc @@ -9,36 +9,173 @@ void ACQ_PARTRFND(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + const char *SVC = "ACQ_PARTRFND"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_cid, h_pid, h_req, h_orig = 0, h_prev = 0; - char h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 매입 상태 전이 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ h_pid = getl(b, T_PURCHASE_ID); - h_req = getl(b, T_AMOUNT); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + h_amt = getl(b, T_AMOUNT); + if (h_amt < 1L) { Bchg(b, T_MSG, 0, "금액 범위 오류", 0L); setl(b, T_RC, 104); FAIL(b); } + gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "일반"); + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - EXEC SQL SELECT amount INTO :h_orig FROM purchase WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; - /* 환불 누계 검증: 이미 환불된 합계 + 이번 요청액 <= 매입금액 */ - EXEC SQL SELECT coalesce(sum(amount), 0) INTO :h_prev FROM ac_cancel - WHERE purchase_id = :h_pid AND cancel_type = 'REFUND'; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_req <= 0 || h_prev + h_req > h_orig) { setl(b, T_RC, 6); FAIL(b); } + /* 3) 업무규칙 분기부: 상태 전이 가능여부/유형 판정(3+ 분기) */ + if (!(strcmp(h_prev, "M") == 0 || strcmp(h_prev, "S") == 0)) { + userlog("%s 거절: 상태 %s 에서 전이 불가", SVC, h_prev); + Bchg(b, T_MSG, 0, "상태 전이 불가", 0L); setl(b, T_RC, 120); FAIL(b); + } + /* 기환불 누계를 확인해 초과 환불을 차단 */ + EXEC SQL SELECT coalesce(sum(amount),0), count(*) INTO :h_sum, :h_cnt + FROM ac_cancel WHERE purchase_id = :h_pid AND cancel_type = 'REFUND'; + if (sqlca.sqlcode < 0) { + userlog("%s: 기환불 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_sum + h_amt > h_gross) { Bchg(b, T_MSG, 0, "환불 누계가 원금 초과", 0L); setl(b, T_RC, 148); FAIL(b); } + h_rem = h_gross - h_sum - h_amt; + if (h_rem == 0) { h_val = 3; Bchg(b, T_MSG, 0, "전액 환불 완료", 0L); } + else if (h_cnt >= 2) { h_val = 2; Bchg(b, T_MSG, 0, "다회 부분환불", 0L); } + else { h_val = 1; Bchg(b, T_MSG, 0, "부분환불", 0L); } + h_delta = -h_amt; - EXEC SQL SELECT nextval('ac_cancel_seq') INTO :h_cid; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL INSERT INTO ac_cancel (cancel_id, purchase_id, cancel_type, amount, reason, biz_date) - VALUES (:h_cid, :h_pid, 'REFUND', :h_req, 'PARTRFND', :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); + /* 4) 본처리부: 부수행 기록 및 상태 갱신 */ + EXEC SQL SELECT nextval('ac_cancel_seq') INTO :h_canid; + if (sqlca.sqlcode < 0) { + userlog("%s: 환불채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO ac_cancel (cancel_id, purchase_id, cancel_type, amount, reason, biz_date) VALUES (:h_canid, :h_pid, 'REFUND', :h_amt, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 환불행 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT nextval('ledger_seq') INTO :h_ledid; + if (sqlca.sqlcode < 0) { + userlog("%s: 원장채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) VALUES (:h_ledid, :h_pid, 'PARTRFND', :h_delta, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 환불분개 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_aff = 1; - if (acdb_insert_ledger(h_pid, "PARTRFND", -h_req, bizdate) < 0) FAIL(b); + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); - setl(b, T_CANCEL_ID, h_cid); - setl(b, T_AMT2, h_prev + h_req); /* 환불 누계 */ + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "부분환불 pid=%ld 환불=%ld 누계=%ld 잔여=%ld", h_pid, h_amt, h_sum + h_amt, h_rem); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_PARTRFND', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_PARTRFND' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_CANCEL_ID, h_canid); + setl(b, T_AMOUNT, h_amt); + setl(b, T_AMT1, h_sum + h_amt); + setl(b, T_ARG1, h_rem); + setl(b, T_ARG2, h_val); + setl(b, T_ID1, h_ledid); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_PROMO.pgc b/app/src/ac/svc/ACQ_PROMO.pgc index 67e8e51..7e36e1e 100644 --- a/app/src/ac/svc/ACQ_PROMO.pgc +++ b/app/src/ac/svc/ACQ_PROMO.pgc @@ -9,34 +9,154 @@ void ACQ_PROMO(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char promo[32], bizdate[16]; + const char *SVC = "ACQ_PROMO"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_amt; - char h_merch[64], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 신규 매입 접수 요청", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + if (merch[0] == 0) { Bchg(b, T_MSG, 0, "가맹점 ID 누락", 0L); setl(b, T_RC, 101); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; h_amt = getl(b, T_AMOUNT); - gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); - gets_(b, T_STR1, promo, sizeof(promo)); + if (h_amt < 1L) { Bchg(b, T_MSG, 0, "금액 범위 오류", 0L); setl(b, T_RC, 104); FAIL(b); } gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - /* 행사 코드 형식 검증: PRM- 접두 필수 */ - if (strncmp(promo, "PRM-", 4) != 0) { setl(b, T_RC, 7); FAIL(b); } - if (h_amt <= 0) { setl(b, T_RC, 3); FAIL(b); } + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; - EXEC SQL SELECT nextval('purchase_seq') INTO :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); + /* 3) 업무규칙 분기부: 가맹점 상태/금액 구간/채널 정책(3+ 분기) */ + if (strcmp(h_status, "ACTIVE") != 0) { Bchg(b, T_MSG, 0, "비활성 가맹점", 0L); setl(b, T_RC, 121); FAIL(b); } + /* 행사 매입은 수수료를 전액 면제하되 금액 상한을 둔다 */ + if (h_amt > 3000000) { Bchg(b, T_MSG, 0, "행사 매입 한도 초과", 0L); setl(b, T_RC, 162); FAIL(b); } + if (h_amt >= 1000000) { h_val = 3; } + else if (h_amt >= 100000) { h_val = 2; } + else { h_val = 1; } - /* 수수료 면제: fee 0, net 전액, 분류 PROMO */ - EXEC SQL INSERT INTO purchase (purchase_id, merchant_id, amount, fee, net, status, biz_date, channel, category) - VALUES (:h_pid, :h_merch, :h_amt, 0, :h_amt, 'A', :h_bizdate, 'POS', 'PROMO'); - if (sqlca.sqlcode < 0) { userlog("ACQ_PROMO FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + /* 4) 본처리부: 수수료 산출 및 매입/승인 원장 생성 */ + bps = h_rate; h_fee = acq_fee(h_amt, bps); h_net = h_amt - h_fee; + /* 수수료 면제: 원 MDR 수수료를 감면액으로 보존해 응답 */ + h_delta = h_fee; + h_fee = 0; + h_net = h_amt; + EXEC SQL SELECT nextval('ledger_seq') INTO :h_ledid; + if (sqlca.sqlcode < 0) { + userlog("%s: 원장채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_new = acdb_next_purchase_id(); + if (h_new < 0) { Bchg(b, T_MSG, 0, "매입 채번 실패", 0L); setl(b, T_RC, 93); FAIL(b); } + if (acdb_insert_purchase(h_new, h_merch, h_amt, h_fee, h_net, "A", "POS", h_bizdate) < 0) + { Bchg(b, T_MSG, 0, "매입 생성 실패", 0L); setl(b, T_RC, 94); FAIL(b); } + if (acdb_insert_approval(h_new, 1) < 0) { Bchg(b, T_MSG, 0, "승인 적재 실패", 0L); setl(b, T_RC, 95); FAIL(b); } + h_aff = 1; h_pid = h_new; + EXEC SQL UPDATE purchase SET category = 'PROMO' WHERE purchase_id = :h_new; + if (sqlca.sqlcode < 0) { + userlog("%s: 분류표시 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } - if (acdb_insert_ledger(h_pid, "PROMO", h_amt, bizdate) < 0) FAIL(b); + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); - setl(b, T_PURCHASE_ID, h_pid); - setl(b, T_NET, h_amt); + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "프로모션매입 merch=%s 금액=%ld 감면=%ld 등급=%ld", h_merch, h_amt, h_delta, h_val); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_PROMO', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_PROMO' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_PURCHASE_ID, h_new); + setl(b, T_FEE, h_fee); + setl(b, T_NET, h_net); + setl(b, T_DELTA, h_delta); + setl(b, T_ARG1, h_val); + Bchg(b, T_CATEGORY, 0, "PROMO", 0L); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_PROMOQRY.pgc b/app/src/ac/svc/ACQ_PROMOQRY.pgc index d0e90cb..e431b75 100644 --- a/app/src/ac/svc/ACQ_PROMOQRY.pgc +++ b/app/src/ac/svc/ACQ_PROMOQRY.pgc @@ -1,7 +1,7 @@ /* * ACQ_PROMOQRY.pgc - ac 모듈 서비스 ACQ_PROMOQRY (one service = one file). - * 프로모션 조회: 영업일 프로모션(category=PROMO) 매입의 건수와 매출, 감면 - * 수수료 합계를 조회해 행사 성과를 응답한다. + * 프로모션 조회: 영업일 프로모션(category=PROMO) 매입의 건수와 매출, + * 감면 수수료 합계를 조회해 행사 성과를 응답한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_PROMOQRY.h" @@ -9,21 +9,148 @@ void ACQ_PROMOQRY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_PROMOQRY"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_n = 0, h_sum = 0, h_fee = 0; - char h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0) - INTO :h_n, :h_sum, :h_fee FROM purchase - WHERE category = 'PROMO' AND biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); - setl(b, T_COUNT, h_n); - setl(b, T_GROSS, h_sum); + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } + + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + 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 biz_date = :h_bizdate AND category = 'PROMO'; + if (sqlca.sqlcode < 0) { + userlog("%s: 프로모션집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_a, :h_b FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 전체집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 프로모션 매출 비중과 수수료 감면 효과를 판정 */ + if (h_b > 0) ratio = (h_gross * 10000L) / h_b; else ratio = 0; + h_e = (h_gross * 25L / 10000L) - h_fee; + if (h_cnt == 0) { h_val = 0; } + else if (ratio >= 3000) { h_val = 3; } + else if (ratio >= 1000) { h_val = 2; } + else { h_val = 1; } + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "프로모션조회 bizdate=%s 건수=%ld 매출=%ld 감면=%ld", h_bizdate, h_cnt, h_gross, h_e); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_PROMOQRY', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_PROMOQRY' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_GROSS, h_gross); setl(b, T_FEE, h_fee); - setl(b, T_RC, h_n > 0 ? 0 : 1); + setl(b, T_NET, h_net); + setl(b, T_ARG1, ratio); + setl(b, T_ARG2, h_e); + setl(b, T_ARG3, h_val); + setl(b, T_ARG4, h_a); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_PROMOSTAT.pgc b/app/src/ac/svc/ACQ_PROMOSTAT.pgc index e9113cd..0112a52 100644 --- a/app/src/ac/svc/ACQ_PROMOSTAT.pgc +++ b/app/src/ac/svc/ACQ_PROMOSTAT.pgc @@ -1,7 +1,7 @@ /* * ACQ_PROMOSTAT.pgc - ac 모듈 서비스 ACQ_PROMOSTAT (one service = one file). - * 프로모션 통계: 영업일 업종/분류별 매출을 그룹 집계해 분류코드·매출 쌍을 - * occurrence 로 내리고 프로모션 분류가 차지하는 비중을 계산한다. + * 프로모션 통계: 영업일 업종/분류별 매출을 그룹 집계해 최다 분류와 + * 프로모션 분류가 차지하는 비중을 계산한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_PROMOSTAT.h" @@ -9,32 +9,153 @@ void ACQ_PROMOSTAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long grand = 0, promo = 0, kinds = 0; + const char *SVC = "ACQ_PROMOSTAT"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_sum; - char h_cat[32], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL DECLARE pms_cur CURSOR FOR - SELECT category, sum(amount) FROM purchase WHERE biz_date = :h_bizdate - GROUP BY category ORDER BY sum(amount) DESC; - EXEC SQL OPEN pms_cur; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH pms_cur INTO :h_cat, :h_sum; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE pms_cur; FAIL(b); } - if (kinds < 8) { Badd(b, T_CATEGORY, h_cat, 0L); Badd(b, T_AMT1, (char *)&h_sum, 0L); } - grand += h_sum; - if (strcmp(h_cat, "PROMO") == 0) promo = h_sum; - kinds++; + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); } - EXEC SQL CLOSE pms_cur; + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } - setl(b, T_GROSS, grand); - setl(b, T_AMT2, promo); - setl(b, T_RC, grand > 0 ? promo * 100 / grand : 0); + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(DISTINCT category), count(*), coalesce(sum(amount),0) INTO :h_n, :h_cnt, :h_gross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 분류모집단 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(category,''), coalesce(sum(amount),0) INTO :h_str2, :h_a FROM purchase WHERE biz_date = :h_bizdate GROUP BY category ORDER BY sum(amount) DESC LIMIT 1; + if (sqlca.sqlcode < 0) { + userlog("%s: 최다분류 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_b FROM purchase WHERE biz_date = :h_bizdate AND category = 'PROMO'; + if (sqlca.sqlcode < 0) { + userlog("%s: 프로모션분 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 프로모션 비중으로 행사 의존도를 판정 */ + if (h_gross > 0) ratio = (h_b * 10000L) / h_gross; else ratio = 0; + if (h_gross == 0) { h_val = 0; } + else if (ratio >= 4000) { h_val = 3; } + else if (ratio >= 1500) { h_val = 2; } + else { h_val = 1; } + h_avg = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "프로모션통계 bizdate=%s 분류수=%ld 최다=%s 행사비중=%ld", h_bizdate, h_n, h_str2, ratio); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_PROMOSTAT', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_PROMOSTAT' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_COUNT, h_n); + Bchg(b, T_CATEGORY, 0, h_str2, 0L); + setl(b, T_AMT1, h_a); + setl(b, T_AMT2, h_b); + setl(b, T_ARG1, ratio); + setl(b, T_ARG2, h_val); + setl(b, T_GROSS, h_gross); + setl(b, T_AMT3, h_avg); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_QTRSTAT.pgc b/app/src/ac/svc/ACQ_QTRSTAT.pgc index 84e6c7f..496daee 100644 --- a/app/src/ac/svc/ACQ_QTRSTAT.pgc +++ b/app/src/ac/svc/ACQ_QTRSTAT.pgc @@ -1,7 +1,7 @@ /* * ACQ_QTRSTAT.pgc - ac 모듈 서비스 ACQ_QTRSTAT (one service = one file). - * 매입 분기별 통계: 지정 연도(T_ARG1)의 매입 매출을 1~4분기로 나눠 분기별 - * 합계를 한 번의 집계로 산출하고 연간 총매출을 함께 응답한다. + * 매입 분기별 통계: 지정 연도(T_ARG1)의 매입 매출을 1~4분기로 나눠 + * 분기별 합계를 한 번의 집계로 산출하고 연간 총매출을 함께 응답한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_QTRSTAT.h" @@ -9,27 +9,156 @@ void ACQ_QTRSTAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long yr; + const char *SVC = "ACQ_QTRSTAT"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_q1 = 0, h_q2 = 0, h_q3 = 0, h_q4 = 0; - int h_yr; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - yr = getl(b, T_ARG1); if (yr < 2000) yr = 2026; - h_yr = (int)yr; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL SELECT coalesce(sum(amount) FILTER (WHERE extract(quarter from biz_date) = 1),0), - coalesce(sum(amount) FILTER (WHERE extract(quarter from biz_date) = 2),0), - coalesce(sum(amount) FILTER (WHERE extract(quarter from biz_date) = 3),0), - coalesce(sum(amount) FILTER (WHERE extract(quarter from biz_date) = 4),0) - INTO :h_q1, :h_q2, :h_q3, :h_q4 FROM purchase - WHERE extract(year from biz_date) = :h_yr; - if (sqlca.sqlcode < 0) FAIL(b); + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); - setl(b, T_ARG2, h_q1); - setl(b, T_ARG3, h_q2); - setl(b, T_ARG4, h_q3); - setl(b, T_ARG5, h_q4); - setl(b, T_GROSS, h_q1 + h_q2 + h_q3 + h_q4); + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + k = getl(b, T_ARG1); if (k < 2000 || k > 2999) k = 2026; + snprintf(logmsg, sizeof(logmsg), "%ld-01-01", k); + strncpy(h_d1, logmsg, sizeof(h_d1)-1); h_d1[sizeof(h_d1)-1] = 0; + snprintf(logmsg, sizeof(logmsg), "%ld-12-31", k); + strncpy(h_d2, logmsg, sizeof(h_d2)-1); h_d2[sizeof(h_d2)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } + + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT coalesce(sum(amount) FILTER (WHERE extract(quarter from biz_date) = 1),0), coalesce(sum(amount) FILTER (WHERE extract(quarter from biz_date) = 2),0), coalesce(sum(amount) FILTER (WHERE extract(quarter from biz_date) = 3),0), coalesce(sum(amount) FILTER (WHERE extract(quarter from biz_date) = 4),0) INTO :h_a, :h_b, :h_c, :h_d FROM purchase WHERE biz_date BETWEEN :h_d1::date AND :h_d2::date; + if (sqlca.sqlcode < 0) { + userlog("%s: 분기분해 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_gross FROM purchase WHERE biz_date BETWEEN :h_d1::date AND :h_d2::date; + if (sqlca.sqlcode < 0) { + userlog("%s: 연간총계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 최대 분기를 골라 계절성 편중을 판정 */ + h_max = h_a; h_top = 1; + if (h_b > h_max) { h_max = h_b; h_top = 2; } + if (h_c > h_max) { h_max = h_c; h_top = 3; } + if (h_d > h_max) { h_max = h_d; h_top = 4; } + if (h_gross > 0) ratio = (h_max * 10000L) / h_gross; else ratio = 0; + if (h_gross == 0) { h_val = 0; } + else if (ratio >= 5000) { h_val = 3; } + else { h_val = 1; } + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "분기통계 연도=%ld 1Q=%ld 2Q=%ld 3Q=%ld 4Q=%ld", k, h_a, h_b, h_c, h_d); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_QTRSTAT', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_QTRSTAT' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_AMT1, h_a); + setl(b, T_AMT2, h_b); + setl(b, T_AMT3, h_c); + setl(b, T_AMT4, h_d); + setl(b, T_ARG2, h_top); + setl(b, T_ARG3, ratio); + setl(b, T_ARG4, h_val); + setl(b, T_COUNT, h_cnt); + setl(b, T_GROSS, h_gross); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_REBUY.pgc b/app/src/ac/svc/ACQ_REBUY.pgc index e395e0d..105cb43 100644 --- a/app/src/ac/svc/ACQ_REBUY.pgc +++ b/app/src/ac/svc/ACQ_REBUY.pgc @@ -1,6 +1,6 @@ /* * ACQ_REBUY.pgc - ac 모듈 서비스 ACQ_REBUY (one service = one file). - * 재매입: 취소(C)된 원매입의 가맹점/금액/채널을 승계해 신규 매입을 생성하고 + * 재매입: 취소(C)된 원매입의 가맹점/금액을 승계해 신규 매입을 생성하고 * 현재 MDR 로 수수료를 재산출, 원장에 REBUY 연결 분개를 남긴다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ @@ -9,34 +9,165 @@ void ACQ_REBUY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long newpid, fee, net, bps = 0; - char bizdate[16]; + const char *SVC = "ACQ_REBUY"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_amt = 0; - char h_merch[64], h_chan[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 신규 매입 접수 요청", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + /* 원매입에서 가맹점/금액을 승계한다 */ + EXEC SQL SELECT merchant_id, amount, status INTO :h_merch, :h_amt, :h_prev + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 원매입승계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "원매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + if (strcmp(h_prev, "C") != 0) { Bchg(b, T_MSG, 0, "취소 건만 재매입 가능", 0L); setl(b, T_RC, 165); FAIL(b); } + if (h_amt <= 0) { Bchg(b, T_MSG, 0, "승계 금액 오류", 0L); setl(b, T_RC, 104); FAIL(b); } gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - /* 취소 상태의 원매입만 재매입 대상 */ - EXEC SQL SELECT merchant_id, amount, channel INTO :h_merch, :h_amt, :h_chan - FROM purchase WHERE purchase_id = :h_pid AND status = 'C'; - if (sqlca.sqlcode == 100) { setl(b, T_RC, 5); FAIL(b); } - if (sqlca.sqlcode < 0) FAIL(b); + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; - if (acdb_merchant_mdr(h_merch, &bps) < 0) FAIL(b); - fee = acq_fee(h_amt, bps); - net = h_amt - fee; + /* 3) 업무규칙 분기부: 가맹점 상태/금액 구간/채널 정책(3+ 분기) */ + if (strcmp(h_status, "ACTIVE") != 0) { Bchg(b, T_MSG, 0, "비활성 가맹점", 0L); setl(b, T_RC, 121); FAIL(b); } + /* 재매입 횟수를 세어 반복 재매입을 제한한다 */ + EXEC SQL SELECT count(*) INTO :h_cnt FROM ledger + WHERE purchase_id = :h_pid AND entry_type = 'REBUY'; + if (sqlca.sqlcode < 0) { + userlog("%s: 재매입이력 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_cnt >= 3) { Bchg(b, T_MSG, 0, "재매입 횟수 초과", 0L); setl(b, T_RC, 166); FAIL(b); } + else if (h_cnt >= 1) { h_val = 3; } + else if (h_amt >= 1000000) { h_val = 2; } + else { h_val = 1; } - newpid = acdb_next_purchase_id(); - if (newpid < 0) FAIL(b); - if (acdb_insert_purchase(newpid, h_merch, h_amt, fee, net, "A", h_chan, bizdate) < 0) FAIL(b); - /* 원매입 → 신매입 연결 분개 */ - if (acdb_insert_ledger(h_pid, "REBUY", h_amt, bizdate) < 0) FAIL(b); + /* 4) 본처리부: 수수료 산출 및 매입/승인 원장 생성 */ + bps = h_rate; h_fee = acq_fee(h_amt, bps); h_net = h_amt - h_fee; + /* 원매입과의 연결을 원장 REBUY 분개로 남긴다 */ + EXEC SQL SELECT nextval('ledger_seq') INTO :h_ledid; + if (sqlca.sqlcode < 0) { + userlog("%s: 원장채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) VALUES (:h_ledid, :h_pid, 'REBUY', :h_amt, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 재매입분개 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_new = acdb_next_purchase_id(); + if (h_new < 0) { Bchg(b, T_MSG, 0, "매입 채번 실패", 0L); setl(b, T_RC, 93); FAIL(b); } + if (acdb_insert_purchase(h_new, h_merch, h_amt, h_fee, h_net, "A", "POS", h_bizdate) < 0) + { Bchg(b, T_MSG, 0, "매입 생성 실패", 0L); setl(b, T_RC, 94); FAIL(b); } + if (acdb_insert_approval(h_new, 1) < 0) { Bchg(b, T_MSG, 0, "승인 적재 실패", 0L); setl(b, T_RC, 95); FAIL(b); } + h_aff = 1; h_pid = h_new; - setl(b, T_ID1, newpid); - setl(b, T_FEE, fee); - setl(b, T_NET, net); + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "재매입 원pid=%ld 신규금액=%ld 회차=%ld 등급=%ld", h_pid, h_amt, h_cnt + 1, h_val); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_REBUY', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_REBUY' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_PURCHASE_ID, h_new); + setl(b, T_ID1, h_pid); + setl(b, T_AMOUNT, h_amt); + setl(b, T_FEE, h_fee); + setl(b, T_NET, h_net); + setl(b, T_COUNT, h_cnt + 1); + setl(b, T_ARG1, h_val); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_REBUYAPPR.pgc b/app/src/ac/svc/ACQ_REBUYAPPR.pgc index 88c6833..a26eade 100644 --- a/app/src/ac/svc/ACQ_REBUYAPPR.pgc +++ b/app/src/ac/svc/ACQ_REBUYAPPR.pgc @@ -1,7 +1,7 @@ /* * ACQ_REBUYAPPR.pgc - ac 모듈 서비스 ACQ_REBUYAPPR (one service = one file). - * 재매입 승인: 취소(C)된 원매입에 한해 승인 원장(approval)에 재승인 행을 - * 남기고 원매입을 접수(A)로 되살려 재매입을 개시한다. + * 재매입 승인: 취소(C)된 원매입에 한해 승인 원장에 재승인 행을 남기고 + * 원매입을 접수(A)로 되살려 재매입을 개시한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_REBUYAPPR.h" @@ -9,26 +9,151 @@ void ACQ_REBUYAPPR(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_REBUYAPPR"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_aid; - char st[4]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 매입 상태 전이 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - EXEC SQL SELECT status INTO :st FROM purchase WHERE purchase_id = :h_pid; - if (sqlca.sqlcode == 100) { setl(b, T_RC, 5); FAIL(b); } - if (sqlca.sqlcode < 0) FAIL(b); - if (st[0] != 'C') { setl(b, T_RC, 2); FAIL(b); } + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; - EXEC SQL SELECT nextval('approval_seq') INTO :h_aid; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL INSERT INTO approval (approval_id, purchase_id, approved) VALUES (:h_aid, :h_pid, true); - if (sqlca.sqlcode < 0) FAIL(b); + /* 3) 업무규칙 분기부: 상태 전이 가능여부/유형 판정(3+ 분기) */ + if (!(strcmp(h_prev, "C") == 0)) { + userlog("%s 거절: 상태 %s 에서 전이 불가", SVC, h_prev); + Bchg(b, T_MSG, 0, "상태 전이 불가", 0L); setl(b, T_RC, 120); FAIL(b); + } + /* 취소 사유가 지불거절 계열이면 재매입을 제한한다 */ + EXEC SQL SELECT count(*) INTO :h_cnt FROM ac_cancel + WHERE purchase_id = :h_pid AND cancel_type LIKE 'CB%'; + if (sqlca.sqlcode < 0) { + userlog("%s: 취소사유 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_cnt > 0) { Bchg(b, T_MSG, 0, "지불거절 건은 재매입 불가", 0L); setl(b, T_RC, 124); FAIL(b); } + if (h_gross >= 3000000) { h_val = 3; } + else if (h_gross >= 300000) { h_val = 2; } + else { h_val = 1; } + + /* 4) 본처리부: 부수행 기록 및 상태 갱신 */ + if (acdb_insert_approval(h_pid, 1) < 0) { Bchg(b, T_MSG, 0, "재승인 적재 실패", 0L); setl(b, T_RC, 95); FAIL(b); } EXEC SQL UPDATE purchase SET status = 'A', updated_at = now() WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0) { userlog("ACQ_REBUYAPPR FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + if (sqlca.sqlcode < 0) { + userlog("%s: 상태갱신 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_aff = sqlca.sqlerrd[2]; + if (h_aff > 0) Bchg(b, T_STATUS, 0, "A", 0L); - setl(b, T_ID1, h_aid); + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "재매입승인 pid=%ld 금액=%ld 등급=%ld", h_pid, h_gross, h_val); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_REBUYAPPR', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_REBUYAPPR' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_AMOUNT, h_gross); + setl(b, T_ARG1, h_val); + setl(b, T_ARG2, h_cnt); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_REBUYHIST.pgc b/app/src/ac/svc/ACQ_REBUYHIST.pgc index ac35412..9e09a75 100644 --- a/app/src/ac/svc/ACQ_REBUYHIST.pgc +++ b/app/src/ac/svc/ACQ_REBUYHIST.pgc @@ -1,7 +1,7 @@ /* * ACQ_REBUYHIST.pgc - ac 모듈 서비스 ACQ_REBUYHIST (one service = one file). - * 재매입 이력: 한 매입의 승인 원장을 승인시각 순으로 커서 조회해 승인 ID를 - * occurrence 로 내리고 총 승인 횟수와 최근 승인 여부를 계산한다. + * 재매입 이력: 한 매입의 승인 원장을 집계해 총 승인 횟수와 최근 승인 ID, + * 거절 이력 유무로 재매입 반복성을 계산한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_REBUYHIST.h" @@ -9,29 +9,154 @@ void ACQ_REBUYHIST(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long n = 0; int last = 0; + const char *SVC = "ACQ_REBUYHIST"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_aid; - int h_ok; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ h_pid = getl(b, T_PURCHASE_ID); - - EXEC SQL DECLARE rbh_cur CURSOR FOR - SELECT approval_id, approved FROM approval WHERE purchase_id = :h_pid - ORDER BY approved_at; - EXEC SQL OPEN rbh_cur; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH rbh_cur INTO :h_aid, :h_ok; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE rbh_cur; FAIL(b); } - Badd(b, T_ID1, (char *)&h_aid, 0L); - last = h_ok; n++; + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); } - EXEC SQL CLOSE rbh_cur; - setl(b, T_COUNT, n); - setl(b, T_RC, last ? 0 : 1); + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*), count(*) FILTER (WHERE approved), coalesce(max(approval_id),0) INTO :h_cnt, :h_a, :h_top FROM approval WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 승인이력 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_b FROM ac_unmatch WHERE purchase_id = :h_pid AND reason LIKE 'REBUY%'; + if (sqlca.sqlcode < 0) { + userlog("%s: 재매입거절 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT status, amount INTO :h_status, :h_amt FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입현황 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 승인 반복 횟수와 거절 이력으로 재매입 성격을 분류 */ + if (h_cnt <= 1 && h_b == 0) { h_val = 0; Bchg(b, T_MSG, 0, "재매입 없음", 0L); } + else if (h_b > 0) { h_val = 3; Bchg(b, T_MSG, 0, "재매입 거절 이력", 0L); } + else if (h_cnt >= 3) { h_val = 2; Bchg(b, T_MSG, 0, "재매입 반복", 0L); } + else { h_val = 1; Bchg(b, T_MSG, 0, "재매입 1회", 0L); } + h_e = h_cnt - h_a; + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "재매입이력 pid=%ld 승인=%ld 거절=%ld 반려=%ld", h_pid, h_cnt, h_e, h_b); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_REBUYHIST', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_REBUYHIST' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_ARG1, h_a); + setl(b, T_ARG2, h_e); + setl(b, T_ARG3, h_b); + setl(b, T_ARG4, h_val); + setl(b, T_ID1, h_top); + setl(b, T_AMOUNT, h_amt); + Bchg(b, T_STATUS, 0, h_status, 0L); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_REBUYREJ.pgc b/app/src/ac/svc/ACQ_REBUYREJ.pgc index efe97d2..e796ab6 100644 --- a/app/src/ac/svc/ACQ_REBUYREJ.pgc +++ b/app/src/ac/svc/ACQ_REBUYREJ.pgc @@ -1,7 +1,7 @@ /* * ACQ_REBUYREJ.pgc - ac 모듈 서비스 ACQ_REBUYREJ (one service = one file). * 재매입 거절: 재매입 요청을 반려하고 미매입 원장에 REBUY_REJ 사유로 - * 거절 이력을 남긴다. 반려 사유는 T_REASON 을 덧붙인다. + * 거절 이력을 남긴다. 반려 사유는 요청 사유를 덧붙인다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_REBUYREJ.h" @@ -9,24 +9,157 @@ void ACQ_REBUYREJ(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char why[80]; + const char *SVC = "ACQ_REBUYREJ"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_id, h_pid; - char h_reason[100], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 매입 상태 전이 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ h_pid = getl(b, T_PURCHASE_ID); - gets_(b, T_REASON, why, sizeof(why)); - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - snprintf(h_reason, sizeof(h_reason), "REBUY_REJ:%s", why[0] ? why : "POLICY"); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "일반"); + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + + /* 3) 업무규칙 분기부: 상태 전이 가능여부/유형 판정(3+ 분기) */ + if (!(strcmp(h_prev, "A") == 0 || strcmp(h_prev, "M") == 0 || strcmp(h_prev, "S") == 0 || strcmp(h_prev, "C") == 0 || strcmp(h_prev, "H") == 0 || strcmp(h_prev, "V") == 0 || strcmp(h_prev, "P") == 0)) { + userlog("%s 거절: 상태 %s 에서 전이 불가", SVC, h_prev); + Bchg(b, T_MSG, 0, "상태 전이 불가", 0L); setl(b, T_RC, 120); FAIL(b); + } + /* 기존 반려 횟수로 최종 반려 여부를 판정 */ + EXEC SQL SELECT count(*) INTO :h_cnt FROM ac_unmatch + WHERE purchase_id = :h_pid AND reason LIKE 'REBUY_REJ%'; + if (sqlca.sqlcode < 0) { + userlog("%s: 반려이력 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_cnt >= 3) { h_val = 3; Bchg(b, T_MSG, 0, "최종 반려(재신청 불가)", 0L); } + else if (h_cnt >= 1) { h_val = 2; Bchg(b, T_MSG, 0, "재반려", 0L); } + else { h_val = 1; Bchg(b, T_MSG, 0, "최초 반려", 0L); } + snprintf(logmsg, sizeof(logmsg), "REBUY_REJ:%s", h_reason); + strncpy(h_reason, logmsg, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + + /* 4) 본처리부: 부수행 기록 및 상태 갱신 */ EXEC SQL SELECT nextval('ac_unmatch_seq') INTO :h_id; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL INSERT INTO ac_unmatch (unmatch_id, purchase_id, reason, biz_date) - VALUES (:h_id, :h_pid, :h_reason, :h_bizdate); - if (sqlca.sqlcode < 0) { userlog("ACQ_REBUYREJ FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + if (sqlca.sqlcode < 0) { + userlog("%s: 반려채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO ac_unmatch (unmatch_id, purchase_id, reason, biz_date) VALUES (:h_id, :h_pid, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 반려등록 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_aff = 1; + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "재매입거절 pid=%ld 반려회차=%ld id=%ld", h_pid, h_cnt + 1, h_id); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_REBUYREJ', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_REBUYREJ' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ setl(b, T_ID1, h_id); + setl(b, T_COUNT, h_cnt + 1); + setl(b, T_ARG1, h_val); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_RECLASS.pgc b/app/src/ac/svc/ACQ_RECLASS.pgc index ddffe74..acb6483 100644 --- a/app/src/ac/svc/ACQ_RECLASS.pgc +++ b/app/src/ac/svc/ACQ_RECLASS.pgc @@ -1,6 +1,7 @@ /* * ACQ_RECLASS.pgc - ac 모듈 서비스 ACQ_RECLASS (one service = one file). - * 업종/분류 재지정: category 갱신 후 같은 가맹점에서 동일 분류 건수를 집계 회신. + * 업종/분류 재지정: 매입의 category 를 요청 분류로 갱신하고 같은 가맹점의 + * 동일 분류 건수를 재집계해 회신한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_RECLASS.h" @@ -8,23 +9,152 @@ void ACQ_RECLASS(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char cat[32]; + const char *SVC = "ACQ_RECLASS"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_cnt = 0; - char h_cat[32]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; 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; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); - 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); - /* how many of this merchant's purchases now carry the new 분류 */ - EXEC SQL SELECT count(*) INTO :h_cnt FROM purchase - WHERE merchant_id = (SELECT merchant_id FROM purchase WHERE purchase_id = :h_pid) - AND category = :h_cat; - if (sqlca.sqlcode < 0) FAIL(b); + userlog("%s 진입: 매입 상태 전이 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_CATEGORY, category, sizeof(category)); if (category[0] == 0) strcpy(category, "GEN"); + strncpy(h_category, category, sizeof(h_category)-1); h_category[sizeof(h_category)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + + /* 3) 업무규칙 분기부: 상태 전이 가능여부/유형 판정(3+ 분기) */ + if (!(strcmp(h_prev, "A") == 0 || strcmp(h_prev, "M") == 0 || strcmp(h_prev, "S") == 0 || strcmp(h_prev, "C") == 0 || strcmp(h_prev, "H") == 0 || strcmp(h_prev, "V") == 0 || strcmp(h_prev, "P") == 0)) { + userlog("%s 거절: 상태 %s 에서 전이 불가", SVC, h_prev); + Bchg(b, T_MSG, 0, "상태 전이 불가", 0L); setl(b, T_RC, 120); FAIL(b); + } + /* 정산 완료건의 분류 변경은 제한한다 */ + if (strcmp(h_prev, "S") == 0) { Bchg(b, T_MSG, 0, "정산 완료건은 재분류 불가", 0L); setl(b, T_RC, 136); FAIL(b); } + if (strcmp(h_category, "PROMO") == 0) { h_val = 3; } + else if (strcmp(h_category, "GEN") == 0) { h_val = 1; } + else { h_val = 2; } + + /* 4) 본처리부: 부수행 기록 및 상태 갱신 */ + EXEC SQL UPDATE purchase SET category = :h_category, updated_at = now() WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 분류갱신 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_rem = sqlca.sqlerrd[2]; + EXEC SQL SELECT count(*) INTO :h_cnt FROM purchase WHERE merchant_id = :h_merch AND category = :h_category; + if (sqlca.sqlcode < 0) { + userlog("%s: 동일분류집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_aff = 1; + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "업종재지정 pid=%ld 분류=%s 동일=%ld 등급=%ld", h_pid, h_category, h_cnt, h_val); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_RECLASS', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_RECLASS' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + Bchg(b, T_CATEGORY, 0, h_category, 0L); setl(b, T_COUNT, h_cnt); + setl(b, T_ARG1, h_rem); + setl(b, T_ARG2, h_val); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_REFUND.pgc b/app/src/ac/svc/ACQ_REFUND.pgc index 6273ab6..6ea316c 100644 --- a/app/src/ac/svc/ACQ_REFUND.pgc +++ b/app/src/ac/svc/ACQ_REFUND.pgc @@ -1,28 +1,176 @@ /* * ACQ_REFUND.pgc - ac 모듈 서비스 ACQ_REFUND (one service = one file). - * Moved verbatim from the former inline ac_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by ac_svr at tpsvrinit. + * 매입 환불: 정산 완료 매입에 대한 환불을 접수한다. REFUND 취소행을 + * 남기고 환불 누계가 원금에 도달하면 매입을 취소(C)로 전이한다. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_REFUND.h" void ACQ_REFUND(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], reason[128]; + const char *SVC = "ACQ_REFUND"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_id, h_pid, h_amt; - char h_bizdate[16], h_reason[128]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; 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; + + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 매입 상태 전이 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + h_amt = getl(b, T_AMOUNT); + if (h_amt < 1L) { Bchg(b, T_MSG, 0, "금액 범위 오류", 0L); setl(b, T_RC, 104); FAIL(b); } + gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "일반"); 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); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + + /* 3) 업무규칙 분기부: 상태 전이 가능여부/유형 판정(3+ 분기) */ + if (!(strcmp(h_prev, "M") == 0 || strcmp(h_prev, "S") == 0)) { + userlog("%s 거절: 상태 %s 에서 전이 불가", SVC, h_prev); + Bchg(b, T_MSG, 0, "상태 전이 불가", 0L); setl(b, T_RC, 120); FAIL(b); + } + /* 환불 누계 도달 여부에 따라 상태 전이를 결정 */ + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_sum FROM ac_cancel + WHERE purchase_id = :h_pid AND cancel_type = 'REFUND'; + if (sqlca.sqlcode < 0) { + userlog("%s: 환불누계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_amt <= 0) h_amt = h_gross - h_sum; + if (h_sum + h_amt > h_gross) { Bchg(b, T_MSG, 0, "환불액이 원금 초과", 0L); setl(b, T_RC, 149); FAIL(b); } + h_rem = h_gross - h_sum - h_amt; + if (h_rem == 0) { h_val = 3; } + else if (h_sum > 0) { h_val = 2; } + else { h_val = 1; } + + /* 4) 본처리부: 부수행 기록 및 상태 갱신 */ + EXEC SQL SELECT nextval('ac_cancel_seq') INTO :h_canid; + if (sqlca.sqlcode < 0) { + userlog("%s: 환불채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO ac_cancel (cancel_id, purchase_id, cancel_type, amount, reason, biz_date) VALUES (:h_canid, :h_pid, 'REFUND', :h_amt, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 환불행 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 전액 환불이면 매입 자체를 취소 상태로 확정한다 */ + if (h_rem == 0) { + if (acdb_update_status(h_pid, "C") < 0) { Bchg(b, T_MSG, 0, "상태 전이 실패", 0L); setl(b, T_RC, 96); FAIL(b); } + Bchg(b, T_STATUS, 0, "C", 0L); + } else { + Bchg(b, T_STATUS, 0, h_prev, 0L); + } + h_aff = 1; + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "매입환불 pid=%ld 환불=%ld 누계=%ld 잔여=%ld", h_pid, h_amt, h_sum + h_amt, h_rem); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_REFUND', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_REFUND' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_CANCEL_ID, h_canid); + setl(b, T_AMOUNT, h_amt); + setl(b, T_ARG1, h_rem); + setl(b, T_ARG2, h_val); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_RELEASE.pgc b/app/src/ac/svc/ACQ_RELEASE.pgc index 3bd2655..de1fc63 100644 --- a/app/src/ac/svc/ACQ_RELEASE.pgc +++ b/app/src/ac/svc/ACQ_RELEASE.pgc @@ -1,6 +1,7 @@ /* * ACQ_RELEASE.pgc - ac 모듈 서비스 ACQ_RELEASE (one service = one file). - * 보류 해제: 직전 상태를 조회한 뒤 H->A 전이, 이전 상태를 함께 회신. + * 보류 해제: 보류(H) 상태의 매입을 접수(A)로 되돌린다. 직전 상태를 함께 + * 회신하고 해제 사유를 원장 및 공통로그에 남긴다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_RELEASE.h" @@ -8,20 +9,156 @@ void ACQ_RELEASE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_RELEASE"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_aff; - char h_prev[8]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 매입 상태 전이 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ h_pid = getl(b, T_PURCHASE_ID); - EXEC SQL SELECT status INTO :h_prev FROM purchase WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - /* release only from HOLD; report whether the transition actually fired */ - EXEC SQL UPDATE purchase SET status = 'A', updated_at = now() - WHERE purchase_id = :h_pid AND status = 'H'; - if (sqlca.sqlcode < 0) FAIL(b); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "일반"); + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + + /* 3) 업무규칙 분기부: 상태 전이 가능여부/유형 판정(3+ 분기) */ + if (!(strcmp(h_prev, "H") == 0)) { + userlog("%s 거절: 상태 %s 에서 전이 불가", SVC, h_prev); + Bchg(b, T_MSG, 0, "상태 전이 불가", 0L); setl(b, T_RC, 120); FAIL(b); + } + /* 보류 사유별로 해제 경로를 나눈다 */ + if (h_gross >= 10000000) { h_val = 3; Bchg(b, T_MSG, 0, "고액 보류 해제", 0L); } + else if (strncmp(h_reason, "LIMIT", 5) == 0) { h_val = 2; Bchg(b, T_MSG, 0, "한도 보류 해제", 0L); } + else { h_val = 1; Bchg(b, T_MSG, 0, "일반 보류 해제", 0L); } + + /* 4) 본처리부: 부수행 기록 및 상태 갱신 */ + EXEC SQL SELECT nextval('ledger_seq') INTO :h_ledid; + if (sqlca.sqlcode < 0) { + userlog("%s: 원장채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) VALUES (:h_ledid, :h_pid, 'RELEASE', 0, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 해제분개 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL UPDATE purchase SET status = 'A', updated_at = now() WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 상태갱신 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } h_aff = sqlca.sqlerrd[2]; - Bchg(b, T_STATUS, 0, h_aff > 0 ? "A" : h_prev, 0L); - setl(b, T_RC, h_aff > 0 ? 0 : 1); + if (h_aff > 0) Bchg(b, T_STATUS, 0, "A", 0L); + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "보류해제 pid=%ld 직전=%s 사유=%s 등급=%ld", h_pid, h_prev, h_reason, h_val); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_RELEASE', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_RELEASE' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + Bchg(b, T_STR1, 0, h_prev, 0L); + setl(b, T_ARG1, h_val); + setl(b, T_ID1, h_ledid); + setl(b, T_AMOUNT, h_gross); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_REMATCH.pgc b/app/src/ac/svc/ACQ_REMATCH.pgc index 7ad3a4a..07543f6 100644 --- a/app/src/ac/svc/ACQ_REMATCH.pgc +++ b/app/src/ac/svc/ACQ_REMATCH.pgc @@ -9,35 +9,162 @@ void ACQ_REMATCH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_REMATCH"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_appr = 0, h_gone, h_up; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 매입 상태 전이 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - /* 유효 승인 존재 여부 재확인 */ - EXEC SQL SELECT count(*) INTO :h_appr FROM approval - WHERE purchase_id = :h_pid AND approved = true; - if (sqlca.sqlcode < 0) FAIL(b); + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; - if (h_appr == 0) { - Bchg(b, T_MSG, 0, "NO-APPROVAL", 0L); - setl(b, T_RC, 1); - OK(b); /* 승인 미도착: 미매입 유지 */ + /* 3) 업무규칙 분기부: 상태 전이 가능여부/유형 판정(3+ 분기) */ + if (!(strcmp(h_prev, "A") == 0 || strcmp(h_prev, "H") == 0)) { + userlog("%s 거절: 상태 %s 에서 전이 불가", SVC, h_prev); + Bchg(b, T_MSG, 0, "상태 전이 불가", 0L); setl(b, T_RC, 120); FAIL(b); + } + /* 유효 승인 존재 여부가 재매칭의 전제 */ + EXEC SQL SELECT count(*) INTO :h_cnt FROM approval + WHERE purchase_id = :h_pid AND approved; + if (sqlca.sqlcode < 0) { + userlog("%s: 승인확인 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_cnt == 0) { Bchg(b, T_MSG, 0, "유효 승인 없음(재매칭 불가)", 0L); setl(b, T_RC, 134); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_a FROM ac_unmatch WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 미매입확인 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_a == 0) { h_val = 1; } + else if (h_a >= 3) { h_val = 3; } + else { h_val = 2; } + + /* 4) 본처리부: 부수행 기록 및 상태 갱신 */ + EXEC SQL DELETE FROM ac_unmatch WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 미매입해소 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_rem = sqlca.sqlerrd[2]; + EXEC SQL UPDATE purchase SET status = 'M', updated_at = now() WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 상태갱신 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_aff = sqlca.sqlerrd[2]; + if (h_aff > 0) Bchg(b, T_STATUS, 0, "M", 0L); + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "재매칭 pid=%ld 승인=%ld 미매입=%ld 해소=%ld", h_pid, h_cnt, h_a, h_rem); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_REMATCH', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); } - EXEC SQL DELETE FROM ac_unmatch - WHERE purchase_id = :h_pid AND reason NOT LIKE 'CLAIM:%'; - if (sqlca.sqlcode < 0) FAIL(b); - h_gone = sqlca.sqlerrd[2]; + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_REMATCH' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); - EXEC SQL UPDATE purchase SET status = 'M', updated_at = now() - WHERE purchase_id = :h_pid AND status = 'A'; - if (sqlca.sqlcode < 0) { userlog("ACQ_REMATCH FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } - h_up = sqlca.sqlerrd[2]; - - setl(b, T_ARG1, h_gone); /* 해소된 미매입 행 수 */ - setl(b, T_ARG2, h_up); /* 상태 전이 건수 */ + /* 8) 응답부 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_ARG1, h_a); + setl(b, T_ARG2, h_rem); + setl(b, T_ARG3, h_val); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_REPROC.pgc b/app/src/ac/svc/ACQ_REPROC.pgc index 9ba2e16..cee5c16 100644 --- a/app/src/ac/svc/ACQ_REPROC.pgc +++ b/app/src/ac/svc/ACQ_REPROC.pgc @@ -1,15 +1,160 @@ /* * ACQ_REPROC.pgc - ac 모듈 서비스 ACQ_REPROC (one service = one file). - * Moved verbatim from the former inline ac_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by ac_svr at tpsvrinit. + * 매입 재처리: 취소/무효로 종료된 매입을 접수(A) 상태로 되살려 재처리 + * 큐에 태운다. 재처리 횟수를 승인 원장 건수로 확인해 과다 재처리를 막는다. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_REPROC.h" 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); + const char *SVC = "ACQ_REPROC"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; + EXEC SQL BEGIN DECLARE SECTION; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; + EXEC SQL END DECLARE SECTION; + + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 매입 상태 전이 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "일반"); + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + + /* 3) 업무규칙 분기부: 상태 전이 가능여부/유형 판정(3+ 분기) */ + if (!(strcmp(h_prev, "C") == 0 || strcmp(h_prev, "V") == 0 || strcmp(h_prev, "H") == 0)) { + userlog("%s 거절: 상태 %s 에서 전이 불가", SVC, h_prev); + Bchg(b, T_MSG, 0, "상태 전이 불가", 0L); setl(b, T_RC, 120); FAIL(b); + } + /* 기존 승인 횟수로 재처리 허용 여부를 판정 */ + EXEC SQL SELECT count(*) INTO :h_cnt FROM approval WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 승인이력 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_cnt >= 5) { Bchg(b, T_MSG, 0, "재처리 횟수 초과", 0L); setl(b, T_RC, 122); FAIL(b); } + else if (h_cnt >= 3) { h_val = 3; } + else if (h_cnt >= 1) { h_val = 2; } + else { h_val = 1; } + + /* 4) 본처리부: 부수행 기록 및 상태 갱신 */ + if (acdb_insert_approval(h_pid, 1) < 0) { Bchg(b, T_MSG, 0, "재승인 실패", 0L); setl(b, T_RC, 95); FAIL(b); } + EXEC SQL UPDATE purchase SET status = 'A', updated_at = now() WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 상태갱신 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_aff = sqlca.sqlerrd[2]; + if (h_aff > 0) Bchg(b, T_STATUS, 0, "A", 0L); + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "매입재처리 pid=%ld 직전=%s 재처리회차=%ld", h_pid, h_prev, h_cnt); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_REPROC', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_REPROC' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_ARG1, h_val); + Bchg(b, T_STR1, 0, h_prev, 0L); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_RESERVE.pgc b/app/src/ac/svc/ACQ_RESERVE.pgc index 3a35bd9..bdb69a4 100644 --- a/app/src/ac/svc/ACQ_RESERVE.pgc +++ b/app/src/ac/svc/ACQ_RESERVE.pgc @@ -1,7 +1,7 @@ /* * ACQ_RESERVE.pgc - ac 모듈 서비스 ACQ_RESERVE (one service = one file). * 매입 예약: 활성(ACTIVE) 가맹점에 대해 미래 영업일 예약 매입(status P)을 - * 만들어 둔다. 수수료는 현행 MDR 로 선산출하고 예약 채널(RSV)로 표시. + * 만들어 둔다. 수수료는 현행 MDR 로 선산출하고 예약 채널(RSV)로 표시한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_RESERVE.h" @@ -9,33 +9,154 @@ void ACQ_RESERVE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_RESERVE"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_amt, h_fee, h_net, h_bps = 0; - char h_merch[64], h_mst[16], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 신규 매입 접수 요청", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + if (merch[0] == 0) { Bchg(b, T_MSG, 0, "가맹점 ID 누락", 0L); setl(b, T_RC, 101); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; h_amt = getl(b, T_AMOUNT); - gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - if (h_amt <= 0) { setl(b, T_RC, 3); FAIL(b); } + if (h_amt < 1L) { Bchg(b, T_MSG, 0, "금액 범위 오류", 0L); setl(b, T_RC, 104); FAIL(b); } + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - /* 활성 가맹점만 예약 허용 + MDR 동시 조회 */ - EXEC SQL SELECT status, mdr_bps INTO :h_mst, :h_bps FROM merchant WHERE merchant_id = :h_merch; - if (sqlca.sqlcode == 100) { setl(b, T_RC, 5); FAIL(b); } - if (sqlca.sqlcode < 0) FAIL(b); - if (strcmp(h_mst, "ACTIVE") != 0) { setl(b, T_RC, 6); FAIL(b); } + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; - h_fee = acq_fee(h_amt, h_bps); - h_net = h_amt - h_fee; + /* 3) 업무규칙 분기부: 가맹점 상태/금액 구간/채널 정책(3+ 분기) */ + if (strcmp(h_status, "ACTIVE") != 0) { Bchg(b, T_MSG, 0, "비활성 가맹점", 0L); setl(b, T_RC, 121); FAIL(b); } + /* 예약일은 반드시 오늘 이후여야 하며 대기 예약 수를 제한한다 */ + EXEC SQL SELECT count(*) INTO :h_cnt FROM purchase + WHERE merchant_id = :h_merch AND status = 'P'; + if (sqlca.sqlcode < 0) { + userlog("%s: 예약대기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_cnt >= 20) { Bchg(b, T_MSG, 0, "예약 대기 한도 초과", 0L); setl(b, T_RC, 164); FAIL(b); } + else if (h_cnt >= 10) { h_val = 3; } + else if (h_cnt >= 3) { h_val = 2; } + else { h_val = 1; } - EXEC SQL SELECT nextval('purchase_seq') INTO :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL INSERT INTO purchase (purchase_id, merchant_id, amount, fee, net, status, biz_date, channel) - VALUES (:h_pid, :h_merch, :h_amt, :h_fee, :h_net, 'P', :h_bizdate, 'RSV'); - if (sqlca.sqlcode < 0) { userlog("ACQ_RESERVE FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + /* 4) 본처리부: 수수료 산출 및 매입/승인 원장 생성 */ + bps = h_rate; h_fee = acq_fee(h_amt, bps); h_net = h_amt - h_fee; + /* 예약 영업일: 요청 영업일의 다음 영업일로 선정 */ + if (acq_next_bizday(h_bizdate, logmsg) == 0) { + strncpy(h_d1, logmsg, sizeof(h_d1)-1); h_d1[sizeof(h_d1)-1] = 0; + } else { + strncpy(h_d1, h_bizdate, sizeof(h_d1)-1); h_d1[sizeof(h_d1)-1] = 0; + } + strncpy(h_bizdate, h_d1, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + h_new = acdb_next_purchase_id(); + if (h_new < 0) { Bchg(b, T_MSG, 0, "매입 채번 실패", 0L); setl(b, T_RC, 93); FAIL(b); } + if (acdb_insert_purchase(h_new, h_merch, h_amt, h_fee, h_net, "P", "RSV", h_bizdate) < 0) + { Bchg(b, T_MSG, 0, "매입 생성 실패", 0L); setl(b, T_RC, 94); FAIL(b); } + if (acdb_insert_approval(h_new, 1) < 0) { Bchg(b, T_MSG, 0, "승인 적재 실패", 0L); setl(b, T_RC, 95); FAIL(b); } + h_aff = 1; h_pid = h_new; - setl(b, T_PURCHASE_ID, h_pid); + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "매입예약 merch=%s 예약일=%s 금액=%ld 대기=%ld", h_merch, h_d1, h_amt, h_cnt); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_RESERVE', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_RESERVE' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_PURCHASE_ID, h_new); + Bchg(b, T_BIZDATE, 0, h_d1, 0L); setl(b, T_FEE, h_fee); + setl(b, T_NET, h_net); + setl(b, T_COUNT, h_cnt); + setl(b, T_ARG1, h_val); + Bchg(b, T_STATUS, 0, "P", 0L); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_RESERVECXL.pgc b/app/src/ac/svc/ACQ_RESERVECXL.pgc index 30c6e29..ee5694b 100644 --- a/app/src/ac/svc/ACQ_RESERVECXL.pgc +++ b/app/src/ac/svc/ACQ_RESERVECXL.pgc @@ -9,23 +9,162 @@ void ACQ_RESERVECXL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_RESERVECXL"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_amt = 0, h_del; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 매입 상태 전이 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "일반"); + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - EXEC SQL SELECT amount INTO :h_amt FROM purchase - WHERE purchase_id = :h_pid AND status = 'P'; - if (sqlca.sqlcode == 100) { setl(b, T_RC, 4); FAIL(b); } /* 예약 아님/전환됨 */ - if (sqlca.sqlcode < 0) FAIL(b); + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + /* 3) 업무규칙 분기부: 상태 전이 가능여부/유형 판정(3+ 분기) */ + if (!(strcmp(h_prev, "P") == 0)) { + userlog("%s 거절: 상태 %s 에서 전이 불가", SVC, h_prev); + Bchg(b, T_MSG, 0, "상태 전이 불가", 0L); setl(b, T_RC, 120); FAIL(b); + } + /* 예약일 도래 여부로 취소 등급을 나눈다 */ + EXEC SQL SELECT biz_date::text INTO :h_d1 FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 예약일 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (strcmp(h_d1, h_bizdate) < 0) { h_val = 3; Bchg(b, T_MSG, 0, "도래 예약 취소", 0L); } + else if (strcmp(h_d1, h_bizdate) == 0) { h_val = 2; Bchg(b, T_MSG, 0, "당일 예약 취소", 0L); } + else { h_val = 1; Bchg(b, T_MSG, 0, "미래 예약 취소", 0L); } + h_amt = h_gross; + + /* 4) 본처리부: 부수행 기록 및 상태 갱신 */ + EXEC SQL SELECT nextval('ledger_seq') INTO :h_ledid; + if (sqlca.sqlcode < 0) { + userlog("%s: 원장채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) VALUES (:h_ledid, :h_pid, 'RSVCXL', :h_amt, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 예약취소분개 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } EXEC SQL DELETE FROM purchase WHERE purchase_id = :h_pid AND status = 'P'; - if (sqlca.sqlcode < 0) { userlog("ACQ_RESERVECXL FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } - h_del = sqlca.sqlerrd[2]; + if (sqlca.sqlcode < 0) { + userlog("%s: 예약삭제 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_rem = sqlca.sqlerrd[2]; + h_aff = 1; - userlog("ACQ_RESERVECXL: pid=%ld amount=%ld deleted=%ld", h_pid, h_amt, h_del); + /* 6) 집계/요약 갱신부 */ + /* 남은 예약 건수를 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM purchase WHERE merchant_id = :h_merch AND status = 'P'; + if (sqlca.sqlcode < 0) { + userlog("%s: 예약재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "예약취소 pid=%ld 예약일=%s 금액=%ld 삭제=%ld", h_pid, h_d1, h_amt, h_rem); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_RESERVECXL', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_RESERVECXL' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ setl(b, T_AMOUNT, h_amt); - setl(b, T_RC, h_del > 0 ? 0 : 1); + setl(b, T_ARG1, h_rem); + setl(b, T_COUNT, h_cnt); + setl(b, T_GROSS, h_sum); + setl(b, T_ARG2, h_val); + setl(b, T_ID1, h_ledid); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_RESERVEQRY.pgc b/app/src/ac/svc/ACQ_RESERVEQRY.pgc index 221b55e..9fef50a 100644 --- a/app/src/ac/svc/ACQ_RESERVEQRY.pgc +++ b/app/src/ac/svc/ACQ_RESERVEQRY.pgc @@ -1,7 +1,7 @@ /* * ACQ_RESERVEQRY.pgc - ac 모듈 서비스 ACQ_RESERVEQRY (one service = one file). - * 매입 예약 조회: 가맹점의 대기 중 예약(P)을 예약일 순 커서로 훑어 건수와 - * 총액, 가장 빠른 예약일을 알려준다. 예약이 없으면 T_RC=1. + * 매입 예약 조회: 가맹점의 대기 중 예약(P)을 집계해 건수와 총액, + * 가장 빠른 예약일과 예약 채널 분포를 알려준다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_RESERVEQRY.h" @@ -9,34 +9,161 @@ void ACQ_RESERVEQRY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long cnt = 0, total = 0; - char first_date[16]; + const char *SVC = "ACQ_RESERVEQRY"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_amt; - char h_merch[64], h_date[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - first_date[0] = 0; - gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL DECLARE rvq_cur CURSOR FOR - SELECT amount, to_char(biz_date, 'YYYY-MM-DD') FROM purchase - WHERE merchant_id = :h_merch AND status = 'P' - ORDER BY biz_date, purchase_id; - EXEC SQL OPEN rvq_cur; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH rvq_cur INTO :h_amt, :h_date; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE rvq_cur; FAIL(b); } - if (cnt == 0) { strncpy(first_date, h_date, sizeof(first_date)-1); first_date[sizeof(first_date)-1] = 0; } - cnt++; total += h_amt; + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + if (merch[0] == 0) { Bchg(b, T_MSG, 0, "가맹점 ID 누락", 0L); setl(b, T_RC, 101); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); } - EXEC SQL CLOSE rvq_cur; + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } - if (first_date[0]) Bchg(b, T_BIZDATE, 0, first_date, 0L); - setl(b, T_COUNT, cnt); - setl(b, T_GROSS, total); - setl(b, T_RC, cnt > 0 ? 0 : 1); + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + EXEC SQL SELECT name, status, daily_limit INTO :h_str1, :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (strcmp(h_status, "ACTIVE") != 0) + userlog("%s 주의: 가맹점 %s 상태=%s", SVC, h_merch, h_status); + /* 가맹점 MDR(bps) 및 상태분포 컨텍스트 로드 */ + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + EXEC SQL SELECT count(*) FILTER (WHERE status IN ('A','M')), count(*) FILTER (WHERE status = 'S') INTO :h_openc, :h_setc FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 상태분포 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산 진행 컨텍스트(기 정산 순액) */ + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_ctx FROM settlement WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(min(biz_date::text),'') INTO :h_cnt, :h_gross, :h_d1 FROM purchase WHERE merchant_id = :h_merch AND status = 'P'; + if (sqlca.sqlcode < 0) { + userlog("%s: 예약집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_a FROM purchase WHERE merchant_id = :h_merch AND status = 'P' AND biz_date <= :h_bizdate::date; + if (sqlca.sqlcode < 0) { + userlog("%s: 도래예약 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 도래한 예약 비중으로 전환 필요도를 판정 */ + if (h_cnt > 0) ratio = (h_a * 10000L) / h_cnt; else ratio = 0; + if (h_cnt == 0) { h_val = 1; Bchg(b, T_MSG, 0, "예약 없음", 0L); } + else if (h_a > 0) { h_val = 3; Bchg(b, T_MSG, 0, "전환 대상 예약 존재", 0L); } + else if (h_cnt >= 5) { h_val = 2; Bchg(b, T_MSG, 0, "예약 다수 대기", 0L); } + else { h_val = 0; Bchg(b, T_MSG, 0, "예약 대기", 0L); } + h_avg = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :h_val2 FROM merchant_summary WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 누적집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_pdgross FROM purchase WHERE merchant_id = :h_merch AND biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_val2 > 0 && h_gross > 0) ratio = (h_gross * 10000L) / (h_val2 + h_gross); + else ratio = 0; + h_share = (h_pdgross > 0) ? (h_gross * 100L / h_pdgross) : 0; + userlog("%s 추이: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_pdgross, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "예약조회 merch=%s 건수=%ld 총액=%ld 최단=%s", h_merch, h_cnt, h_gross, h_d1); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_RESERVEQRY', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_RESERVEQRY' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_GROSS, h_gross); + Bchg(b, T_BIZDATE, 0, h_d1, 0L); + setl(b, T_ARG1, h_a); + setl(b, T_ARG2, ratio); + setl(b, T_ARG3, h_val); + setl(b, T_AMT1, h_avg); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_RETURN.pgc b/app/src/ac/svc/ACQ_RETURN.pgc index b3381c5..98856dc 100644 --- a/app/src/ac/svc/ACQ_RETURN.pgc +++ b/app/src/ac/svc/ACQ_RETURN.pgc @@ -9,37 +9,155 @@ void ACQ_RETURN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char why[110]; + const char *SVC = "ACQ_RETURN"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_cid, h_amt = 0, h_aff; - char h_why[128], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 매입 상태 전이 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ h_pid = getl(b, T_PURCHASE_ID); - gets_(b, T_REASON, why, sizeof(why)); - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - snprintf(h_why, sizeof(h_why), "RETURN:%s", why[0] ? why : "FORMAT"); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "일반"); + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - EXEC SQL SELECT amount INTO :h_amt FROM purchase - WHERE purchase_id = :h_pid AND status = 'A'; - if (sqlca.sqlcode == 100) { setl(b, T_RC, 5); FAIL(b); } - if (sqlca.sqlcode < 0) FAIL(b); + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; - /* 접수 상태에서만 반송 전이 허용 */ - EXEC SQL UPDATE purchase SET status = 'C', updated_at = now() - WHERE purchase_id = :h_pid AND status = 'A'; - if (sqlca.sqlcode < 0) FAIL(b); + /* 3) 업무규칙 분기부: 상태 전이 가능여부/유형 판정(3+ 분기) */ + if (!(strcmp(h_prev, "A") == 0 || strcmp(h_prev, "M") == 0)) { + userlog("%s 거절: 상태 %s 에서 전이 불가", SVC, h_prev); + Bchg(b, T_MSG, 0, "상태 전이 불가", 0L); setl(b, T_RC, 120); FAIL(b); + } + /* 반송 시점(대사 전/후)에 따라 처리 등급을 나눈다 */ + if (strcmp(h_prev, "M") == 0) { h_val = 3; Bchg(b, T_MSG, 0, "대사후 반송", 0L); } + else if (h_gross >= 1000000) { h_val = 2; Bchg(b, T_MSG, 0, "고액 반송", 0L); } + else { h_val = 1; Bchg(b, T_MSG, 0, "일반 반송", 0L); } + + /* 4) 본처리부: 부수행 기록 및 상태 갱신 */ + EXEC SQL SELECT nextval('ac_cancel_seq') INTO :h_canid; + if (sqlca.sqlcode < 0) { + userlog("%s: 취소채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO ac_cancel (cancel_id, purchase_id, cancel_type, amount, reason, biz_date) VALUES (:h_canid, :h_pid, 'RETURN', :h_gross, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 반송행 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL UPDATE purchase SET status = 'C', updated_at = now() WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 상태갱신 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } h_aff = sqlca.sqlerrd[2]; - if (h_aff == 0) { setl(b, T_RC, 1); FAIL(b); } + if (h_aff > 0) Bchg(b, T_STATUS, 0, "C", 0L); - EXEC SQL SELECT nextval('ac_cancel_seq') INTO :h_cid; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL INSERT INTO ac_cancel (cancel_id, purchase_id, cancel_type, amount, reason, biz_date) - VALUES (:h_cid, :h_pid, 'RETURN', :h_amt, :h_why, :h_bizdate); - if (sqlca.sqlcode < 0) { userlog("ACQ_RETURN FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); - setl(b, T_CANCEL_ID, h_cid); - setl(b, T_AMOUNT, h_amt); + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "매입반송 pid=%ld 금액=%ld 직전=%s 등급=%ld", h_pid, h_gross, h_prev, h_val); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_RETURN', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_RETURN' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_CANCEL_ID, h_canid); + setl(b, T_AMOUNT, h_gross); + setl(b, T_ARG1, h_val); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_RETURNQRY.pgc b/app/src/ac/svc/ACQ_RETURNQRY.pgc index 04b9f4c..54f9321 100644 --- a/app/src/ac/svc/ACQ_RETURNQRY.pgc +++ b/app/src/ac/svc/ACQ_RETURNQRY.pgc @@ -1,7 +1,7 @@ /* * ACQ_RETURNQRY.pgc - ac 모듈 서비스 ACQ_RETURNQRY (one service = one file). * 매입 반품 조회: 가맹점의 영업일 반품/교환(RETURN·EXCHANGE) 취소 이력을 - * 커서로 조회해 취소 ID·금액 쌍을 occurrence 로 내리고 총 반품액을 합산한다. + * 집계해 총 반품액과 최근 취소 ID를 응답한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_RETURNQRY.h" @@ -9,31 +9,162 @@ void ACQ_RETURNQRY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long n = 0, tot = 0; + const char *SVC = "ACQ_RETURNQRY"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_cid, h_amt; - char h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL DECLARE rtq_cur CURSOR FOR - SELECT cancel_id, amount FROM ac_cancel - WHERE biz_date = :h_bizdate AND cancel_type IN ('RETURN', 'EXCHANGE') - ORDER BY cancel_id; - EXEC SQL OPEN rtq_cur; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH rtq_cur INTO :h_cid, :h_amt; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE rtq_cur; FAIL(b); } - if (n < 8) { Badd(b, T_CANCEL_ID, (char *)&h_cid, 0L); Badd(b, T_AMT1, (char *)&h_amt, 0L); } - n++; tot += h_amt; + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + if (merch[0] == 0) { Bchg(b, T_MSG, 0, "가맹점 ID 누락", 0L); setl(b, T_RC, 101); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); } - EXEC SQL CLOSE rtq_cur; + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } - setl(b, T_COUNT, n); - setl(b, T_GROSS, tot); - setl(b, T_RC, n > 0 ? 0 : 1); + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + EXEC SQL SELECT name, status, daily_limit INTO :h_str1, :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (strcmp(h_status, "ACTIVE") != 0) + userlog("%s 주의: 가맹점 %s 상태=%s", SVC, h_merch, h_status); + /* 가맹점 MDR(bps) 및 상태분포 컨텍스트 로드 */ + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + EXEC SQL SELECT count(*) FILTER (WHERE status IN ('A','M')), count(*) FILTER (WHERE status = 'S') INTO :h_openc, :h_setc FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 상태분포 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산 진행 컨텍스트(기 정산 순액) */ + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_ctx FROM settlement WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*), coalesce(sum(c.amount),0), coalesce(max(c.cancel_id),0) INTO :h_cnt, :h_gross, :h_top FROM ac_cancel c JOIN purchase p ON p.purchase_id = c.purchase_id WHERE p.merchant_id = :h_merch AND c.biz_date = :h_bizdate AND c.cancel_type IN ('RETURN','EXCHANGE'); + if (sqlca.sqlcode < 0) { + userlog("%s: 반품집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*) FILTER (WHERE c.cancel_type = 'RETURN'), count(*) FILTER (WHERE c.cancel_type = 'EXCHANGE') INTO :h_a, :h_b FROM ac_cancel c JOIN purchase p ON p.purchase_id = c.purchase_id WHERE p.merchant_id = :h_merch AND c.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 유형분해 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 반품/교환 구성비로 처리 성격을 분류 */ + if (h_cnt > 0) ratio = (h_a * 10000L) / h_cnt; else ratio = 0; + if (h_cnt == 0) { h_val = 0; } + else if (h_b > h_a) { h_val = 2; } + else if (ratio >= 8000) { h_val = 3; } + else { h_val = 1; } + h_avg = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :h_val2 FROM merchant_summary WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 누적집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_pdgross FROM purchase WHERE merchant_id = :h_merch AND biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_val2 > 0 && h_gross > 0) ratio = (h_gross * 10000L) / (h_val2 + h_gross); + else ratio = 0; + h_share = (h_pdgross > 0) ? (h_gross * 100L / h_pdgross) : 0; + userlog("%s 추이: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_pdgross, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "반품조회 merch=%s 건수=%ld 금액=%ld 반품=%ld 교환=%ld", h_merch, h_cnt, h_gross, h_a, h_b); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_RETURNQRY', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_RETURNQRY' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_GROSS, h_gross); + setl(b, T_CANCEL_ID, h_top); + setl(b, T_ARG1, h_a); + setl(b, T_ARG2, h_b); + setl(b, T_ARG3, ratio); + setl(b, T_ARG4, h_val); + setl(b, T_AMT1, h_avg); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_RETURNSTAT.pgc b/app/src/ac/svc/ACQ_RETURNSTAT.pgc index 6d36096..79a942d 100644 --- a/app/src/ac/svc/ACQ_RETURNSTAT.pgc +++ b/app/src/ac/svc/ACQ_RETURNSTAT.pgc @@ -1,7 +1,7 @@ /* * ACQ_RETURNSTAT.pgc - ac 모듈 서비스 ACQ_RETURNSTAT (one service = one file). - * 매입 반품 통계: 영업일 취소 원장을 반품/교환/환불 유형별로 한 번에 분해 - * 집계해 유형별 건수와 금액을 산출하고 전체 반품 금액을 함께 낸다. + * 매입 반품 통계: 영업일 취소 원장을 반품/교환/환불 유형별로 분해 집계해 + * 유형별 건수와 금액을 산출하고 전체 반품 금액을 함께 낸다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_RETURNSTAT.h" @@ -9,23 +9,152 @@ void ACQ_RETURNSTAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_RETURNSTAT"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_rn = 0, h_ex = 0, h_rf = 0, h_amt = 0; - char h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL SELECT count(*) FILTER (WHERE cancel_type = 'RETURN'), - count(*) FILTER (WHERE cancel_type = 'EXCHANGE'), - count(*) FILTER (WHERE cancel_type = 'REFUND'), - coalesce(sum(amount) FILTER (WHERE cancel_type IN ('RETURN','EXCHANGE','REFUND')),0) - INTO :h_rn, :h_ex, :h_rf, :h_amt FROM ac_cancel WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); - setl(b, T_ARG1, h_rn); - setl(b, T_ARG2, h_ex); - setl(b, T_ARG3, h_rf); - setl(b, T_GROSS, h_amt); + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } + + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*) FILTER (WHERE cancel_type = 'RETURN'), count(*) FILTER (WHERE cancel_type = 'EXCHANGE'), count(*) FILTER (WHERE cancel_type = 'REFUND'), count(*) INTO :h_a, :h_b, :h_c, :h_cnt FROM ac_cancel WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 유형건수 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(sum(amount) FILTER (WHERE cancel_type = 'RETURN'),0), coalesce(sum(amount) FILTER (WHERE cancel_type = 'EXCHANGE'),0), coalesce(sum(amount) FILTER (WHERE cancel_type = 'REFUND'),0), coalesce(sum(amount),0) INTO :h_val, :h_val2, :h_sum, :h_gross FROM ac_cancel WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 유형금액 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 최대 유형을 판정하고 환불 비중으로 손실 성격을 본다 */ + h_max = h_a; h_top = 1; + if (h_b > h_max) { h_max = h_b; h_top = 2; } + if (h_c > h_max) { h_max = h_c; h_top = 3; } + if (h_gross > 0) ratio = (h_sum * 10000L) / h_gross; else ratio = 0; + if (h_cnt == 0) { h_e = 0; } + else if (ratio >= 5000) { h_e = 3; } + else { h_e = 1; } + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "반품통계 bizdate=%s 반품=%ld 교환=%ld 환불=%ld 총액=%ld", h_bizdate, h_a, h_b, h_c, h_gross); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_RETURNSTAT', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_RETURNSTAT' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_ARG1, h_a); + setl(b, T_ARG2, h_b); + setl(b, T_ARG3, h_c); + setl(b, T_AMT1, h_val); + setl(b, T_AMT2, h_val2); + setl(b, T_AMT3, h_sum); + setl(b, T_ARG4, h_top); + setl(b, T_ARG5, ratio); + setl(b, T_ARG6, h_e); + setl(b, T_COUNT, h_cnt); + setl(b, T_GROSS, h_gross); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_SETTLEDUE.pgc b/app/src/ac/svc/ACQ_SETTLEDUE.pgc index 5e377bb..b7cbaf2 100644 --- a/app/src/ac/svc/ACQ_SETTLEDUE.pgc +++ b/app/src/ac/svc/ACQ_SETTLEDUE.pgc @@ -9,28 +9,153 @@ void ACQ_SETTLEDUE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char nextday[16]; + const char *SVC = "ACQ_SETTLEDUE"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt = 0, h_due = 0; - char h_merch[64], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL SELECT count(*), coalesce(sum(u.net), 0) - INTO :h_cnt, :h_due - FROM purchase u - WHERE u.merchant_id = :h_merch AND u.biz_date = :h_bizdate AND u.status = 'M' - AND NOT EXISTS (SELECT 1 FROM settlement s WHERE s.purchase_id = u.purchase_id); - if (sqlca.sqlcode < 0) { userlog("ACQ_SETTLEDUE FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); - /* 정산예정일 = 다음 영업일 (주말 건너뜀) */ - if (acq_next_bizday(h_bizdate, nextday) == 0) - Bchg(b, T_STR1, 0, nextday, 0L); + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*), coalesce(sum(p.net),0), coalesce(sum(p.amount),0) INTO :h_cnt, :h_net, :h_gross FROM purchase p WHERE p.biz_date = :h_bizdate AND p.status = 'M' AND NOT EXISTS (SELECT 1 FROM settlement s WHERE s.purchase_id = p.purchase_id); + if (sqlca.sqlcode < 0) { + userlog("%s: 정산예정 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_a, :h_b FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 기정산 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(DISTINCT merchant_id) INTO :h_n FROM purchase WHERE biz_date = :h_bizdate AND status = 'M'; + if (sqlca.sqlcode < 0) { + userlog("%s: 대상가맹점 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 예정 대비 기정산 진행률로 정산 준비도를 판정 */ + h_e = h_cnt + h_a; + if (h_e > 0) ratio = (h_a * 10000L) / h_e; else ratio = 0; + if (h_cnt == 0) { h_val = 0; Bchg(b, T_MSG, 0, "정산예정 없음", 0L); } + else if (ratio >= 8000) { h_val = 1; Bchg(b, T_MSG, 0, "정산 대부분 완료", 0L); } + else if (ratio > 0) { h_val = 2; Bchg(b, T_MSG, 0, "정산 진행중", 0L); } + else { h_val = 3; Bchg(b, T_MSG, 0, "정산 미착수", 0L); } + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "정산예정 bizdate=%s 예정=%ld 예정액=%ld 기정산=%ld", h_bizdate, h_cnt, h_net, h_a); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_SETTLEDUE', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_SETTLEDUE' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ setl(b, T_COUNT, h_cnt); - setl(b, T_NET, h_due); - setl(b, T_RC, h_cnt > 0 ? 0 : 1); + setl(b, T_NET, h_net); + setl(b, T_GROSS, h_gross); + setl(b, T_ARG1, h_a); + setl(b, T_AMT1, h_b); + setl(b, T_ARG2, ratio); + setl(b, T_ARG3, h_val); + setl(b, T_ID1, h_n); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_SETTLEEXP.pgc b/app/src/ac/svc/ACQ_SETTLEEXP.pgc index 178dd3a..505824c 100644 --- a/app/src/ac/svc/ACQ_SETTLEEXP.pgc +++ b/app/src/ac/svc/ACQ_SETTLEEXP.pgc @@ -9,20 +9,167 @@ void ACQ_SETTLEEXP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_SETTLEEXP"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_n = 0, h_net = 0; - char h_merch[64], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_n, :h_net FROM purchase - WHERE merchant_id = :h_merch AND biz_date = :h_bizdate AND status = 'M'; - if (sqlca.sqlcode < 0) FAIL(b); + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); - setl(b, T_COUNT, h_n); + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + if (merch[0] == 0) { Bchg(b, T_MSG, 0, "가맹점 ID 누락", 0L); setl(b, T_RC, 101); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } + + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + EXEC SQL SELECT name, status, daily_limit INTO :h_str1, :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (strcmp(h_status, "ACTIVE") != 0) + userlog("%s 주의: 가맹점 %s 상태=%s", SVC, h_merch, h_status); + /* 가맹점 MDR(bps) 및 상태분포 컨텍스트 로드 */ + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + EXEC SQL SELECT count(*) FILTER (WHERE status IN ('A','M')), count(*) FILTER (WHERE status = 'S') INTO :h_openc, :h_setc FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 상태분포 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산 진행 컨텍스트(기 정산 순액) */ + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_ctx FROM settlement WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0), coalesce(sum(fee),0) INTO :h_cnt, :h_net, :h_fee FROM purchase WHERE merchant_id = :h_merch AND status = 'M'; + if (sqlca.sqlcode < 0) { + userlog("%s: 예정집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_a FROM settlement WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 당일정산 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_gross FROM purchase WHERE merchant_id = :h_merch AND status = 'M'; + if (sqlca.sqlcode < 0) { + userlog("%s: 예정원금 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 예정액 규모로 지급 준비 등급을 판정 */ + if (h_gross > 0) ratio = (h_fee * 10000L) / h_gross; else ratio = 0; + if (h_cnt == 0) { h_val = 0; } + else if (h_net >= 10000000) { h_val = 3; } + else if (h_net >= 1000000) { h_val = 2; } + else { h_val = 1; } + h_avg = (h_cnt > 0) ? (h_net / h_cnt) : 0; + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :h_val2 FROM merchant_summary WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 누적집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_pdgross FROM purchase WHERE merchant_id = :h_merch AND biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_val2 > 0 && h_gross > 0) ratio = (h_gross * 10000L) / (h_val2 + h_gross); + else ratio = 0; + h_share = (h_pdgross > 0) ? (h_gross * 100L / h_pdgross) : 0; + userlog("%s 추이: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_pdgross, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "정산예상 merch=%s 건수=%ld 예정액=%ld 당일정산=%ld", h_merch, h_cnt, h_net, h_a); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_SETTLEEXP', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_SETTLEEXP' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_COUNT, h_cnt); setl(b, T_NET, h_net); - setl(b, T_RC, h_n > 0 ? 0 : 1); + setl(b, T_FEE, h_fee); + setl(b, T_GROSS, h_gross); + setl(b, T_AMT1, h_a); + setl(b, T_ARG1, ratio); + setl(b, T_ARG2, h_val); + setl(b, T_AMT2, h_avg); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_SETTLELINK.pgc b/app/src/ac/svc/ACQ_SETTLELINK.pgc index 9dd0a77..e4e616d 100644 --- a/app/src/ac/svc/ACQ_SETTLELINK.pgc +++ b/app/src/ac/svc/ACQ_SETTLELINK.pgc @@ -1,6 +1,7 @@ /* * ACQ_SETTLELINK.pgc - ac 모듈 서비스 ACQ_SETTLELINK (one service = one file). - * 정산 연계: 가맹점 미지정 시 매입에서 조회, settlement 링크 행(status='LINKED') 생성. + * 정산 연계: 대사완료 매입에 대해 settlement 링크 행(status='LINKED')을 + * 생성해 정산 배치가 집을 수 있도록 연결한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_SETTLELINK.h" @@ -8,29 +9,154 @@ void ACQ_SETTLELINK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; + const char *SVC = "ACQ_SETTLELINK"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_id, h_pid, h_net; - char h_merch[64], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; 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)); - if (merch[0] == 0) { - EXEC SQL SELECT merchant_id INTO :h_merch FROM purchase WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - } else { - 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; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); - EXEC SQL SELECT nextval('settlement_seq') INTO :h_id; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL INSERT INTO settlement (settlement_id, purchase_id, merchant_id, net, biz_date, status) - VALUES (:h_id, :h_pid, :h_merch, :h_net, :h_bizdate, 'LINKED'); - if (sqlca.sqlcode < 0) { userlog("ACQ_SETTLELINK FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } - setl(b, T_SETTLE_ID, h_id); + userlog("%s 진입: 매입 상태 전이 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + + /* 3) 업무규칙 분기부: 상태 전이 가능여부/유형 판정(3+ 분기) */ + if (!(strcmp(h_prev, "M") == 0 || strcmp(h_prev, "S") == 0)) { + userlog("%s 거절: 상태 %s 에서 전이 불가", SVC, h_prev); + Bchg(b, T_MSG, 0, "상태 전이 불가", 0L); setl(b, T_RC, 120); FAIL(b); + } + /* 중복 링크를 막고 순정산액 규모로 등급을 정한다 */ + EXEC SQL SELECT count(*) INTO :h_cnt FROM settlement WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 링크확인 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_cnt > 0) { Bchg(b, T_MSG, 0, "이미 정산 연계됨", 0L); setl(b, T_RC, 142); FAIL(b); } + if (h_net >= 5000000) { h_val = 3; } + else if (h_net >= 500000) { h_val = 2; } + else { h_val = 1; } + + /* 4) 본처리부: 부수행 기록 및 상태 갱신 */ + EXEC SQL SELECT nextval('settlement_seq') INTO :h_setid; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO settlement (settlement_id, purchase_id, merchant_id, net, biz_date, status) VALUES (:h_setid, :h_pid, :h_merch, :h_net, :h_bizdate, 'LINKED'); + if (sqlca.sqlcode < 0) { + userlog("%s: 정산링크 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_aff = 1; + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "정산연계 pid=%ld setid=%ld 순액=%ld 등급=%ld", h_pid, h_setid, h_net, h_val); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_SETTLELINK', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_SETTLELINK' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_SETTLE_ID, h_setid); + setl(b, T_NET, h_net); + setl(b, T_ARG1, h_val); + Bchg(b, T_MERCHANT, 0, h_merch, 0L); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_SETTLEPRE.pgc b/app/src/ac/svc/ACQ_SETTLEPRE.pgc index fa77991..13f0587 100644 --- a/app/src/ac/svc/ACQ_SETTLEPRE.pgc +++ b/app/src/ac/svc/ACQ_SETTLEPRE.pgc @@ -1,7 +1,7 @@ /* * ACQ_SETTLEPRE.pgc - ac 모듈 서비스 ACQ_SETTLEPRE (one service = one file). - * 매입-정산 예정대사: 가맹점의 영업일 대사완료(M) 매입 순정산 예정액과 이미 - * 정산(settlement)된 금액을 비교해 미정산 차액과 대사 일치 여부를 낸다. + * 매입-정산 예정대사: 가맹점의 영업일 대사완료(M) 매입 순정산 예정액과 + * 이미 정산된 금액을 비교해 미정산 차액과 일치 여부를 낸다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_SETTLEPRE.h" @@ -9,26 +9,167 @@ void ACQ_SETTLEPRE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long diff; + const char *SVC = "ACQ_SETTLEPRE"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_exp = 0, h_done = 0; - char h_merch[64], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL SELECT coalesce(sum(net),0) INTO :h_exp FROM purchase - WHERE merchant_id = :h_merch AND biz_date = :h_bizdate AND status = 'M'; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT coalesce(sum(net),0) INTO :h_done FROM settlement - WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - diff = h_exp - h_done; + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); - setl(b, T_NET, h_exp); - setl(b, T_AMT1, h_done); - setl(b, T_DELTA, diff); - setl(b, T_RC, diff == 0 ? 0 : 1); + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + if (merch[0] == 0) { Bchg(b, T_MSG, 0, "가맹점 ID 누락", 0L); setl(b, T_RC, 101); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } + + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + EXEC SQL SELECT name, status, daily_limit INTO :h_str1, :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (strcmp(h_status, "ACTIVE") != 0) + userlog("%s 주의: 가맹점 %s 상태=%s", SVC, h_merch, h_status); + /* 가맹점 MDR(bps) 및 상태분포 컨텍스트 로드 */ + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + EXEC SQL SELECT count(*) FILTER (WHERE status IN ('A','M')), count(*) FILTER (WHERE status = 'S') INTO :h_openc, :h_setc FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 상태분포 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산 진행 컨텍스트(기 정산 순액) */ + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_ctx FROM settlement WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_cnt, :h_net FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate AND status = 'M'; + if (sqlca.sqlcode < 0) { + userlog("%s: 예정순액 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_a, :h_b FROM settlement WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산실적 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_gross FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 매출총액 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 예정과 실적 차액으로 대사 일치 여부를 판정 */ + h_e = h_net - h_b; + if (h_net > 0) ratio = (h_b * 10000L) / h_net; else ratio = 0; + if (h_e == 0 && h_cnt > 0) { h_val = 0; Bchg(b, T_MSG, 0, "예정대사 일치", 0L); } + else if (h_e > 0) { h_val = 2; Bchg(b, T_MSG, 0, "미정산 잔액 존재", 0L); } + else if (h_e < 0) { h_val = 3; Bchg(b, T_MSG, 0, "과다 정산 의심", 0L); } + else { h_val = 1; Bchg(b, T_MSG, 0, "대상 없음", 0L); } + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :h_val2 FROM merchant_summary WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 누적집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_pdgross FROM purchase WHERE merchant_id = :h_merch AND biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_val2 > 0 && h_gross > 0) ratio = (h_gross * 10000L) / (h_val2 + h_gross); + else ratio = 0; + h_share = (h_pdgross > 0) ? (h_gross * 100L / h_pdgross) : 0; + userlog("%s 추이: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_pdgross, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "예정대사 merch=%s 예정=%ld 정산=%ld 차액=%ld", h_merch, h_net, h_b, h_e); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_SETTLEPRE', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_SETTLEPRE' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_NET, h_net); + setl(b, T_AMT1, h_b); + setl(b, T_ARG1, h_e); + setl(b, T_ARG2, h_a); + setl(b, T_ARG3, ratio); + setl(b, T_ARG4, h_val); + setl(b, T_GROSS, h_gross); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_SPLITPAY.pgc b/app/src/ac/svc/ACQ_SPLITPAY.pgc index 73fe24b..01073b6 100644 --- a/app/src/ac/svc/ACQ_SPLITPAY.pgc +++ b/app/src/ac/svc/ACQ_SPLITPAY.pgc @@ -9,35 +9,165 @@ void ACQ_SPLITPAY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long n, per, rest, i; + const char *SVC = "ACQ_SPLITPAY"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_amt = 0, h_seq, h_n, h_m; - char h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 할부/분할 스케줄 요청", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ h_pid = getl(b, T_PURCHASE_ID); - n = getl(b, T_INSTALL_N); - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - if (n < 2 || n > 24) { setl(b, T_RC, 7); FAIL(b); } + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + h_n = getl(b, T_ARG1); + if (h_n < 2 || h_n > 12) { Bchg(b, T_MSG, 0, "분할 회차 범위 오류(2~12)", 0L); setl(b, T_RC, 191); FAIL(b); } + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - EXEC SQL SELECT amount INTO :h_amt FROM purchase WHERE purchase_id = :h_pid AND status <> 'C'; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; - per = h_amt / n; - rest = h_amt - per * n; /* 마지막 회차 가산 잔액 */ - h_n = n; + /* 3) 업무규칙 분기부: 개월/회차/금액 구간 판정(3+ 분기) */ + /* 회차당 최소 금액 요건과 기존 스케줄을 확인한다 */ + EXEC SQL SELECT count(*) INTO :h_cnt FROM ac_install WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 기존분할 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_cnt > 0) { Bchg(b, T_MSG, 0, "이미 분할 등록됨", 0L); setl(b, T_RC, 192); FAIL(b); } + h_mamt = h_gross / h_n; + if (h_mamt < 1000) { Bchg(b, T_MSG, 0, "회차당 최소금액 미달", 0L); setl(b, T_RC, 193); FAIL(b); } + h_rem = h_gross - (h_mamt * h_n); + if (h_n >= 10) { h_val = 3; } + else if (h_n >= 5) { h_val = 2; } + else { h_val = 1; } + h_months = h_n; - for (i = 1; i <= n; i++) { + /* 4) 본처리부: 회차별 스케줄 원장 루프 적재 */ + for (i = 1; i <= h_n; i++) { h_seq = i; - h_m = (i == n) ? per + rest : per; - EXEC SQL INSERT INTO ac_install (install_id, purchase_id, seq_no, months, month_amount, biz_date) - VALUES (nextval('ac_install_seq'), :h_pid, :h_seq, :h_n, :h_m, :h_bizdate); - if (sqlca.sqlcode < 0) { userlog("ACQ_SPLITPAY FAIL seq=%ld [%d]", i, sqlca.sqlcode); FAIL(b); } + h_amt = h_mamt; + if (i == h_n) h_amt = h_mamt + h_rem; + EXEC SQL SELECT nextval('ac_install_seq') INTO :h_id; + if (sqlca.sqlcode < 0) { + userlog("%s: 분할채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); 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_amt, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 분할적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_sum = h_sum + h_amt; + } + h_aff = h_n; + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "분할납부 pid=%ld 회차=%ld 회차금액=%ld 합계=%ld", h_pid, h_n, h_mamt, h_sum); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_SPLITPAY', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); } - setl(b, T_COUNT, n); - setl(b, T_AMT1, per); - setl(b, T_AMT2, per + rest); /* 마지막 회차 금액 */ + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_SPLITPAY' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_COUNT, h_n); + setl(b, T_AMT1, h_mamt); + setl(b, T_GROSS, h_sum); + setl(b, T_ARG2, h_rem); + setl(b, T_ARG3, h_val); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_SPLITQRY.pgc b/app/src/ac/svc/ACQ_SPLITQRY.pgc index de79b84..7a3c106 100644 --- a/app/src/ac/svc/ACQ_SPLITQRY.pgc +++ b/app/src/ac/svc/ACQ_SPLITQRY.pgc @@ -1,7 +1,7 @@ /* * ACQ_SPLITQRY.pgc - ac 모듈 서비스 ACQ_SPLITQRY (one service = one file). - * 분할납부 회차 조회: 매입 건의 회차 스케줄을 커서로 순회하며 회차수·합계와 - * 최소/최대 회차금액을 산출하고, 원금 대비 스케줄 합계 일치 여부를 점검한다. + * 분할납부 회차 조회: 매입 건의 회차 스케줄을 집계해 회차수·합계와 최소/최대 + * 회차금액을 산출하고 원금 대비 스케줄 합계 일치 여부를 점검한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_SPLITQRY.h" @@ -9,35 +9,151 @@ void ACQ_SPLITQRY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long cnt = 0, sum = 0, mn = 0, mx = 0; + const char *SVC = "ACQ_SPLITQRY"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_seq, h_m, h_orig = 0; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ h_pid = getl(b, T_PURCHASE_ID); - - EXEC SQL DECLARE spq_cur CURSOR FOR - SELECT seq_no, month_amount FROM ac_install - WHERE purchase_id = :h_pid ORDER BY seq_no; - EXEC SQL OPEN spq_cur; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH spq_cur INTO :h_seq, :h_m; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE spq_cur; FAIL(b); } - cnt++; sum += h_m; - if (mn == 0 || h_m < mn) mn = h_m; - if (h_m > mx) mx = h_m; + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); } - EXEC SQL CLOSE spq_cur; - EXEC SQL SELECT amount INTO :h_orig FROM purchase WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - setl(b, T_COUNT, cnt); - setl(b, T_GROSS, sum); - setl(b, T_AMT1, mn); - setl(b, T_AMT2, mx); - setl(b, T_RC, (cnt > 0 && sum == h_orig) ? 0 : 1); /* 스케줄-원금 일치 여부 */ + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*), coalesce(sum(month_amount),0), coalesce(min(month_amount),0), coalesce(max(month_amount),0) INTO :h_cnt, :h_sum, :h_min, :h_max FROM ac_install WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 스케줄집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT amount, net, status INTO :h_amt, :h_net, :h_status FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 원매입 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 원금 대비 스케줄 합계 정합과 잔액 배분을 판정 */ + h_e = h_amt - h_sum; + if (h_cnt == 0) { h_val = 1; Bchg(b, T_MSG, 0, "분할 스케줄 없음", 0L); } + else if (h_e == 0) { h_val = 0; Bchg(b, T_MSG, 0, "스케줄 정합", 0L); } + else if (h_e > 0 && h_e < h_cnt) { h_val = 2; Bchg(b, T_MSG, 0, "단수 잔액", 0L); } + else { h_val = 3; Bchg(b, T_MSG, 0, "스케줄 불일치", 0L); } + if (h_amt > 0) ratio = (h_sum * 10000L) / h_amt; else ratio = 0; + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "분할조회 pid=%ld 회차=%ld 합계=%ld 차이=%ld", h_pid, h_cnt, h_sum, h_e); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_SPLITQRY', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_SPLITQRY' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_GROSS, h_sum); + setl(b, T_AMT1, h_min); + setl(b, T_AMT2, h_max); + setl(b, T_AMOUNT, h_amt); + setl(b, T_ARG1, h_e); + setl(b, T_ARG2, ratio); + setl(b, T_ARG3, h_val); + Bchg(b, T_STATUS, 0, h_status, 0L); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_STATUS.pgc b/app/src/ac/svc/ACQ_STATUS.pgc index 424adc5..c5d30dc 100644 --- a/app/src/ac/svc/ACQ_STATUS.pgc +++ b/app/src/ac/svc/ACQ_STATUS.pgc @@ -1,16 +1,138 @@ /* * ACQ_STATUS.pgc - ac 모듈 서비스 ACQ_STATUS (one service = one file). - * Moved verbatim from the former inline ac_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by ac_svr at tpsvrinit. + * 매입 상태 조회: 매입 ID 로 현재 상태를 읽어 돌려주고, 상태에 따른 진행 단계와 + * 부수 이력(취소/조정/미매입) 건수를 함께 응답한다. + * 입출력 계약 보존: 입력 T_PURCHASE_ID, 출력 T_STATUS (acdb_get_status 사용). + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_STATUS.h" 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); + const char *SVC = "ACQ_STATUS"; + char st[4], logmsg[256]; + long pid, stage = 0; + EXEC SQL BEGIN DECLARE SECTION; + long h_pid = 0, h_can = 0, h_adj = 0, h_unm = 0, h_apr = 0, h_logid = 0; + long h_amt = 0, h_fee = 0, h_net = 0, h_ctx = 0, h_led = 0, h_set = 0, h_inst = 0; + char h_bizdate[16], h_merch[64], h_chan[16], h_logmsg[256]; + EXEC SQL END DECLARE SECTION; + + userlog("%s 진입: 매입 상태 조회 요청", SVC); + + /* 1) 입력 검증부 */ + pid = getl(b, T_PURCHASE_ID); + if (pid <= 0) { + Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); + } + h_pid = pid; + + /* 2) 마스터/상태 조회부: 계약 보존을 위해 dbio 헬퍼로 상태를 읽는다 */ + st[0] = 0; + if (acdb_get_status(pid, st) < 0) { + userlog("%s: 상태 조회 실패 pid=%ld", SVC, pid); + Bchg(b, T_MSG, 0, "상태 조회 실패", 0L); setl(b, T_RC, 111); FAIL(b); + } + EXEC SQL SELECT merchant_id, amount, fee, net, channel, biz_date::text + INTO :h_merch, :h_amt, :h_fee, :h_net, :h_chan, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 조회 실패 [%d]", SVC, sqlca.sqlcode); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { + Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); + } + if (h_bizdate[0] == 0) strcpy(h_bizdate, "2026-07-19"); + userlog("%s 컨텍스트: pid=%ld merch=%s 상태=%s", SVC, pid, h_merch, st); + + /* 부수 이력 건수 로드 */ + EXEC SQL SELECT count(*) INTO :h_can FROM ac_cancel WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_adj FROM ac_fee_adj WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_unm FROM ac_unmatch WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_apr FROM approval WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); } + + /* 정산/원장/할부 연계 컨텍스트 로드 */ + EXEC SQL SELECT count(*) INTO :h_set FROM settlement WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_led, :h_ctx + FROM ledger WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_inst FROM ac_install WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); } + userlog("%s 연계: 정산=%ld 원장=%ld 할부=%ld", SVC, h_set, h_led, h_inst); + + /* 3) 업무규칙 분기부: 상태코드를 진행 단계로 환산 (3+ 분기) */ + if (strcmp(st, "A") == 0) { + stage = 1; Bchg(b, T_MSG, 0, "접수 단계", 0L); + } else if (strcmp(st, "M") == 0) { + stage = 2; Bchg(b, T_MSG, 0, "대사완료 단계", 0L); + } else if (strcmp(st, "S") == 0) { + stage = 3; Bchg(b, T_MSG, 0, "정산완료 단계", 0L); + } else if (strcmp(st, "H") == 0) { + stage = 4; Bchg(b, T_MSG, 0, "보류 상태", 0L); + } else if (strcmp(st, "C") == 0 || strcmp(st, "V") == 0) { + stage = 5; Bchg(b, T_MSG, 0, "종료(취소/무효) 상태", 0L); + } else { + stage = 0; Bchg(b, T_MSG, 0, "기타 상태", 0L); + } + + /* 7) 예외/보상부: 상태와 이력의 정합 감시 */ + if (strcmp(st, "S") == 0 && h_unm > 0) + userlog("%s 경보: 정산완료건에 미매입 이력 %ld 건", SVC, h_unm); + if (h_apr == 0) + userlog("%s 경보: pid=%ld 승인 이력 없음", SVC, pid); + if (strcmp(st, "S") == 0 && h_set == 0) + userlog("%s 경보: 정산완료건에 정산행 없음 pid=%ld", SVC, pid); + if (strcmp(st, "C") == 0 && h_can == 0) + userlog("%s 경보: 취소건에 취소이력 없음 pid=%ld", SVC, pid); + if (h_amt != h_fee + h_net) + userlog("%s 경보: 금액 정합 불일치 pid=%ld (%ld <> %ld+%ld)", SVC, pid, h_amt, h_fee, h_net); + + /* 5) 이력/감사 적재부 */ + snprintf(logmsg, sizeof(logmsg), "상태조회 pid=%ld 상태=%s 단계=%ld 취소=%ld 조정=%ld", + pid, st, stage, h_can, h_adj); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_STATUS', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_STATUS' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 조회 %ld 회로 과다", SVC, h_ctx); + + /* 8) 응답부 (계약 보존: T_STATUS) */ Bchg(b, T_STATUS, 0, st, 0L); + Bchg(b, T_MERCHANT, 0, h_merch, 0L); + Bchg(b, T_CHANNEL, 0, h_chan, 0L); + setl(b, T_AMOUNT, h_amt); + setl(b, T_FEE, h_fee); + setl(b, T_NET, h_net); + setl(b, T_ARG1, stage); + setl(b, T_ARG2, h_can); + setl(b, T_ARG3, h_adj); + setl(b, T_ARG4, h_unm); + setl(b, T_ARG5, h_apr); + setl(b, T_ARG6, h_set); + setl(b, T_ARG7, h_led); + setl(b, T_ARG8, h_inst); + setl(b, T_AMT1, h_ctx); + setl(b, T_RC, 0); + userlog("%s 완료: pid=%ld 상태=%s 단계=%ld", SVC, pid, st, stage); OK(b); } diff --git a/app/src/ac/svc/ACQ_TAXCXL.pgc b/app/src/ac/svc/ACQ_TAXCXL.pgc index f1bbef8..39b10fc 100644 --- a/app/src/ac/svc/ACQ_TAXCXL.pgc +++ b/app/src/ac/svc/ACQ_TAXCXL.pgc @@ -9,34 +9,159 @@ void ACQ_TAXCXL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_TAXCXL"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_inv, h_neg, h_pid = 0, h_supply = 0, h_vat = 0, h_bal = 0; - char h_merch[64], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; - h_inv = getl(b, T_ID1); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); - EXEC SQL SELECT purchase_id, merchant_id, supply, vat, to_char(biz_date, 'YYYY-MM-DD') - INTO :h_pid, :h_merch, :h_supply, :h_vat, :h_bizdate - FROM ac_tax_invoice WHERE inv_id = :h_inv; - if (sqlca.sqlcode == 100) { setl(b, T_RC, 4); FAIL(b); } - if (sqlca.sqlcode < 0) FAIL(b); - if (h_supply < 0) { setl(b, T_RC, 3); FAIL(b); } /* 음수 계산서는 취소 불가 */ + userlog("%s 진입: 매입 상태 전이 요청 접수", SVC); - /* 동일 매입의 계산서 잔액이 이미 0 이면 상쇄 완료 상태 */ - EXEC SQL SELECT coalesce(sum(supply), 0) INTO :h_bal - FROM ac_tax_invoice WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_bal == 0) { setl(b, T_RC, 2); FAIL(b); } + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - EXEC SQL SELECT nextval('ac_taxinv_seq') INTO :h_neg; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL INSERT INTO ac_tax_invoice (inv_id, purchase_id, merchant_id, supply, vat, biz_date) - VALUES (:h_neg, :h_pid, :h_merch, -:h_supply, -:h_vat, :h_bizdate); - if (sqlca.sqlcode < 0) { userlog("ACQ_TAXCXL FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; - setl(b, T_ID2, h_neg); - setl(b, T_TAX, -h_vat); + /* 3) 업무규칙 분기부: 상태 전이 가능여부/유형 판정(3+ 분기) */ + if (!(strcmp(h_prev, "A") == 0 || strcmp(h_prev, "M") == 0 || strcmp(h_prev, "S") == 0 || strcmp(h_prev, "C") == 0 || strcmp(h_prev, "H") == 0 || strcmp(h_prev, "V") == 0 || strcmp(h_prev, "P") == 0)) { + userlog("%s 거절: 상태 %s 에서 전이 불가", SVC, h_prev); + Bchg(b, T_MSG, 0, "상태 전이 불가", 0L); setl(b, T_RC, 120); FAIL(b); + } + /* 현재 계산서 누계를 확인해 상쇄 가능 여부를 판정 */ + EXEC SQL SELECT count(*), coalesce(sum(supply),0), coalesce(sum(vat),0) + INTO :h_cnt, :h_supply, :h_vat FROM ac_tax_invoice + WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 계산서누계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_cnt == 0) { Bchg(b, T_MSG, 0, "취소 대상 계산서 없음", 0L); setl(b, T_RC, 150); FAIL(b); } + if (h_supply == 0) { Bchg(b, T_MSG, 0, "이미 상쇄된 계산서", 0L); setl(b, T_RC, 151); FAIL(b); } + if (h_supply >= 1000000) { h_val = 3; } + else if (h_supply >= 100000) { h_val = 2; } + else { h_val = 1; } + h_delta = -h_supply; h_tax = -h_vat; + + /* 4) 본처리부: 부수행 기록 및 상태 갱신 */ + EXEC SQL SELECT nextval('ac_taxinv_seq') INTO :h_invid; + if (sqlca.sqlcode < 0) { + userlog("%s: 계산서채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO ac_tax_invoice (inv_id, purchase_id, merchant_id, supply, vat, biz_date) VALUES (:h_invid, :h_pid, :h_merch, :h_delta, :h_tax, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 상쇄계산서 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_aff = 1; + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "계산서취소 pid=%ld inv=%ld 공급가=%ld 부가세=%ld", h_pid, h_invid, h_delta, h_tax); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_TAXCXL', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_TAXCXL' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_ID1, h_invid); + setl(b, T_GROSS, h_delta); + setl(b, T_TAX, h_tax); + setl(b, T_COUNT, h_cnt); + setl(b, T_ARG1, h_val); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_TAXINV.pgc b/app/src/ac/svc/ACQ_TAXINV.pgc index a925d39..837f99f 100644 --- a/app/src/ac/svc/ACQ_TAXINV.pgc +++ b/app/src/ac/svc/ACQ_TAXINV.pgc @@ -1,22 +1,154 @@ /* * ACQ_TAXINV.pgc - ac 모듈 서비스 ACQ_TAXINV (one service = one file). - * Moved verbatim from the former inline ac_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by ac_svr at tpsvrinit. + * 세금계산서 발행: 매입 공급가에 대한 부가세를 산출해 ac_tax_invoice 에 + * 계산서를 발행한다. 동일 매입 중복 발행은 사유코드로 거절한다. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_TAXINV.h" 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)); + const char *SVC = "ACQ_TAXINV"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; + EXEC SQL BEGIN DECLARE SECTION; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; + EXEC SQL END DECLARE SECTION; + + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 매입 금액/수수료 조정 요청", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } 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); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + + /* 3) 업무규칙 분기부: 조정 유형/부호/한도 판정(3+ 분기) */ + /* 공급가를 확정하고 중복 발행을 차단 */ + h_supply = getl(b, T_AMOUNT); if (h_supply <= 0) h_supply = h_gross; + EXEC SQL SELECT count(*) INTO :h_cnt FROM ac_tax_invoice + WHERE purchase_id = :h_pid AND supply > 0; + if (sqlca.sqlcode < 0) { + userlog("%s: 중복발행 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_cnt > 0) { Bchg(b, T_MSG, 0, "이미 계산서 발행됨", 0L); setl(b, T_RC, 183); FAIL(b); } + h_vat = acq_vat(h_supply); + if (h_supply >= 10000000) { h_val = 3; } + else if (h_supply >= 1000000) { h_val = 2; } + else { h_val = 1; } + + /* 4) 본처리부: 조정 원장 기록 및 매입 금액 보정 */ + h_invid = acdb_insert_tax_invoice(h_pid, h_merch, h_supply, h_vat, h_bizdate); + if (h_invid < 0) { Bchg(b, T_MSG, 0, "계산서 발행 실패", 0L); setl(b, T_RC, 100); FAIL(b); } + h_aff = 1; + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "계산서발행 pid=%ld inv=%ld 공급가=%ld 부가세=%ld", h_pid, h_invid, h_supply, h_vat); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_TAXINV', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_TAXINV' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_ID1, h_invid); + setl(b, T_TAX, h_vat); + setl(b, T_GROSS, h_supply); + setl(b, T_ARG1, h_val); + Bchg(b, T_MERCHANT, 0, h_merch, 0L); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_TAXMODIFY.pgc b/app/src/ac/svc/ACQ_TAXMODIFY.pgc index 4c846bf..aaccfd0 100644 --- a/app/src/ac/svc/ACQ_TAXMODIFY.pgc +++ b/app/src/ac/svc/ACQ_TAXMODIFY.pgc @@ -1,7 +1,7 @@ /* * ACQ_TAXMODIFY.pgc - ac 모듈 서비스 ACQ_TAXMODIFY (one service = one file). - * 세금계산서 수정발행: 기 발행된 계산서(ac_tax_invoice)의 공급가·부가세를 - * 정정 금액으로 갱신한다. 부가세는 공급가의 10%로 재계산해 반영한다. + * 세금계산서 수정발행: 기 발행된 계산서의 공급가·부가세를 정정 금액으로 + * 갱신한다. 부가세는 공급가의 10%로 재계산해 반영한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_TAXMODIFY.h" @@ -9,24 +9,159 @@ void ACQ_TAXMODIFY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long supply, vat; + const char *SVC = "ACQ_TAXMODIFY"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_inv, h_supply, h_vat, h_aff; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; - h_inv = getl(b, T_ID1); - supply = getl(b, T_GROSS); - if (supply <= 0) { setl(b, T_RC, 3); FAIL(b); } - vat = supply / 10; - h_supply = supply; h_vat = vat; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); - EXEC SQL UPDATE ac_tax_invoice SET supply = :h_supply, vat = :h_vat - WHERE inv_id = :h_inv; - if (sqlca.sqlcode < 0) { userlog("ACQ_TAXMODIFY FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + userlog("%s 진입: 매입 금액/수수료 조정 요청", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + h_amt = getl(b, T_AMOUNT); + if (h_amt < 1L) { Bchg(b, T_MSG, 0, "금액 범위 오류", 0L); setl(b, T_RC, 104); FAIL(b); } + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + + /* 3) 업무규칙 분기부: 조정 유형/부호/한도 판정(3+ 분기) */ + /* 수정 대상 계산서를 찾고 정정 폭을 판정 */ + EXEC SQL SELECT count(*), coalesce(max(inv_id),0) INTO :h_cnt, :h_invid + FROM ac_tax_invoice WHERE purchase_id = :h_pid AND supply > 0; + if (sqlca.sqlcode < 0) { + userlog("%s: 대상계산서 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_cnt == 0) { Bchg(b, T_MSG, 0, "수정 대상 계산서 없음", 0L); setl(b, T_RC, 184); FAIL(b); } + EXEC SQL SELECT supply, vat INTO :h_supply, :h_vat FROM ac_tax_invoice + WHERE inv_id = :h_invid; + if (sqlca.sqlcode < 0) { + userlog("%s: 원계산서 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_delta = h_amt - h_supply; + if (h_delta == 0) { Bchg(b, T_MSG, 0, "공급가 변동 없음", 0L); setl(b, T_RC, 185); FAIL(b); } + else if (h_delta > 0) { h_val = 3; Bchg(b, T_MSG, 0, "공급가 증액 수정", 0L); } + else { h_val = 2; Bchg(b, T_MSG, 0, "공급가 감액 수정", 0L); } + computed = acq_vat(h_amt); + + /* 4) 본처리부: 조정 원장 기록 및 매입 금액 보정 */ + h_comp = computed; + EXEC SQL UPDATE ac_tax_invoice SET supply = :h_amt, vat = :h_comp WHERE inv_id = :h_invid; + if (sqlca.sqlcode < 0) { + userlog("%s: 계산서수정 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } h_aff = sqlca.sqlerrd[2]; - if (h_aff == 0) { setl(b, T_RC, 5); FAIL(b); } - setl(b, T_GROSS, supply); - setl(b, T_TAX, vat); + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "계산서수정 inv=%ld 공급가 %ld -> %ld 부가세=%ld", h_invid, h_supply, h_amt, computed); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_TAXMODIFY', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_TAXMODIFY' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_ID1, h_invid); + setl(b, T_GROSS, h_amt); + setl(b, T_TAX, computed); + setl(b, T_DELTA, h_delta); + setl(b, T_ARG1, h_val); + setl(b, T_AMT1, h_supply); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_TAXQRY.pgc b/app/src/ac/svc/ACQ_TAXQRY.pgc index 23ee058..434eda8 100644 --- a/app/src/ac/svc/ACQ_TAXQRY.pgc +++ b/app/src/ac/svc/ACQ_TAXQRY.pgc @@ -9,19 +9,153 @@ void ACQ_TAXQRY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_TAXQRY"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_supply = 0, h_vat = 0, h_n = 0; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } - EXEC SQL SELECT coalesce(sum(supply),0), coalesce(sum(vat),0), count(*) - INTO :h_supply, :h_vat, :h_n FROM ac_tax_invoice WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*), coalesce(sum(supply),0), coalesce(sum(vat),0) INTO :h_cnt, :h_supply, :h_vat FROM ac_tax_invoice WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 계산서집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*) FILTER (WHERE supply < 0), count(*) FILTER (WHERE supply > 0) INTO :h_a, :h_b FROM ac_tax_invoice WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 정역발행 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT amount INTO :h_amt FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 원매입 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 정발행/역발행 구성으로 계산서 상태를 분류 */ + if (h_cnt == 0) { h_val = 1; Bchg(b, T_MSG, 0, "계산서 없음", 0L); } + else if (h_supply == 0) { h_val = 3; Bchg(b, T_MSG, 0, "전액 상쇄됨", 0L); } + else if (h_a > 0) { h_val = 2; Bchg(b, T_MSG, 0, "역발행 포함", 0L); } + else { h_val = 0; Bchg(b, T_MSG, 0, "정상 발행", 0L); } + h_e = acq_vat(h_supply) - h_vat; + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "계산서조회 pid=%ld 건수=%ld 공급가=%ld 부가세=%ld", h_pid, h_cnt, h_supply, h_vat); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_TAXQRY', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_TAXQRY' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_supply); setl(b, T_TAX, h_vat); - setl(b, T_COUNT, h_n); - setl(b, T_RC, h_n > 0 ? 0 : 1); + setl(b, T_ARG1, h_a); + setl(b, T_ARG2, h_b); + setl(b, T_ARG3, h_e); + setl(b, T_ARG4, h_val); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_TAXREISSUE.pgc b/app/src/ac/svc/ACQ_TAXREISSUE.pgc index a0b9f9a..ceb4ee1 100644 --- a/app/src/ac/svc/ACQ_TAXREISSUE.pgc +++ b/app/src/ac/svc/ACQ_TAXREISSUE.pgc @@ -9,31 +9,151 @@ void ACQ_TAXREISSUE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_TAXREISSUE"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_old, h_new, h_pid = 0, h_supply = 0, h_vat = 0; - char h_merch[64], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; - h_old = getl(b, T_ID1); - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); - EXEC SQL SELECT purchase_id, merchant_id, supply, vat - INTO :h_pid, :h_merch, :h_supply, :h_vat - FROM ac_tax_invoice WHERE inv_id = :h_old; - if (sqlca.sqlcode == 100) { setl(b, T_RC, 4); FAIL(b); } - if (sqlca.sqlcode < 0) FAIL(b); + userlog("%s 진입: 매입 금액/수수료 조정 요청", SVC); - /* 부가세 정합 재검증: 공급가의 10% 와 다르면 재발행 거부 */ - if (h_vat != acq_vat(h_supply)) { setl(b, T_RC, 2); FAIL(b); } + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - EXEC SQL SELECT nextval('ac_taxinv_seq') INTO :h_new; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL INSERT INTO ac_tax_invoice (inv_id, purchase_id, merchant_id, supply, vat, biz_date) - VALUES (:h_new, :h_pid, :h_merch, :h_supply, :h_vat, :h_bizdate); - if (sqlca.sqlcode < 0) { userlog("ACQ_TAXREISSUE FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; - setl(b, T_ID2, h_new); - setl(b, T_TAX, h_vat); + /* 3) 업무규칙 분기부: 조정 유형/부호/한도 판정(3+ 분기) */ + /* 원본 계산서를 찾아 승계 금액을 확보하고 부가세를 재검증 */ + EXEC SQL SELECT count(*), coalesce(max(inv_id),0) INTO :h_cnt, :h_id + FROM ac_tax_invoice WHERE purchase_id = :h_pid AND supply > 0; + if (sqlca.sqlcode < 0) { + userlog("%s: 원본조회 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_cnt == 0) { Bchg(b, T_MSG, 0, "재발행 원본 없음", 0L); setl(b, T_RC, 186); FAIL(b); } + EXEC SQL SELECT supply, vat INTO :h_supply, :h_vat FROM ac_tax_invoice + WHERE inv_id = :h_id; + if (sqlca.sqlcode < 0) { + userlog("%s: 원본금액 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + computed = acq_vat(h_supply); + if (h_vat != computed) { h_val = 3; Bchg(b, T_MSG, 0, "원본 부가세 정정 후 재발행", 0L); } + else if (h_cnt >= 2) { h_val = 2; Bchg(b, T_MSG, 0, "다중 원본 중 최신 재발행", 0L); } + else { h_val = 1; Bchg(b, T_MSG, 0, "정상 재발행", 0L); } + + /* 4) 본처리부: 조정 원장 기록 및 매입 금액 보정 */ + h_invid = acdb_insert_tax_invoice(h_pid, h_merch, h_supply, computed, h_bizdate); + if (h_invid < 0) { Bchg(b, T_MSG, 0, "재발행 실패", 0L); setl(b, T_RC, 100); FAIL(b); } + h_aff = 1; + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "계산서재발행 pid=%ld 원inv=%ld 신inv=%ld 공급가=%ld", h_pid, h_id, h_invid, h_supply); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_TAXREISSUE', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_TAXREISSUE' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_ID1, h_invid); + setl(b, T_ID2, h_id); + setl(b, T_GROSS, h_supply); + setl(b, T_TAX, computed); + setl(b, T_ARG1, h_val); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_TAXREVERSE.pgc b/app/src/ac/svc/ACQ_TAXREVERSE.pgc index ae2db8f..260c523 100644 --- a/app/src/ac/svc/ACQ_TAXREVERSE.pgc +++ b/app/src/ac/svc/ACQ_TAXREVERSE.pgc @@ -9,26 +9,164 @@ void ACQ_TAXREVERSE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_TAXREVERSE"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_src, h_new, h_pid = 0, h_supply = 0, h_vat = 0; - char h_merch[64], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; - h_src = getl(b, T_ID1); - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); - EXEC SQL SELECT purchase_id, merchant_id, supply, vat - INTO :h_pid, :h_merch, :h_supply, :h_vat FROM ac_tax_invoice WHERE inv_id = :h_src; - if (sqlca.sqlcode == 100) { setl(b, T_RC, 5); FAIL(b); } - if (sqlca.sqlcode < 0) FAIL(b); + userlog("%s 진입: 매입 상태 전이 요청 접수", SVC); - EXEC SQL SELECT nextval('ac_taxinv_seq') INTO :h_new; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL INSERT INTO ac_tax_invoice (inv_id, purchase_id, merchant_id, supply, vat, biz_date) - VALUES (:h_new, :h_pid, :h_merch, -:h_supply, -:h_vat, :h_bizdate); - if (sqlca.sqlcode < 0) { userlog("ACQ_TAXREVERSE FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - setl(b, T_ID2, h_new); + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + + /* 3) 업무규칙 분기부: 상태 전이 가능여부/유형 판정(3+ 분기) */ + if (!(strcmp(h_prev, "A") == 0 || strcmp(h_prev, "M") == 0 || strcmp(h_prev, "S") == 0 || strcmp(h_prev, "C") == 0 || strcmp(h_prev, "H") == 0 || strcmp(h_prev, "V") == 0 || strcmp(h_prev, "P") == 0)) { + userlog("%s 거절: 상태 %s 에서 전이 불가", SVC, h_prev); + Bchg(b, T_MSG, 0, "상태 전이 불가", 0L); setl(b, T_RC, 120); FAIL(b); + } + /* 역발행 기준이 될 최근 정발행 계산서를 찾는다 */ + EXEC SQL SELECT count(*), coalesce(max(inv_id),0) INTO :h_cnt, :h_id + FROM ac_tax_invoice WHERE purchase_id = :h_pid AND supply > 0; + if (sqlca.sqlcode < 0) { + userlog("%s: 정발행조회 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_cnt == 0) { Bchg(b, T_MSG, 0, "역발행 대상 없음", 0L); setl(b, T_RC, 152); FAIL(b); } + EXEC SQL SELECT supply, vat INTO :h_supply, :h_vat FROM ac_tax_invoice + WHERE inv_id = :h_id; + if (sqlca.sqlcode < 0) { + userlog("%s: 원계산서 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_delta = -h_supply; + h_tax = -acq_vat(h_supply); + if (h_vat != acq_vat(h_supply)) { h_val = 3; Bchg(b, T_MSG, 0, "원 부가세 불일치(재계산 적용)", 0L); } + else if (h_cnt >= 2) { h_val = 2; Bchg(b, T_MSG, 0, "다중 정발행 중 최신 역발행", 0L); } + else { h_val = 1; Bchg(b, T_MSG, 0, "정상 역발행", 0L); } + + /* 4) 본처리부: 부수행 기록 및 상태 갱신 */ + EXEC SQL SELECT nextval('ac_taxinv_seq') INTO :h_invid; + if (sqlca.sqlcode < 0) { + userlog("%s: 역발행채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO ac_tax_invoice (inv_id, purchase_id, merchant_id, supply, vat, biz_date) VALUES (:h_invid, :h_pid, :h_merch, :h_delta, :h_tax, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 역발행계산서 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_aff = 1; + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "계산서역발행 pid=%ld 원inv=%ld 역inv=%ld 공급가=%ld", h_pid, h_id, h_invid, h_delta); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_TAXREVERSE', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_TAXREVERSE' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_ID1, h_invid); + setl(b, T_ID2, h_id); + setl(b, T_GROSS, h_delta); + setl(b, T_TAX, h_tax); + setl(b, T_ARG1, h_val); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_TOPMERCH.pgc b/app/src/ac/svc/ACQ_TOPMERCH.pgc index 9563b4d..e491c80 100644 --- a/app/src/ac/svc/ACQ_TOPMERCH.pgc +++ b/app/src/ac/svc/ACQ_TOPMERCH.pgc @@ -1,7 +1,7 @@ /* * ACQ_TOPMERCH.pgc - ac 모듈 서비스 ACQ_TOPMERCH (one service = one file). - * 상위 가맹점 조회: 영업일 매출 상위 N개 가맹점을 커서로 뽑아 가맹점 ID와 - * 매출액 쌍을 occurrence 로 내려주고 상위권 합계를 계산한다. + * 상위 가맹점 조회: 영업일 매출 1위 가맹점과 매출액, 상위 3사 합계를 + * 뽑아 상위권 집중도를 계산해 응답한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_TOPMERCH.h" @@ -9,36 +9,154 @@ void ACQ_TOPMERCH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long want, got = 0, topsum = 0; + const char *SVC = "ACQ_TOPMERCH"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_want, h_sum; - char h_merch[64], h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - want = getl(b, T_ARG1); - if (want < 1 || want > 10) want = 5L; - h_want = want; - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL DECLARE tpm_cur CURSOR FOR - SELECT merchant_id, sum(amount) FROM purchase - WHERE biz_date = :h_bizdate AND status NOT IN ('C', 'V') - GROUP BY merchant_id ORDER BY sum(amount) DESC, merchant_id - LIMIT :h_want; - EXEC SQL OPEN tpm_cur; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH tpm_cur INTO :h_merch, :h_sum; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE tpm_cur; FAIL(b); } - Badd(b, T_KEY1, h_merch, 0L); - Badd(b, T_AMT2, (char *)&h_sum, 0L); - got++; topsum += h_sum; + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); } - EXEC SQL CLOSE tpm_cur; + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } - setl(b, T_COUNT, got); - setl(b, T_GROSS, topsum); /* 상위권 매출 합계 */ - setl(b, T_RC, got > 0 ? 0 : 1); + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(DISTINCT merchant_id), count(*), coalesce(sum(amount),0) INTO :h_n, :h_cnt, :h_gross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 모집단 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(merchant_id,''), coalesce(sum(amount),0), count(*) INTO :h_merch, :h_a, :h_b FROM purchase WHERE biz_date = :h_bizdate GROUP BY merchant_id ORDER BY sum(amount) DESC LIMIT 1; + if (sqlca.sqlcode < 0) { + userlog("%s: 1위가맹점 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(sum(s),0) INTO :h_c FROM (SELECT sum(amount) AS s FROM purchase WHERE biz_date = :h_bizdate GROUP BY merchant_id ORDER BY sum(amount) DESC LIMIT 3) t3; + if (sqlca.sqlcode < 0) { + userlog("%s: 상위3사 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 상위 집중도로 가맹점 포트폴리오 편중을 판정 */ + if (h_gross > 0) ratio = (h_a * 10000L) / h_gross; else ratio = 0; + if (h_gross > 0) h_share = (h_c * 10000L) / h_gross; else h_share = 0; + if (h_n == 0) { h_val = 0; } + else if (h_share >= 9000) { h_val = 3; } + else if (ratio >= 5000) { h_val = 2; } + else { h_val = 1; } + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "상위가맹점 bizdate=%s 1위=%s 매출=%ld 집중=%ld", h_bizdate, h_merch, h_a, h_share); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_TOPMERCH', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_TOPMERCH' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + Bchg(b, T_MERCHANT, 0, h_merch, 0L); + setl(b, T_AMT1, h_a); + setl(b, T_ARG1, h_b); + setl(b, T_AMT2, h_c); + setl(b, T_ARG2, ratio); + setl(b, T_ARG3, h_share); + setl(b, T_ARG4, h_val); + setl(b, T_COUNT, h_n); + setl(b, T_GROSS, h_gross); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_UNMATCH.pgc b/app/src/ac/svc/ACQ_UNMATCH.pgc index bd9c9fc..3993503 100644 --- a/app/src/ac/svc/ACQ_UNMATCH.pgc +++ b/app/src/ac/svc/ACQ_UNMATCH.pgc @@ -1,29 +1,171 @@ /* * ACQ_UNMATCH.pgc - ac 모듈 서비스 ACQ_UNMATCH (one service = one file). - * Moved verbatim from the former inline ac_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by ac_svr at tpsvrinit. + * 미매입 등록: 대사에서 짝을 찾지 못한 매입을 미매입 원장에 사유와 함께 + * 등록하고 원매입을 보류(H)로 내려 후속 조사를 대기시킨다. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_UNMATCH.h" void ACQ_UNMATCH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + const char *SVC = "ACQ_UNMATCH"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_cnt; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; + + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 매입 상태 전이 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "일반"); + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); - 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); + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + + /* 3) 업무규칙 분기부: 상태 전이 가능여부/유형 판정(3+ 분기) */ + if (!(strcmp(h_prev, "A") == 0 || strcmp(h_prev, "M") == 0)) { + userlog("%s 거절: 상태 %s 에서 전이 불가", SVC, h_prev); + Bchg(b, T_MSG, 0, "상태 전이 불가", 0L); setl(b, T_RC, 120); FAIL(b); + } + /* 동일 사유 중복 등록을 막고 누적 미매입으로 등급 판정 */ + EXEC SQL SELECT count(*) INTO :h_cnt FROM ac_unmatch + WHERE purchase_id = :h_pid AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 중복확인 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_cnt >= 5) { Bchg(b, T_MSG, 0, "미매입 등록 한도 초과", 0L); setl(b, T_RC, 135); FAIL(b); } + else if (h_cnt >= 2) { h_val = 3; } + else if (h_cnt == 1) { h_val = 2; } + else { h_val = 1; } + + /* 4) 본처리부: 부수행 기록 및 상태 갱신 */ + EXEC SQL SELECT nextval('ac_unmatch_seq') INTO :h_id; + if (sqlca.sqlcode < 0) { + userlog("%s: 미매입채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO ac_unmatch (unmatch_id, purchase_id, reason, biz_date) VALUES (:h_id, :h_pid, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 미매입등록 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL UPDATE purchase SET status = 'H', updated_at = now() WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 상태갱신 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_aff = sqlca.sqlerrd[2]; + if (h_aff > 0) Bchg(b, T_STATUS, 0, "H", 0L); + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "미매입등록 pid=%ld id=%ld 누적=%ld 사유=%s", h_pid, h_id, h_cnt + 1, h_reason); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_UNMATCH', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_UNMATCH' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_ID1, h_id); + setl(b, T_COUNT, h_cnt + 1); + setl(b, T_ARG1, h_val); + setl(b, T_AMOUNT, h_gross); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_VELOCITY.pgc b/app/src/ac/svc/ACQ_VELOCITY.pgc index 2181850..f33687d 100644 --- a/app/src/ac/svc/ACQ_VELOCITY.pgc +++ b/app/src/ac/svc/ACQ_VELOCITY.pgc @@ -9,24 +9,167 @@ void ACQ_VELOCITY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long allow; + const char *SVC = "ACQ_VELOCITY"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_10m = 0, h_1h = 0; - char h_merch[64]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - allow = getl(b, T_ARG2); /* 10분 허용 건수 */ - if (allow <= 0) allow = 100L; - gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL SELECT count(*) FILTER (WHERE created_at >= now() - interval '10 minutes'), - count(*) FILTER (WHERE created_at >= now() - interval '1 hour') - INTO :h_10m, :h_1h - FROM purchase WHERE merchant_id = :h_merch; - if (sqlca.sqlcode < 0) { userlog("ACQ_VELOCITY FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); - setl(b, T_ARG1, h_10m); /* 최근 10분 건수 */ - setl(b, T_ARG3, h_1h); /* 최근 1시간 건수 */ - setl(b, T_RC, h_10m > allow ? 1 : 0); + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + if (merch[0] == 0) { Bchg(b, T_MSG, 0, "가맹점 ID 누락", 0L); setl(b, T_RC, 101); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + threshold = getl(b, T_ARG1); if (threshold <= 0) threshold = 20; + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } + + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + EXEC SQL SELECT name, status, daily_limit INTO :h_str1, :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (strcmp(h_status, "ACTIVE") != 0) + userlog("%s 주의: 가맹점 %s 상태=%s", SVC, h_merch, h_status); + /* 가맹점 MDR(bps) 및 상태분포 컨텍스트 로드 */ + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + EXEC SQL SELECT count(*) FILTER (WHERE status IN ('A','M')), count(*) FILTER (WHERE status = 'S') INTO :h_openc, :h_setc FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 상태분포 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산 진행 컨텍스트(기 정산 순액) */ + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_ctx FROM settlement WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_a, :h_val FROM purchase WHERE merchant_id = :h_merch AND created_at >= now() - interval '10 minutes'; + if (sqlca.sqlcode < 0) { + userlog("%s: 10분유입 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_b, :h_val2 FROM purchase WHERE merchant_id = :h_merch AND created_at >= now() - interval '1 hour'; + if (sqlca.sqlcode < 0) { + userlog("%s: 1시간유입 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_gross FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 당일누적 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 10분 유입을 허용치와 비교하고 시간당 추세로 보정 */ + ratio = h_a * 6; + if (h_a > threshold) { h_e = 2; Bchg(b, T_MSG, 0, "유입속도 초과", 0L); } + else if (ratio > h_b * 2 && h_b > 0) { h_e = 1; Bchg(b, T_MSG, 0, "유입 급증 추세", 0L); } + else if (h_a == 0) { h_e = 0; Bchg(b, T_MSG, 0, "유입 없음", 0L); } + else { h_e = 0; Bchg(b, T_MSG, 0, "정상 유입", 0L); } + if (h_e >= 2) userlog("%s 경보: %ld건/10분 (허용 %ld)", SVC, h_a, threshold); + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :h_val2 FROM merchant_summary WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 누적집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_pdgross FROM purchase WHERE merchant_id = :h_merch AND biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_val2 > 0 && h_gross > 0) ratio = (h_gross * 10000L) / (h_val2 + h_gross); + else ratio = 0; + h_share = (h_pdgross > 0) ? (h_gross * 100L / h_pdgross) : 0; + userlog("%s 추이: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_pdgross, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "속도점검 merch=%s 10분=%ld 1시간=%ld 판정=%ld", h_merch, h_a, h_b, h_e); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_VELOCITY', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_VELOCITY' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ + setl(b, T_COUNT, h_a); + setl(b, T_ARG2, h_b); + setl(b, T_ARG3, ratio); + setl(b, T_ARG4, h_e); + setl(b, T_AMT1, h_val); + setl(b, T_AMT2, h_val2); + setl(b, T_GROSS, h_gross); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_VOID.pgc b/app/src/ac/svc/ACQ_VOID.pgc index 15b8fff..1d6c629 100644 --- a/app/src/ac/svc/ACQ_VOID.pgc +++ b/app/src/ac/svc/ACQ_VOID.pgc @@ -1,28 +1,165 @@ /* * ACQ_VOID.pgc - ac 모듈 서비스 ACQ_VOID (one service = one file). - * Moved verbatim from the former inline ac_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by ac_svr at tpsvrinit. + * 매입 무효(void): 당일 접수분에 한해 매입을 무효(V)로 전이한다. + * 정산이 끝난 건은 무효화할 수 없으며 무효 금액을 원장에 역분개한다. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_VOID.h" void ACQ_VOID(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + const char *SVC = "ACQ_VOID"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_id, h_pid, h_amt; - char h_bizdate[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); h_amt = getl(b, T_AMOUNT); + + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 매입 상태 전이 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } + gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "일반"); + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; 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); + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + + /* 3) 업무규칙 분기부: 상태 전이 가능여부/유형 판정(3+ 분기) */ + if (!(strcmp(h_prev, "A") == 0 || strcmp(h_prev, "H") == 0)) { + userlog("%s 거절: 상태 %s 에서 전이 불가", SVC, h_prev); + Bchg(b, T_MSG, 0, "상태 전이 불가", 0L); setl(b, T_RC, 120); FAIL(b); + } + /* 무효 대상 금액 구간에 따라 승인 경로를 나눈다 */ + if (h_gross >= 5000000) { h_val = 3; } + else if (h_gross >= 500000) { h_val = 2; } + else { h_val = 1; } + h_delta = -h_gross; + + /* 4) 본처리부: 부수행 기록 및 상태 갱신 */ + EXEC SQL SELECT nextval('ledger_seq') INTO :h_ledid; + if (sqlca.sqlcode < 0) { + userlog("%s: 원장채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) VALUES (:h_ledid, :h_pid, 'VOID', :h_delta, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 무효역분개 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL UPDATE purchase SET status = 'V', updated_at = now() WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 상태갱신 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_aff = sqlca.sqlerrd[2]; + if (h_aff > 0) Bchg(b, T_STATUS, 0, "V", 0L); + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "매입무효 pid=%ld 금액=%ld 등급=%ld 사유=%s", h_pid, h_gross, h_val, h_reason); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_VOID', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_VOID' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_AMOUNT, h_gross); + setl(b, T_DELTA, h_delta); + setl(b, T_ARG1, h_val); + setl(b, T_ID1, h_ledid); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_WEEKSTAT.pgc b/app/src/ac/svc/ACQ_WEEKSTAT.pgc index 9c7d8d3..ffcee1c 100644 --- a/app/src/ac/svc/ACQ_WEEKSTAT.pgc +++ b/app/src/ac/svc/ACQ_WEEKSTAT.pgc @@ -1,7 +1,7 @@ /* * ACQ_WEEKSTAT.pgc - ac 모듈 서비스 ACQ_WEEKSTAT (one service = one file). * 매입 주간 통계: 기준일 포함 직전 7일 구간의 건수/총액/수수료 합계와 - * 최대 거래일(거래액 기준)을 산출한다. 구간 경계는 date 연산으로 계산. + * 최대 거래일을 산출한다. 구간 경계는 date 연산으로 계산한다. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_WEEKSTAT.h" @@ -9,36 +9,151 @@ void ACQ_WEEKSTAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *SVC = "ACQ_WEEKSTAT"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt = 0, h_amt = 0, h_fee = 0, h_peak = 0; - char h_bizdate[16], h_peakday[16]; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - h_peakday[0] = 0; + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); - EXEC SQL SELECT count(*), coalesce(sum(amount), 0), coalesce(sum(fee), 0) - INTO :h_cnt, :h_amt, :h_fee - FROM purchase - WHERE biz_date > CAST(:h_bizdate AS date) - 7 - AND biz_date <= CAST(:h_bizdate AS date) - AND status <> 'C'; - if (sqlca.sqlcode < 0) FAIL(b); + userlog("%s 진입: 매입 조회/집계 요청 접수", SVC); - /* 주간 최대 거래일 */ - EXEC SQL SELECT to_char(biz_date, 'YYYY-MM-DD'), sum(amount) - INTO :h_peakday, :h_peak - FROM purchase - WHERE biz_date > CAST(:h_bizdate AS date) - 7 - AND biz_date <= CAST(:h_bizdate AS date) - AND status <> 'C' - GROUP BY biz_date ORDER BY sum(amount) DESC LIMIT 1; - if (sqlca.sqlcode < 0) { userlog("ACQ_WEEKSTAT FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + /* 1) 입력 검증부: 전 필드 필수/형식 점검, 위반 시 사유코드로 거절 */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); setl(b, T_RC, 105); FAIL(b); } - if (h_peakday[0]) Bchg(b, T_STR1, 0, h_peakday, 0L); + /* 작업 컨텍스트 확인 로깅 */ + userlog("%s 컨텍스트: merch=%s pid=%ld bizdate=%s", SVC, h_merch, h_pid, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 조회/집계 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부: 기준 정보 및 운영 파라미터 로드 */ + /* 영업일 전체 컨텍스트 로드(개별 집계의 비교 기준) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_ctx, :h_pdgross FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 일자컨텍스트 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 정산주기 운영 파라미터 로드 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'SETTLE_CYCLE'; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산주기 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + k = (sqlca.sqlcode == 0) ? atol(h_str2) : 1; + userlog("%s 일자컨텍스트: 전체 %ld건 %ld원 주기=%ld", SVC, h_ctx, h_pdgross, k); + + /* 4) 본처리부: 실적 집계 질의 수행 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), count(DISTINCT biz_date) INTO :h_cnt, :h_gross, :h_fee, :h_n FROM purchase WHERE biz_date BETWEEN (:h_bizdate::date - 6) AND :h_bizdate::date; + if (sqlca.sqlcode < 0) { + userlog("%s: 주간집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL SELECT coalesce(biz_date::text,''), coalesce(sum(amount),0) INTO :h_d1, :h_max FROM purchase WHERE biz_date BETWEEN (:h_bizdate::date - 6) AND :h_bizdate::date GROUP BY biz_date ORDER BY sum(amount) DESC LIMIT 1; + if (sqlca.sqlcode < 0) { + userlog("%s: 최대거래일 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 3) 업무규칙 분기부: 결과 구간/임계에 따른 파생 판정(3+ 분기) */ + /* 일평균 대비 최대일 편차로 주간 변동성을 판정 */ + h_avg = (h_n > 0) ? (h_gross / h_n) : 0; + if (h_avg > 0) ratio = (h_max * 100L) / h_avg; else ratio = 0; + if (h_cnt == 0) { h_val = 0; } + else if (ratio >= 300) { h_val = 3; } + else if (ratio >= 180) { h_val = 2; } + else { h_val = 1; } + h_net = h_gross - h_fee; + + /* 6) 집계/요약 갱신부: 보조 재집계로 맥락 지표 산출 */ + /* 직전 영업일 전체 실적과 비교해 추세 지수를 만든다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_min, :h_val2 FROM purchase WHERE biz_date = (:h_bizdate::date - 1); + if (sqlca.sqlcode < 0) { + userlog("%s: 직전일전체 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + ratio = (h_cnt > 0) ? (h_gross / h_cnt) : 0; + h_avg = ratio; + h_share = (h_val2 > 0) ? (h_gross * 100L / h_val2) : 0; + userlog("%s 추세: 당일=%ld 직전일=%ld 지수=%ld", SVC, h_gross, h_val2, h_share); + + /* 7) 예외/보상부: 결과 이상징후 감시(무실적/음수/한도) */ + if (h_cnt == 0) userlog("%s 경보: 조회 실적 없음 (%s)", SVC, h_bizdate); + if (h_gross < 0) { Bchg(b, T_MSG, 0, "집계 음수 이상", 0L); setl(b, T_RC, 91); FAIL(b); } + if (h_lim > 0 && h_gross > h_lim) userlog("%s 경보: 집계 %ld 가 한도 %ld 초과", SVC, h_gross, h_lim); + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "주간통계 기준=%s 건수=%ld 총액=%ld 최대일=%s", h_bizdate, h_cnt, h_gross, h_d1); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_WEEKSTAT', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_WEEKSTAT' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부: 결과 필드 채움 */ setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_amt); + setl(b, T_GROSS, h_gross); setl(b, T_FEE, h_fee); - setl(b, T_AMT1, h_peak); + setl(b, T_NET, h_net); + Bchg(b, T_STR1, 0, h_d1, 0L); + setl(b, T_AMT1, h_max); + setl(b, T_AMT2, h_avg); + setl(b, T_ARG1, h_n); + setl(b, T_ARG2, ratio); + setl(b, T_ARG3, h_val); + /* 공통 컨텍스트 지표 동봉(수수료율/미결/정산완료) */ + setl(b, T_ARG7, h_rate); + setl(b, T_ARG8, h_openc); + setl(b, T_ID2, h_setc); + userlog("%s 완료: 건수=%ld 총액=%ld 지표=%ld", SVC, h_cnt, h_gross, ratio); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_WHT.pgc b/app/src/ac/svc/ACQ_WHT.pgc index 9b8942a..79c10e4 100644 --- a/app/src/ac/svc/ACQ_WHT.pgc +++ b/app/src/ac/svc/ACQ_WHT.pgc @@ -1,26 +1,168 @@ /* * ACQ_WHT.pgc - ac 모듈 서비스 ACQ_WHT (one service = one file). - * Moved verbatim from the former inline ac_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by ac_svr at tpsvrinit. + * 원천징수: 가맹점 수수료에 대한 원천세를 운영 파라미터(WHT_BPS) 기준으로 + * 산출해 ac_wht 원장에 적립하고 세액을 회신한다. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_WHT.h" void ACQ_WHT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long pid, tax, wht_id; + const char *SVC = "ACQ_WHT"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; EXEC SQL BEGIN DECLARE SECTION; - long e_pid, e_fee; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; EXEC SQL END DECLARE SECTION; - pid = getl(b, T_PURCHASE_ID); + + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 매입 금액/수수료 조정 요청", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } 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); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + + /* 3) 업무규칙 분기부: 조정 유형/부호/한도 판정(3+ 분기) */ + /* 운영 파라미터에서 원천징수율을 읽어 세액 산출 */ + EXEC SQL SELECT param_val INTO :h_str2 FROM cm_param WHERE param_key = 'WHT_BPS'; + if (sqlca.sqlcode < 0) { + userlog("%s: 원천율 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + threshold = (sqlca.sqlcode == 0) ? atol(h_str2) : 2200; + if (threshold <= 0) threshold = 2200; + computed = acq_wht_amt(h_fee, threshold); + if (computed <= 0) { Bchg(b, T_MSG, 0, "원천세 대상 아님", 0L); setl(b, T_RC, 181); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(tax_amount),0) INTO :h_cnt, :h_sum + FROM ac_wht WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 기징수 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_cnt > 0) { Bchg(b, T_MSG, 0, "이미 원천징수됨", 0L); setl(b, T_RC, 182); FAIL(b); } + if (computed >= 100000) { h_val = 3; } + else if (computed >= 10000) { h_val = 2; } + else { h_val = 1; } + + /* 4) 본처리부: 조정 원장 기록 및 매입 금액 보정 */ + h_tax = computed; + h_id = acdb_insert_wht(h_pid, h_tax, h_bizdate); + if (h_id < 0) { Bchg(b, T_MSG, 0, "원천세 적재 실패", 0L); setl(b, T_RC, 99); FAIL(b); } + EXEC SQL UPDATE purchase SET net = net - :h_tax, updated_at = now() WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 정산금차감 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_aff = sqlca.sqlerrd[2]; + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "원천징수 pid=%ld 수수료=%ld 세율=%ld 세액=%ld", h_pid, h_fee, threshold, h_tax); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_WHT', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_WHT' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_TAX, h_tax); + setl(b, T_ID1, h_id); + setl(b, T_ARG2, threshold); + setl(b, T_FEE, h_fee); + setl(b, T_NET, h_net - h_tax); + setl(b, T_ARG1, h_val); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/ac/svc/ACQ_XFERLINK.pgc b/app/src/ac/svc/ACQ_XFERLINK.pgc index 1b12b36..0ba1805 100644 --- a/app/src/ac/svc/ACQ_XFERLINK.pgc +++ b/app/src/ac/svc/ACQ_XFERLINK.pgc @@ -1,16 +1,164 @@ /* * ACQ_XFERLINK.pgc - ac 모듈 서비스 ACQ_XFERLINK (one service = one file). - * Moved verbatim from the former inline ac_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by ac_svr at tpsvrinit. + * 이체 연계: 정산 완료 매입의 지급 이체를 원장에 XFER 분개로 연결한다. + * 이체 금액은 순정산액이며 중복 이체 연계를 방지한다. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr. */ #include "ACQ_XFERLINK.h" 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); + const char *SVC = "ACQ_XFERLINK"; + char merch[64], bizdate[16], ccy[8], issuer[32], channel[16], logmsg[256]; + char reason[128], docno[64]; + long threshold = 0, ratio = 0, k = 0; + char category[24]; + long i = 0, bps = 25, computed = 0, prior = 0; + EXEC SQL BEGIN DECLARE SECTION; + long h_cnt = 0, h_gross = 0, h_net = 0, h_fee = 0, h_sum = 0, h_avg = 0, h_max = 0; + long h_a = 0, h_b = 0, h_c = 0, h_d = 0, h_e = 0, h_top = 0, h_val = 0, h_val2 = 0; + long h_lim = 0, h_rate = 0, h_pid = 0, h_amt = 0, h_logid = 0, h_n = 0, h_id = 0; + long h_openc = 0, h_setc = 0, h_pdgross = 0, h_ctx = 0, h_share = 0, h_min = 0; + long h_supply = 0, h_vat = 0, h_months = 0, h_delta = 0, h_tax = 0; + char h_merch[64], h_bizdate[16], h_ccy[8], h_issuer[32], h_status[8], h_chan[16]; + char h_reason[128], h_str1[64], h_str2[40], h_logmsg[256], h_d1[16], h_d2[16]; + long h_krw = 0, h_fx = 0, h_comp = 0, h_rate2 = 0, h_sfee = 0, h_snet = 0; + long h_seq = 0, h_mamt = 0, h_new = 0, h_aff = 0, h_prev2 = 0; + long h_canid = 0, h_adjid = 0, h_invid = 0, h_ledid = 0, h_setid = 0, h_rem = 0; + char h_prev[8], h_ctype[16], h_category[24], h_docno[64]; + EXEC SQL END DECLARE SECTION; + + /* 작업영역 초기화: 문자열 호스트변수/로컬버퍼를 모두 널로 시작한다 */ + memset(h_merch, 0, sizeof(h_merch)); memset(h_bizdate, 0, sizeof(h_bizdate)); + memset(h_ccy, 0, sizeof(h_ccy)); memset(h_issuer, 0, sizeof(h_issuer)); + memset(h_status, 0, sizeof(h_status)); memset(h_chan, 0, sizeof(h_chan)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_str1, 0, sizeof(h_str1)); + memset(h_str2, 0, sizeof(h_str2)); memset(h_logmsg, 0, sizeof(h_logmsg)); + memset(h_d1, 0, sizeof(h_d1)); memset(h_d2, 0, sizeof(h_d2)); + memset(merch, 0, sizeof(merch)); memset(bizdate, 0, sizeof(bizdate)); + memset(ccy, 0, sizeof(ccy)); memset(issuer, 0, sizeof(issuer)); + memset(channel, 0, sizeof(channel)); memset(reason, 0, sizeof(reason)); + memset(docno, 0, sizeof(docno)); memset(logmsg, 0, sizeof(logmsg)); + memset(h_prev, 0, sizeof(h_prev)); memset(h_ctype, 0, sizeof(h_ctype)); + memset(h_category, 0, sizeof(h_category)); memset(h_docno, 0, sizeof(h_docno)); + memset(category, 0, sizeof(category)); + + userlog("%s 진입: 매입 상태 전이 요청 접수", SVC); + + /* 1) 입력 검증부: 전 필드 필수/범위/형식 점검 후 위반 시 거절 */ + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 102); FAIL(b); } gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (acdb_insert_ledger(pid, "XFER", amt, bizdate) < 0) FAIL(b); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + if (h_bizdate[0] == 0) { + EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE'; + if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19"); + } + if (strlen(h_bizdate) != 10) { Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); setl(b, T_RC, 105); FAIL(b); } + userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld bizdate=%s", SVC, h_pid, h_merch, h_amt, h_bizdate); + /* 요청 파라미터 감사 덤프 (운영 추적용) */ + userlog("%s 요청감사: 갱신 요청 수신", SVC); + userlog("%s 요청감사: merch=%s pid=%ld amt=%ld biz=%s", SVC, h_merch, h_pid, h_amt, h_bizdate); + + /* 2) 마스터/정책/상태 조회부 */ + EXEC SQL SELECT status, merchant_id, amount, fee, net INTO :h_prev, :h_merch, :h_gross, :h_fee, :h_net FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { + userlog("%s: 매입원장 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "매입 미존재", 0L); setl(b, T_RC, 111); FAIL(b); } + userlog("%s 원매입: pid=%ld status=%s amount=%ld", SVC, h_pid, h_prev, h_gross); + EXEC SQL SELECT status, daily_limit INTO :h_status, :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0) { + userlog("%s: 가맹점마스터 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "가맹점 미존재", 0L); setl(b, T_RC, 110); FAIL(b); } + if (acdb_merchant_mdr(h_merch, &h_rate) < 0) h_rate = 25; + + /* 3) 업무규칙 분기부: 상태 전이 가능여부/유형 판정(3+ 분기) */ + if (!(strcmp(h_prev, "S") == 0 || strcmp(h_prev, "M") == 0)) { + userlog("%s 거절: 상태 %s 에서 전이 불가", SVC, h_prev); + Bchg(b, T_MSG, 0, "상태 전이 불가", 0L); setl(b, T_RC, 120); FAIL(b); + } + /* 기존 XFER 분개 존재 여부로 중복 이체를 막는다 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM ledger WHERE purchase_id = :h_pid AND entry_type = 'XFER'; + if (sqlca.sqlcode < 0) { + userlog("%s: 이체확인 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_cnt > 0) { Bchg(b, T_MSG, 0, "이미 이체 연계됨", 0L); setl(b, T_RC, 143); FAIL(b); } + if (h_net <= 0) { Bchg(b, T_MSG, 0, "이체 대상 순액 없음", 0L); setl(b, T_RC, 144); FAIL(b); } + if (h_net >= 10000000) { h_val = 3; } + else if (h_net >= 1000000) { h_val = 2; } + else { h_val = 1; } + + /* 4) 본처리부: 부수행 기록 및 상태 갱신 */ + EXEC SQL SELECT nextval('ledger_seq') INTO :h_ledid; + if (sqlca.sqlcode < 0) { + userlog("%s: 원장채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) VALUES (:h_ledid, :h_pid, 'XFER', :h_net, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 이체분개 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + h_aff = 1; + + /* 6) 집계/요약 갱신부 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_sum, :h_sfee, :h_snet FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 재집계 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + /* 재집계 결과(총액/수수료/순액/건수)를 가맹점 일별 요약에 반영 */ + if (acdb_upsert_merch_sum(h_merch, h_bizdate, h_sum, h_sfee, h_snet, h_cnt) < 0) + userlog("%s 주의: 요약 갱신 스킵", SVC); + userlog("%s 요약갱신: merch=%s 건수=%ld 총액=%ld 수수료=%ld", SVC, h_merch, h_cnt, h_sum, h_sfee); + + /* 7) 예외/보상부: 반영 결과 정합 확인 */ + if (h_aff < 0) { Bchg(b, T_MSG, 0, "반영 실패", 0L); setl(b, T_RC, 92); FAIL(b); } + + /* 5) 이력/감사 적재부: 처리 요약을 공통 로그(cm_log)에 남긴다 */ + snprintf(logmsg, sizeof(logmsg), "이체연계 pid=%ld ledid=%ld 이체액=%ld 등급=%ld", h_pid, h_ledid, h_net, h_val); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0; + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid; + if (sqlca.sqlcode < 0) { + userlog("%s: 로그채번 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_logid, 'ACQ_XFERLINK', 'I', :h_logmsg, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("%s: 로그적재 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + + /* 9) 사후 검증부: 당일 처리 이력 자기 점검 및 과다 호출 감시 */ + EXEC SQL SELECT count(*) INTO :h_ctx FROM cm_log + WHERE svc_name = 'ACQ_XFERLINK' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + userlog("%s: 사후검증 SQL 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + setl(b, T_RC, 90); FAIL(b); + } + if (h_ctx <= 0) { + userlog("%s 경보: 처리 이력이 적재되지 않음", SVC); + Bchg(b, T_MSG, 0, "이력 적재 확인 실패", 0L); + setl(b, T_RC, 89); FAIL(b); + } + if (h_ctx > 1000) + userlog("%s 경보: 당일 호출 %ld 회로 과다", SVC, h_ctx); + setl(b, T_ARG6, h_ctx); + + /* 8) 응답부 */ + setl(b, T_ID1, h_ledid); + setl(b, T_NET, h_net); + setl(b, T_AMOUNT, h_gross); + setl(b, T_ARG1, h_val); + userlog("%s 완료: pid=%ld 반영=%ld", SVC, h_pid, h_aff); + setl(b, T_RC, 0); OK(b); } diff --git a/app/src/au/svc/APPROVAL_RATE.pgc b/app/src/au/svc/APPROVAL_RATE.pgc index 24289f9..f81779d 100644 --- a/app/src/au/svc/APPROVAL_RATE.pgc +++ b/app/src/au/svc/APPROVAL_RATE.pgc @@ -6,37 +6,202 @@ */ #include "APPROVAL_RATE.h" +/* + * 심화 구현 노트 (APPROVAL_RATE): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void APPROVAL_RATE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; - long bps = 0; + const char *svcname = "APPROVAL_RATE"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_all = 0, h_ok = 0; - char h_merch[64], h_bizdate[16]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_MERCHANT, merch, sizeof(merch)); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (merch[0] == 0 || bizdate[0] == 0) FAIL(b); - strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 가맹점 승인율 조회: 영업일 접수 대비 승인 성공(A/M/S) 비율을 b", svcname); - EXEC SQL SELECT count(*), count(*) FILTER (WHERE status IN ('A','M','S')) - INTO :h_all, :h_ok + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_merch[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "가맹점ID 필수 누락", 0L); userlog("[%s] 입력검증 실패: 가맹점ID 누락", svcname); FAIL(b); } + if (h_bizdate[0] == 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8803L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 마스터/정책 조회부: 대상 원장 스칼라 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + count(*) FILTER (WHERE status IN ('A','M','S')), + count(*) FILTER (WHERE status='D'), count(*) FILTER (WHERE status='C') + INTO :h_cnt, :h_sum, :h_appr_cnt, :h_dec_cnt, :h_v1 FROM au_authorization - WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); + WHERE (:h_card = '' OR card_no = :h_card) + AND (:h_merch = '' OR merchant_id = :h_merch) + AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 101L); FAIL(b); } - if (h_all > 0) bps = (h_ok * 10000L) / h_all; - setl(b, T_ARG1, bps); - setl(b, T_COUNT, h_all); - setl(b, T_ARG2, h_ok); - if (h_all >= 10 && bps < 8000) { - userlog("APPROVAL_RATE 저조 경보 merch=%s %ldbps (%ld/%ld)", merch, bps, h_ok, h_all); - setl(b, T_RC, 1); - } else { - setl(b, T_RC, 0); + /* 3) 지표 분기부: 승인율/거절율 구간 판정 */ + if (h_cnt == 0) { rc = 0; h_v2 = 0; strcpy(outmsg, "실적 없음"); } + else { + h_v2 = h_appr_cnt * 10000L / h_cnt; /* 승인율 bps */ + h_v3 = h_dec_cnt * 10000L / h_cnt; /* 거절율 bps */ + if (h_v2 >= 9500) { rc = 0; strcpy(res_status, "GOOD"); } + else if (h_v2 >= 8000) { rc = 1; strcpy(res_status, "WARN"); } + else { rc = 2; strcpy(res_status, "POOR"); } } + + /* 4) 본처리부: 평균단가/최고금액 등 파생지표 */ + EXEC SQL SELECT coalesce(avg(amount),0)::bigint, coalesce(max(amount),0) + INTO :h_score, :h_id FROM au_authorization + WHERE (:h_bizdate = '' OR biz_date = :h_bizdate::date) + AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 102L); FAIL(b); } + avail = h_score; + + /* 5) 재집계부: 가맹점 정책(mdr/한도) 참고 로드 */ + if (h_merch[0]) { + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_dlim, :h_mstat + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) strcpy(h_mstat, "NONE"); + else if (sqlca.sqlcode < 0) { setl(b, T_RC, 103L); FAIL(b); } + } + /* 6) 요약 갱신부: 가맹점 당일 집계 upsert(있을 때) */ + if (h_merch[0] && h_bizdate[0]) + audb_upsert_auth_summary(h_merch, h_bizdate, h_cnt, h_appr_cnt, h_dec_cnt, h_sum); + /* 7) 예외/보상: 조회/집계 전용 - 실패시 FAIL */ + outv = h_v2; + userlog("[%s] 집계 cnt=%ld appr=%ld rate=%ldbps rc=%ld", svcname, h_cnt, h_appr_cnt, h_v2, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (APPROVAL_RATE) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_ARG1, h_v1); + setl(b, T_COUNT, h_cnt); + setl(b, T_ARG2, h_v2); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/AUTH.pgc b/app/src/au/svc/AUTH.pgc index 227be75..4888afd 100644 --- a/app/src/au/svc/AUTH.pgc +++ b/app/src/au/svc/AUTH.pgc @@ -6,40 +6,217 @@ */ #include "AUTH.h" +/* + * 심화 구현 노트 (AUTH): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void AUTH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24], merch[64], bizdate[16], appr[12]; - long amount, auth_id, lim = 0, used = 0; + const char *svcname = "AUTH"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; + EXEC SQL BEGIN DECLARE SECTION; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; + EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - if (card[0] == 0) { userlog("AUTH: T_KEY1(카드번호) 누락"); FAIL(b); } - gets_(b, T_MERCHANT, merch, sizeof(merch)); - amount = getl(b, T_AMOUNT); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 승인 접수: 한도 점검 후 authorization 원장 기록 + 한도 ", svcname); - /* 한도 점검: 초과면 거절(D) 상태로 기록 */ - if (audb_limit_get(card, &lim, &used) < 0) FAIL(b); - if (used + amount > lim) { - userlog("AUTH 한도초과 card=%s used=%ld amt=%ld lim=%ld -> 거절", card, used, amount, lim); - auth_id = audb_next_auth_id(); - if (auth_id < 0) FAIL(b); - if (audb_insert_auth(auth_id, card, merch, amount, "000000", "AUTH", "D", bizdate) < 0) FAIL(b); - setl(b, T_ID1, auth_id); - setl(b, T_RC, 1); - Bchg(b, T_STATUS, 0, "D", 0L); - OK(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_merch[0] == 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "가맹점ID 필수 누락", 0L); userlog("[%s] 입력검증 실패: 가맹점ID 누락", svcname); FAIL(b); } + if (h_bizdate[0] == 0) { setl(b, T_RC, 8803L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (h_amount < 0) { setl(b, T_RC, 8804L); Bchg(b, T_MSG, 0, "금액/수치 음수 불가", 0L); userlog("[%s] 입력검증 실패: 음수값", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8805L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 마스터/정책 조회부: 카드 한도원장 + 가맹점 상태 */ + if (audb_limit_get(h_card, &h_limit, &h_used) < 0) { + setl(b, T_RC, 21L); Bchg(b, T_MSG, 0, "한도원장 조회 실패", 0L); + userlog("[%s] 한도원장 조회 실패 card=%s", svcname, h_card); FAIL(b); } + avail = h_limit - h_used; + if (h_merch[0]) { + EXEC SQL SELECT status, mdr_bps, daily_limit INTO :h_mstat, :h_mdr, :h_dlim + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { strcpy(h_mstat, "NONE"); } + else if (sqlca.sqlcode < 0) { setl(b, T_RC, 22L); FAIL(b); } } - auth_id = audb_next_auth_id(); - if (auth_id < 0) FAIL(b); - sprintf(appr, "%06ld", auth_id % 1000000); - if (audb_insert_auth(auth_id, card, merch, amount, appr, "AUTH", "A", bizdate) < 0) FAIL(b); - if (audb_limit_dec(card, amount) < 0) FAIL(b); - setl(b, T_ID1, auth_id); - setl(b, T_RC, 0); - Bchg(b, T_STR2, 0, appr, 0L); - Bchg(b, T_STATUS, 0, "A", 0L); - userlog("AUTH 승인 auth_id=%ld card=%s merch=%s amt=%ld appr=%s", auth_id, card, merch, amount, appr); + /* 3) 업무규칙 분기부: 한도/가맹점상태/금액 구간별 승인 판정 */ + if (strcmp(h_mstat, "CLOSED") == 0) { + strcpy(res_status, "D"); strcpy(h_reason, "MERCH_CLOSED"); rc = 3; + } else if (h_amount <= 0) { + strcpy(res_status, "D"); strcpy(h_reason, "ZERO_AMT"); rc = 4; + } else if (h_used + h_amount > h_limit) { + strcpy(res_status, "D"); strcpy(h_reason, "OVER_LIMIT"); rc = 1; + } else if (h_amount >= 1000000) { + strcpy(res_status, "A"); strcpy(h_reason, "HIGH_OK"); rc = 0; + } else { + strcpy(res_status, "A"); strcpy(h_reason, "OK"); rc = 0; + } + + /* 4) 본처리부: 승인 원장 채번·삽입 (+승인시 한도 차감) */ + h_auth = audb_next_auth_id(); + if (h_auth < 0) { setl(b, T_RC, 23L); FAIL(b); } + sprintf(h_appr, "%06ld", h_auth % 1000000L); + if (audb_insert_auth(h_auth, h_card, h_merch, h_amount, h_appr, + "AUTH", res_status, h_bizdate) < 0) { + setl(b, T_RC, 24L); Bchg(b, T_MSG, 0, "승인원장 적재 실패", 0L); FAIL(b); } + if (res_status[0] == 'A') { + if (audb_limit_dec(h_card, h_amount) < 0) { + setl(b, T_RC, 25L); Bchg(b, T_MSG, 0, "한도 차감 실패", 0L); FAIL(b); } + h_used += h_amount; avail = h_limit - h_used; + } + + /* 5) 이력/재집계부: 당일 카드 승인 실적 재집계(감사 요약) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 26L); FAIL(b); } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계 upsert */ + if (h_merch[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status='A'), + count(*) FILTER (WHERE status='D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode >= 0) + audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + /* 7) 예외/보상: 개별 EXEC SQL 실패시 상단에서 FAIL→TPFAIL 롤백 (부분처리 방지) */ + outv = h_amount; avail = h_limit - h_used; + userlog("[%s] 판정 %s auth_id=%ld amt=%ld avail=%ld", svcname, res_status, h_auth, h_amount, avail); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (AUTH) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_ID1, h_auth); + setl(b, T_RC, rc); + Bchg(b, T_STATUS, 0, res_status, 0L); + Bchg(b, T_STR2, 0, h_appr, 0L); /* 승인번호 (계약 보존) */ + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/AUTH_AMT_ADJUST.pgc b/app/src/au/svc/AUTH_AMT_ADJUST.pgc index 2ab794f..15026fa 100644 --- a/app/src/au/svc/AUTH_AMT_ADJUST.pgc +++ b/app/src/au/svc/AUTH_AMT_ADJUST.pgc @@ -7,51 +7,206 @@ */ #include "AUTH_AMT_ADJUST.h" +/* + * 심화 구현 노트 (AUTH_AMT_ADJUST): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void AUTH_AMT_ADJUST(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long delta, lim = 0, used = 0; + const char *svcname = "AUTH_AMT_ADJUST"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_id, h_delta, h_amt = 0; - char h_card[24], h_status[4]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - h_id = getl(b, T_ID1); - delta = getl(b, T_DELTA); - if (h_id <= 0 || delta == 0) { userlog("AUTH_AMT_ADJUST: 인자 오류"); FAIL(b); } - memset(h_card, 0, sizeof(h_card)); - memset(h_status, 0, sizeof(h_status)); + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 승인금액 조정(incremental/decremental auth): 호", svcname); - EXEC SQL SELECT card_no, status, amount INTO :h_card, :h_status, :h_amt - FROM au_authorization WHERE auth_id = :h_id; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); - if (h_status[0] != 'A') { /* 매입확정/취소 후에는 조정 불가 */ - setl(b, T_AMOUNT, h_amt); - setl(b, T_RC, 1); - OK(b); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + h_auth = getl(b, T_ID1); + h_delta = getl(b, T_DELTA); + if (h_auth <= 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "식별자(ID) 범위 오류", 0L); userlog("[%s] 입력검증 실패: ID<=0", svcname); FAIL(b); } + if (h_delta < 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "금액/수치 음수 불가", 0L); userlog("[%s] 입력검증 실패: 음수값", svcname); FAIL(b); } + + /* 2) 상태 조회부: 대상 승인의 현재 상태/금액/카드 확보 */ + if (h_auth <= 0) h_auth = getl(b, T_ID2); + EXEC SQL SELECT status, amount, card_no, to_char(biz_date,'YYYY-MM-DD') + INTO :h_status, :h_amount, :h_card, :h_bizdate + FROM au_authorization WHERE auth_id = :h_auth; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 31L); + Bchg(b, T_MSG, 0, "대상 승인건 없음", 0L); + userlog("[%s] 대상없음 auth_id=%ld", svcname, h_auth); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 32L); FAIL(b); } + strncpy(res_status, h_status, sizeof(res_status)-1); + + /* 3) 전이 가능여부 분기부: 허용 상태에서만 A 로 전이 */ + if (strchr("A", h_status[0]) == NULL) { + setl(b, T_RC, 33L); Bchg(b, T_MSG, 0, "상태전이 불가", 0L); + userlog("[%s] 전이불가 cur=%s auth_id=%ld", svcname, h_status, h_auth); rc = 33; + setl(b, T_RC, rc); goto respond; } + if (h_delta < 0) h_delta = -h_delta; + + /* 4) 본처리부: 원장 상태 전이 (+금액조정/취소원장) */ + EXEC SQL UPDATE au_authorization SET status = 'A', updated_at = now() + WHERE auth_id = :h_auth AND status IN ('A'); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 34L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 35L); + Bchg(b, T_MSG, 0, "반영 건수 0 (경합/이미전이)", 0L); rc = 35; goto respond; } + strcpy(res_status, "A"); rc = 0; + /* 금액조정형: 조정 델타를 승인금액에 반영 */ + if (h_delta > 0) { + EXEC SQL UPDATE au_authorization SET amount = amount + :h_delta, updated_at = now() + WHERE auth_id = :h_auth; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 36L); FAIL(b); } + h_amount += h_delta; } - if (h_amt + delta <= 0) FAIL(b); /* 전액 이하로의 감액은 취소로 처리해야 함 */ - if (delta > 0) { - if (audb_limit_get(h_card, &lim, &used) < 0) FAIL(b); - if (used + delta > lim) { - userlog("AUTH_AMT_ADJUST 한도부족 auth=%ld delta=%ld", h_id, delta); - setl(b, T_AMOUNT, h_amt); - setl(b, T_RC, 2); - OK(b); + /* 5) 이력/재집계부: 카드 당일 상태분포 재집계 */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 37L); FAIL(b); } + } + /* 6) 집계 갱신부: 상태 전이 결과 상위 카운트 */ + EXEC SQL SELECT count(*) INTO :h_v1 FROM au_authorization + WHERE card_no = :h_card AND status = 'A'; + if (sqlca.sqlcode < 0) h_v1 = 0; + /* 7) 예외/보상: 위 EXEC SQL 실패는 FAIL→XA 롤백으로 부분전이 방지 */ +respond: + outv = h_amount; avail = h_v1; + userlog("[%s] 전이 %s->%s auth_id=%ld rc=%ld", svcname, h_status, res_status, h_auth, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (AUTH_AMT_ADJUST) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); } } - h_delta = delta; - EXEC SQL UPDATE au_authorization - SET amount = amount + :h_delta, updated_at = now() - WHERE auth_id = :h_id AND status = 'A'; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - if (audb_limit_dec(h_card, delta) < 0) FAIL(b); /* 음수 delta 면 사용액 환원 */ + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } - userlog("AUTH_AMT_ADJUST auth=%ld %ld -> %ld", h_id, h_amt, h_amt + delta); - setl(b, T_AMOUNT, h_amt + delta); - setl(b, T_RC, 0); + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_AMOUNT, h_amount); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/AUTH_APPROVE.pgc b/app/src/au/svc/AUTH_APPROVE.pgc index 714d926..1617762 100644 --- a/app/src/au/svc/AUTH_APPROVE.pgc +++ b/app/src/au/svc/AUTH_APPROVE.pgc @@ -7,11 +7,204 @@ */ #include "AUTH_APPROVE.h" +/* + * 심화 구현 노트 (AUTH_APPROVE): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void AUTH_APPROVE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long auth_id = getl(b, T_ID1); - if (audb_update_auth_status(auth_id, "A") < 0) FAIL(b); - Bchg(b, T_STATUS, 0, "A", 0L); + const char *svcname = "AUTH_APPROVE"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; + EXEC SQL BEGIN DECLARE SECTION; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; + EXEC SQL END DECLARE SECTION; + + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 승인 확정(pending->approved): dbio 래퍼로 상태만 A", svcname); + + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + h_auth = getl(b, T_ID1); + if (h_auth <= 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "식별자(ID) 범위 오류", 0L); userlog("[%s] 입력검증 실패: ID<=0", svcname); FAIL(b); } + + /* 2) 상태 조회부: 대상 승인의 현재 상태/금액/카드 확보 */ + if (h_auth <= 0) h_auth = getl(b, T_ID2); + EXEC SQL SELECT status, amount, card_no, to_char(biz_date,'YYYY-MM-DD') + INTO :h_status, :h_amount, :h_card, :h_bizdate + FROM au_authorization WHERE auth_id = :h_auth; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 31L); + Bchg(b, T_MSG, 0, "대상 승인건 없음", 0L); + userlog("[%s] 대상없음 auth_id=%ld", svcname, h_auth); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 32L); FAIL(b); } + strncpy(res_status, h_status, sizeof(res_status)-1); + + /* 3) 전이 가능여부 분기부: 허용 상태에서만 A 로 전이 */ + if (strchr("A", h_status[0]) == NULL) { + setl(b, T_RC, 33L); Bchg(b, T_MSG, 0, "상태전이 불가", 0L); + userlog("[%s] 전이불가 cur=%s auth_id=%ld", svcname, h_status, h_auth); rc = 33; + setl(b, T_RC, rc); goto respond; } + if (h_delta < 0) h_delta = -h_delta; + + /* 4) 본처리부: 원장 상태 전이 (+금액조정/취소원장) */ + EXEC SQL UPDATE au_authorization SET status = 'A', updated_at = now() + WHERE auth_id = :h_auth AND status IN ('A'); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 34L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 35L); + Bchg(b, T_MSG, 0, "반영 건수 0 (경합/이미전이)", 0L); rc = 35; goto respond; } + strcpy(res_status, "A"); rc = 0; + /* 금액조정형: 조정 델타를 승인금액에 반영 */ + if (h_delta > 0) { + EXEC SQL UPDATE au_authorization SET amount = amount + :h_delta, updated_at = now() + WHERE auth_id = :h_auth; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 36L); FAIL(b); } + h_amount += h_delta; + } + + /* 5) 이력/재집계부: 카드 당일 상태분포 재집계 */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 37L); FAIL(b); } + } + /* 6) 집계 갱신부: 상태 전이 결과 상위 카운트 */ + EXEC SQL SELECT count(*) INTO :h_v1 FROM au_authorization + WHERE card_no = :h_card AND status = 'A'; + if (sqlca.sqlcode < 0) h_v1 = 0; + /* 7) 예외/보상: 위 EXEC SQL 실패는 FAIL→XA 롤백으로 부분전이 방지 */ +respond: + outv = h_amount; avail = h_v1; + userlog("[%s] 전이 %s->%s auth_id=%ld rc=%ld", svcname, h_status, res_status, h_auth, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (AUTH_APPROVE) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + Bchg(b, T_STATUS, 0, res_status, 0L); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/AUTH_CANCEL.pgc b/app/src/au/svc/AUTH_CANCEL.pgc index b7e84c1..a9e3b34 100644 --- a/app/src/au/svc/AUTH_CANCEL.pgc +++ b/app/src/au/svc/AUTH_CANCEL.pgc @@ -6,32 +6,214 @@ */ #include "AUTH_CANCEL.h" +/* + * 심화 구현 노트 (AUTH_CANCEL): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void AUTH_CANCEL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24], bizdate[16], reason[128]; + const char *svcname = "AUTH_CANCEL"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_id, h_auth, h_amt; - char h_card[24], h_bizdate[16], h_reason[128]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - h_auth = getl(b, T_ID1); - h_amt = getl(b, T_AMOUNT); - gets_(b, T_KEY1, card, sizeof(card)); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "CUSTOMER"); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 승인 전체 취소 + 한도 원복.", svcname); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + gets_(b, T_REASON, h_reason, sizeof(h_reason)); + h_auth = getl(b, T_ID1); + h_amount = getl(b, T_AMOUNT); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_bizdate[0] == 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (h_reason[0] == 0) { setl(b, T_RC, 8803L); Bchg(b, T_MSG, 0, "사유 필수 누락", 0L); userlog("[%s] 입력검증 실패: 사유 누락", svcname); FAIL(b); } + if (h_auth <= 0) { setl(b, T_RC, 8804L); Bchg(b, T_MSG, 0, "식별자(ID) 범위 오류", 0L); userlog("[%s] 입력검증 실패: ID<=0", svcname); FAIL(b); } + if (h_amount < 0) { setl(b, T_RC, 8805L); Bchg(b, T_MSG, 0, "금액/수치 음수 불가", 0L); userlog("[%s] 입력검증 실패: 음수값", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8806L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 상태 조회부: 대상 승인의 현재 상태/금액/카드 확보 */ + if (h_auth <= 0) h_auth = getl(b, T_ID2); + EXEC SQL SELECT status, amount, card_no, to_char(biz_date,'YYYY-MM-DD') + INTO :h_status, :h_amount, :h_card, :h_bizdate + FROM au_authorization WHERE auth_id = :h_auth; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 31L); + Bchg(b, T_MSG, 0, "대상 승인건 없음", 0L); + userlog("[%s] 대상없음 auth_id=%ld", svcname, h_auth); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 32L); FAIL(b); } + strncpy(res_status, h_status, sizeof(res_status)-1); + + /* 3) 전이 가능여부 분기부: 허용 상태에서만 C 로 전이 */ + if (strchr("AM", h_status[0]) == NULL) { + setl(b, T_RC, 33L); Bchg(b, T_MSG, 0, "상태전이 불가", 0L); + userlog("[%s] 전이불가 cur=%s auth_id=%ld", svcname, h_status, h_auth); rc = 33; + setl(b, T_RC, rc); goto respond; } + if (h_delta < 0) h_delta = -h_delta; + + /* 4) 본처리부: 원장 상태 전이 (+금액조정/취소원장) */ + EXEC SQL UPDATE au_authorization SET status = 'C', updated_at = now() + WHERE auth_id = :h_auth AND status IN ('A','M'); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 34L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 35L); + Bchg(b, T_MSG, 0, "반영 건수 0 (경합/이미전이)", 0L); rc = 35; goto respond; } + strcpy(res_status, "C"); rc = 0; + /* 취소/역전/무효: 승인금액 한도 원복 */ + if (h_card[0] && h_amount > 0) { + EXEC SQL UPDATE card_limit SET used_amount = GREATEST(used_amount - :h_amount, 0), + updated_at = now() WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 36L); FAIL(b); } + } EXEC SQL SELECT nextval('au_cancel_seq') INTO :h_id; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL INSERT INTO au_cancel (cancel_id, auth_id, cancel_type, amount, reason, biz_date) - VALUES (:h_id, :h_auth, 'FULL', :h_amt, :h_reason, :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); - if (audb_update_auth_status(h_auth, "C") < 0) FAIL(b); - if (card[0] && audb_limit_dec(card, -h_amt) < 0) FAIL(b); + if (sqlca.sqlcode >= 0) + EXEC SQL INSERT INTO au_cancel (cancel_id, auth_id, cancel_type, amount, reason, biz_date) + VALUES (:h_id, :h_auth, 'FULL', :h_amount, :h_reason, :h_bizdate); + + /* 5) 이력/재집계부: 카드 당일 상태분포 재집계 */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 37L); FAIL(b); } + } + /* 6) 집계 갱신부: 상태 전이 결과 상위 카운트 */ + EXEC SQL SELECT count(*) INTO :h_v1 FROM au_authorization + WHERE card_no = :h_card AND status = 'C'; + if (sqlca.sqlcode < 0) h_v1 = 0; + /* 7) 예외/보상: 위 EXEC SQL 실패는 FAIL→XA 롤백으로 부분전이 방지 */ +respond: + outv = h_amount; avail = h_v1; + userlog("[%s] 전이 %s->%s auth_id=%ld rc=%ld", svcname, h_status, res_status, h_auth, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (AUTH_CANCEL) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ setl(b, T_CANCEL_ID, h_id); - Bchg(b, T_STATUS, 0, "C", 0L); + Bchg(b, T_STATUS, 0, res_status, 0L); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/AUTH_CAPTURE.pgc b/app/src/au/svc/AUTH_CAPTURE.pgc index 27cbcf2..166b95f 100644 --- a/app/src/au/svc/AUTH_CAPTURE.pgc +++ b/app/src/au/svc/AUTH_CAPTURE.pgc @@ -6,18 +6,204 @@ */ #include "AUTH_CAPTURE.h" +/* + * 심화 구현 노트 (AUTH_CAPTURE): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void AUTH_CAPTURE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *svcname = "AUTH_CAPTURE"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_auth; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; + + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 승인 매입확정(capture): 상태 A->M(매입대상).", svcname); + + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ h_auth = getl(b, T_ID1); - EXEC SQL UPDATE au_authorization SET status = 'M', captured_at = now(), updated_at = now() - WHERE auth_id = :h_auth AND status = 'A'; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlcode == 100) { setl(b, T_RC, 1); FAIL(b); } - setl(b, T_RC, 0); - Bchg(b, T_STATUS, 0, "M", 0L); + if (h_auth <= 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "식별자(ID) 범위 오류", 0L); userlog("[%s] 입력검증 실패: ID<=0", svcname); FAIL(b); } + + /* 2) 상태 조회부: 대상 승인의 현재 상태/금액/카드 확보 */ + if (h_auth <= 0) h_auth = getl(b, T_ID2); + EXEC SQL SELECT status, amount, card_no, to_char(biz_date,'YYYY-MM-DD') + INTO :h_status, :h_amount, :h_card, :h_bizdate + FROM au_authorization WHERE auth_id = :h_auth; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 31L); + Bchg(b, T_MSG, 0, "대상 승인건 없음", 0L); + userlog("[%s] 대상없음 auth_id=%ld", svcname, h_auth); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 32L); FAIL(b); } + strncpy(res_status, h_status, sizeof(res_status)-1); + + /* 3) 전이 가능여부 분기부: 허용 상태에서만 M 로 전이 */ + if (strchr("A", h_status[0]) == NULL) { + setl(b, T_RC, 33L); Bchg(b, T_MSG, 0, "상태전이 불가", 0L); + userlog("[%s] 전이불가 cur=%s auth_id=%ld", svcname, h_status, h_auth); rc = 33; + setl(b, T_RC, rc); goto respond; } + if (h_delta < 0) h_delta = -h_delta; + + /* 4) 본처리부: 원장 상태 전이 (+금액조정/취소원장) */ + EXEC SQL UPDATE au_authorization SET status = 'M', updated_at = now() + WHERE auth_id = :h_auth AND status IN ('A'); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 34L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 35L); + Bchg(b, T_MSG, 0, "반영 건수 0 (경합/이미전이)", 0L); rc = 35; goto respond; } + strcpy(res_status, "M"); rc = 0; + /* 금액조정형: 조정 델타를 승인금액에 반영 */ + if (h_delta > 0) { + EXEC SQL UPDATE au_authorization SET amount = amount + :h_delta, updated_at = now() + WHERE auth_id = :h_auth; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 36L); FAIL(b); } + h_amount += h_delta; + } + + /* 5) 이력/재집계부: 카드 당일 상태분포 재집계 */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 37L); FAIL(b); } + } + /* 6) 집계 갱신부: 상태 전이 결과 상위 카운트 */ + EXEC SQL SELECT count(*) INTO :h_v1 FROM au_authorization + WHERE card_no = :h_card AND status = 'M'; + if (sqlca.sqlcode < 0) h_v1 = 0; + /* 7) 예외/보상: 위 EXEC SQL 실패는 FAIL→XA 롤백으로 부분전이 방지 */ +respond: + outv = h_amount; avail = h_v1; + userlog("[%s] 전이 %s->%s auth_id=%ld rc=%ld", svcname, h_status, res_status, h_auth, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (AUTH_CAPTURE) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_RC, rc); + Bchg(b, T_STATUS, 0, res_status, 0L); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/AUTH_DECLINE.pgc b/app/src/au/svc/AUTH_DECLINE.pgc index 83c0cf1..a272139 100644 --- a/app/src/au/svc/AUTH_DECLINE.pgc +++ b/app/src/au/svc/AUTH_DECLINE.pgc @@ -6,20 +6,206 @@ */ #include "AUTH_DECLINE.h" +/* + * 심화 구현 노트 (AUTH_DECLINE): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void AUTH_DECLINE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char reason[128]; + const char *svcname = "AUTH_DECLINE"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_auth; - char h_reason[128]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; + + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 승인 거절 처리: 거절 사유와 함께 상태 D 로 갱신.", svcname); + + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_REASON, h_reason, sizeof(h_reason)); h_auth = getl(b, T_ID1); - gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "DECLINE"); - strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; - EXEC SQL UPDATE au_authorization SET status = 'D', decline_reason = :h_reason, updated_at = now() - WHERE auth_id = :h_auth; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - Bchg(b, T_STATUS, 0, "D", 0L); + if (h_reason[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "사유 필수 누락", 0L); userlog("[%s] 입력검증 실패: 사유 누락", svcname); FAIL(b); } + if (h_auth <= 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "식별자(ID) 범위 오류", 0L); userlog("[%s] 입력검증 실패: ID<=0", svcname); FAIL(b); } + + /* 2) 상태 조회부: 대상 승인의 현재 상태/금액/카드 확보 */ + if (h_auth <= 0) h_auth = getl(b, T_ID2); + EXEC SQL SELECT status, amount, card_no, to_char(biz_date,'YYYY-MM-DD') + INTO :h_status, :h_amount, :h_card, :h_bizdate + FROM au_authorization WHERE auth_id = :h_auth; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 31L); + Bchg(b, T_MSG, 0, "대상 승인건 없음", 0L); + userlog("[%s] 대상없음 auth_id=%ld", svcname, h_auth); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 32L); FAIL(b); } + strncpy(res_status, h_status, sizeof(res_status)-1); + + /* 3) 전이 가능여부 분기부: 허용 상태에서만 D 로 전이 */ + if (strchr("APH", h_status[0]) == NULL) { + setl(b, T_RC, 33L); Bchg(b, T_MSG, 0, "상태전이 불가", 0L); + userlog("[%s] 전이불가 cur=%s auth_id=%ld", svcname, h_status, h_auth); rc = 33; + setl(b, T_RC, rc); goto respond; } + if (h_delta < 0) h_delta = -h_delta; + + /* 4) 본처리부: 원장 상태 전이 (+금액조정/취소원장) */ + EXEC SQL UPDATE au_authorization SET status = 'D', updated_at = now() + WHERE auth_id = :h_auth AND status IN ('A','P','H'); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 34L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 35L); + Bchg(b, T_MSG, 0, "반영 건수 0 (경합/이미전이)", 0L); rc = 35; goto respond; } + strcpy(res_status, "D"); rc = 0; + /* 금액조정형: 조정 델타를 승인금액에 반영 */ + if (h_delta > 0) { + EXEC SQL UPDATE au_authorization SET amount = amount + :h_delta, updated_at = now() + WHERE auth_id = :h_auth; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 36L); FAIL(b); } + h_amount += h_delta; + } + + /* 5) 이력/재집계부: 카드 당일 상태분포 재집계 */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 37L); FAIL(b); } + } + /* 6) 집계 갱신부: 상태 전이 결과 상위 카운트 */ + EXEC SQL SELECT count(*) INTO :h_v1 FROM au_authorization + WHERE card_no = :h_card AND status = 'D'; + if (sqlca.sqlcode < 0) h_v1 = 0; + /* 7) 예외/보상: 위 EXEC SQL 실패는 FAIL→XA 롤백으로 부분전이 방지 */ +respond: + outv = h_amount; avail = h_v1; + userlog("[%s] 전이 %s->%s auth_id=%ld rc=%ld", svcname, h_status, res_status, h_auth, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (AUTH_DECLINE) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + Bchg(b, T_STATUS, 0, res_status, 0L); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/AUTH_DELAY_MON.pgc b/app/src/au/svc/AUTH_DELAY_MON.pgc index a4fe96d..b5423f4 100644 --- a/app/src/au/svc/AUTH_DELAY_MON.pgc +++ b/app/src/au/svc/AUTH_DELAY_MON.pgc @@ -6,34 +6,203 @@ */ #include "AUTH_DELAY_MON.h" +/* + * 심화 구현 노트 (AUTH_DELAY_MON): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void AUTH_DELAY_MON(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + const char *svcname = "AUTH_DELAY_MON"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_min, h_cnt = 0, h_avg = 0, h_max = 0; - char h_bizdate[16]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (bizdate[0] == 0) FAIL(b); - h_min = getl(b, T_ARG1); if (h_min <= 0) h_min = 30; - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 승인 지연 모니터: 승인(A) 상태로 기준 시간(분)을 넘겨 매입확정이 ", svcname); - EXEC SQL SELECT count(*), - CAST(coalesce(avg(EXTRACT(EPOCH FROM now() - created_at)),0) AS bigint), - CAST(coalesce(max(EXTRACT(EPOCH FROM now() - created_at)),0) AS bigint) - INTO :h_cnt, :h_avg, :h_max + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_v1 = getl(b, T_ARG1); + if (h_bizdate[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (h_v1 < 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "금액/수치 음수 불가", 0L); userlog("[%s] 입력검증 실패: 음수값", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8803L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 마스터/정책 조회부: 대상 원장 스칼라 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + count(*) FILTER (WHERE status IN ('A','M','S')), + count(*) FILTER (WHERE status='D'), count(*) FILTER (WHERE status='C') + INTO :h_cnt, :h_sum, :h_appr_cnt, :h_dec_cnt, :h_v1 FROM au_authorization - WHERE biz_date = :h_bizdate AND status = 'A' AND captured_at IS NULL - AND created_at < now() - CAST(:h_min AS bigint) * interval '1 minute'; - if (sqlca.sqlcode < 0) FAIL(b); + WHERE (:h_card = '' OR card_no = :h_card) + AND (:h_merch = '' OR merchant_id = :h_merch) + AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 101L); FAIL(b); } - if (h_cnt > 0) - userlog("AUTH_DELAY_MON 지연 %ld건 (기준 %ld분, 최대 %ld초)", h_cnt, h_min, h_max); + /* 3) 지표 분기부: 승인율/거절율 구간 판정 */ + if (h_cnt == 0) { rc = 0; h_v2 = 0; strcpy(outmsg, "실적 없음"); } + else { + h_v2 = h_appr_cnt * 10000L / h_cnt; /* 승인율 bps */ + h_v3 = h_dec_cnt * 10000L / h_cnt; /* 거절율 bps */ + if (h_v2 >= 9500) { rc = 0; strcpy(res_status, "GOOD"); } + else if (h_v2 >= 8000) { rc = 1; strcpy(res_status, "WARN"); } + else { rc = 2; strcpy(res_status, "POOR"); } + } + + /* 4) 본처리부: 평균단가/최고금액 등 파생지표 */ + EXEC SQL SELECT coalesce(avg(amount),0)::bigint, coalesce(max(amount),0) + INTO :h_score, :h_id FROM au_authorization + WHERE (:h_bizdate = '' OR biz_date = :h_bizdate::date) + AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 102L); FAIL(b); } + avail = h_score; + + /* 5) 재집계부: 가맹점 정책(mdr/한도) 참고 로드 */ + if (h_merch[0]) { + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_dlim, :h_mstat + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) strcpy(h_mstat, "NONE"); + else if (sqlca.sqlcode < 0) { setl(b, T_RC, 103L); FAIL(b); } + } + /* 6) 요약 갱신부: 가맹점 당일 집계 upsert(있을 때) */ + if (h_merch[0] && h_bizdate[0]) + audb_upsert_auth_summary(h_merch, h_bizdate, h_cnt, h_appr_cnt, h_dec_cnt, h_sum); + /* 7) 예외/보상: 조회/집계 전용 - 실패시 FAIL */ + outv = h_v2; + userlog("[%s] 집계 cnt=%ld appr=%ld rate=%ldbps rc=%ld", svcname, h_cnt, h_appr_cnt, h_v2, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (AUTH_DELAY_MON) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ setl(b, T_COUNT, h_cnt); - setl(b, T_ARG2, h_avg); - setl(b, T_ARG3, h_max); - setl(b, T_RC, (h_cnt > 0) ? 1 : 0); + setl(b, T_ARG2, h_v2); + setl(b, T_ARG3, h_v3); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/AUTH_DUP_CHK.pgc b/app/src/au/svc/AUTH_DUP_CHK.pgc index 7ee9fe4..6da609b 100644 --- a/app/src/au/svc/AUTH_DUP_CHK.pgc +++ b/app/src/au/svc/AUTH_DUP_CHK.pgc @@ -7,37 +7,201 @@ */ #include "AUTH_DUP_CHK.h" +/* + * 심화 구현 노트 (AUTH_DUP_CHK): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void AUTH_DUP_CHK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24], merch[64]; + const char *svcname = "AUTH_DUP_CHK"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_amt, h_cnt = 0, h_last = 0; - char h_card[24], h_merch[64]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - gets_(b, T_MERCHANT, merch, sizeof(merch)); - h_amt = getl(b, T_AMOUNT); - if (card[0] == 0 || merch[0] == 0 || h_amt <= 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; - strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 중복승인 검출: 단말 재시도 등으로 동일 카드+가맹점+금액 조합이 직전 ", svcname); - EXEC SQL SELECT count(*), coalesce(max(auth_id),0) INTO :h_cnt, :h_last + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + h_amount = getl(b, T_AMOUNT); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_merch[0] == 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "가맹점ID 필수 누락", 0L); userlog("[%s] 입력검증 실패: 가맹점ID 누락", svcname); FAIL(b); } + if (h_amount < 0) { setl(b, T_RC, 8803L); Bchg(b, T_MSG, 0, "금액/수치 음수 불가", 0L); userlog("[%s] 입력검증 실패: 음수값", svcname); FAIL(b); } + + /* 2) 마스터/정책 조회부: 대상 원장 스칼라 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + count(*) FILTER (WHERE status IN ('A','M','S')), + count(*) FILTER (WHERE status='D'), count(*) FILTER (WHERE status='C') + INTO :h_cnt, :h_sum, :h_appr_cnt, :h_dec_cnt, :h_v1 FROM au_authorization - WHERE card_no = :h_card AND merchant_id = :h_merch AND amount = :h_amt - AND status = 'A' - AND created_at >= now() - interval '5 minutes'; - if (sqlca.sqlcode < 0) FAIL(b); + WHERE (:h_card = '' OR card_no = :h_card) + AND (:h_merch = '' OR merchant_id = :h_merch) + AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 101L); FAIL(b); } - setl(b, T_ARG1, h_cnt); - if (h_cnt > 0) { - userlog("AUTH_DUP_CHK 중복 의심 card=%s merch=%s amt=%ld 원승인=%ld", card, merch, h_amt, h_last); - setl(b, T_ID1, h_last); - setl(b, T_RC, 1); - } else { - setl(b, T_ID1, 0); - setl(b, T_RC, 0); + /* 3) 지표 분기부: 승인율/거절율 구간 판정 */ + if (h_cnt == 0) { rc = 0; h_v2 = 0; strcpy(outmsg, "실적 없음"); } + else { + h_v2 = h_appr_cnt * 10000L / h_cnt; /* 승인율 bps */ + h_v3 = h_dec_cnt * 10000L / h_cnt; /* 거절율 bps */ + if (h_v2 >= 9500) { rc = 0; strcpy(res_status, "GOOD"); } + else if (h_v2 >= 8000) { rc = 1; strcpy(res_status, "WARN"); } + else { rc = 2; strcpy(res_status, "POOR"); } } + + /* 4) 본처리부: 평균단가/최고금액 등 파생지표 */ + EXEC SQL SELECT coalesce(avg(amount),0)::bigint, coalesce(max(amount),0) + INTO :h_score, :h_id FROM au_authorization + WHERE (:h_bizdate = '' OR biz_date = :h_bizdate::date) + AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 102L); FAIL(b); } + avail = h_score; + + /* 5) 재집계부: 가맹점 정책(mdr/한도) 참고 로드 */ + if (h_merch[0]) { + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_dlim, :h_mstat + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) strcpy(h_mstat, "NONE"); + else if (sqlca.sqlcode < 0) { setl(b, T_RC, 103L); FAIL(b); } + } + /* 6) 요약 갱신부: 가맹점 당일 집계 upsert(있을 때) */ + if (h_merch[0] && h_bizdate[0]) + audb_upsert_auth_summary(h_merch, h_bizdate, h_cnt, h_appr_cnt, h_dec_cnt, h_sum); + /* 7) 예외/보상: 조회/집계 전용 - 실패시 FAIL */ + outv = h_v2; + userlog("[%s] 집계 cnt=%ld appr=%ld rate=%ldbps rc=%ld", svcname, h_cnt, h_appr_cnt, h_v2, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (AUTH_DUP_CHK) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_ARG1, h_v1); + setl(b, T_ID1, h_auth); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/AUTH_EXPIRE_CHK.pgc b/app/src/au/svc/AUTH_EXPIRE_CHK.pgc index 6c745a1..8732d4a 100644 --- a/app/src/au/svc/AUTH_EXPIRE_CHK.pgc +++ b/app/src/au/svc/AUTH_EXPIRE_CHK.pgc @@ -7,38 +7,202 @@ */ #include "AUTH_EXPIRE_CHK.h" +/* + * 심화 구현 노트 (AUTH_EXPIRE_CHK): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void AUTH_EXPIRE_CHK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *svcname = "AUTH_EXPIRE_CHK"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_days, h_cnt = 0, h_amt = 0, h_oldest = 0; - char h_odate[16]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - h_days = getl(b, T_ARG1); if (h_days <= 0) h_days = 14; - memset(h_odate, 0, sizeof(h_odate)); + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 미매입 승인 만기 점검: 기준일수를 넘겨도 매입확정되지 않은 승인(A) ", svcname); - EXEC SQL SELECT count(*), coalesce(sum(amount),0) - INTO :h_cnt, :h_amt + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + h_v1 = getl(b, T_ARG1); + if (h_v1 < 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "금액/수치 음수 불가", 0L); userlog("[%s] 입력검증 실패: 음수값", svcname); FAIL(b); } + + /* 2) 마스터/정책 조회부: 대상 원장 스칼라 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + count(*) FILTER (WHERE status IN ('A','M','S')), + count(*) FILTER (WHERE status='D'), count(*) FILTER (WHERE status='C') + INTO :h_cnt, :h_sum, :h_appr_cnt, :h_dec_cnt, :h_v1 FROM au_authorization - WHERE status = 'A' AND captured_at IS NULL - AND biz_date < current_date - CAST(:h_days AS integer); - if (sqlca.sqlcode < 0) FAIL(b); + WHERE (:h_card = '' OR card_no = :h_card) + AND (:h_merch = '' OR merchant_id = :h_merch) + AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 101L); FAIL(b); } + /* 3) 지표 분기부: 승인율/거절율 구간 판정 */ + if (h_cnt == 0) { rc = 0; h_v2 = 0; strcpy(outmsg, "실적 없음"); } + else { + h_v2 = h_appr_cnt * 10000L / h_cnt; /* 승인율 bps */ + h_v3 = h_dec_cnt * 10000L / h_cnt; /* 거절율 bps */ + if (h_v2 >= 9500) { rc = 0; strcpy(res_status, "GOOD"); } + else if (h_v2 >= 8000) { rc = 1; strcpy(res_status, "WARN"); } + else { rc = 2; strcpy(res_status, "POOR"); } + } + + /* 4) 본처리부: 평균단가/최고금액 등 파생지표 */ + EXEC SQL SELECT coalesce(avg(amount),0)::bigint, coalesce(max(amount),0) + INTO :h_score, :h_id FROM au_authorization + WHERE (:h_bizdate = '' OR biz_date = :h_bizdate::date) + AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 102L); FAIL(b); } + avail = h_score; + + /* 5) 재집계부: 가맹점 정책(mdr/한도) 참고 로드 */ + if (h_merch[0]) { + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_dlim, :h_mstat + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) strcpy(h_mstat, "NONE"); + else if (sqlca.sqlcode < 0) { setl(b, T_RC, 103L); FAIL(b); } + } + /* 6) 요약 갱신부: 가맹점 당일 집계 upsert(있을 때) */ + if (h_merch[0] && h_bizdate[0]) + audb_upsert_auth_summary(h_merch, h_bizdate, h_cnt, h_appr_cnt, h_dec_cnt, h_sum); + /* 7) 예외/보상: 조회/집계 전용 - 실패시 FAIL */ + outv = h_v2; + userlog("[%s] 집계 cnt=%ld appr=%ld rate=%ldbps rc=%ld", svcname, h_cnt, h_appr_cnt, h_v2, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (AUTH_EXPIRE_CHK) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_amt); - if (h_cnt == 0) { setl(b, T_RC, 0); setl(b, T_ID1, 0); OK(b); } - - EXEC SQL SELECT auth_id, CAST(biz_date AS text) INTO :h_oldest, :h_odate - FROM au_authorization - WHERE status = 'A' AND captured_at IS NULL - AND biz_date < current_date - CAST(:h_days AS integer) - ORDER BY biz_date, auth_id LIMIT 1; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - - userlog("AUTH_EXPIRE_CHK 만기 %ld건 %ld원 (최고 오래된 auth=%ld %s)", h_cnt, h_amt, h_oldest, h_odate); - setl(b, T_ID1, h_oldest); - Bchg(b, T_STR1, 0, h_odate, 0L); - setl(b, T_RC, 1); + setl(b, T_GROSS, h_sum); + setl(b, T_RC, rc); + setl(b, T_ID1, h_auth); + Bchg(b, T_STR1, 0, (res_status[0] ? res_status : outs), 0L); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/AUTH_HISTORY.pgc b/app/src/au/svc/AUTH_HISTORY.pgc index fdfab53..53ecd3e 100644 --- a/app/src/au/svc/AUTH_HISTORY.pgc +++ b/app/src/au/svc/AUTH_HISTORY.pgc @@ -6,18 +6,198 @@ */ #include "AUTH_HISTORY.h" +/* + * 심화 구현 노트 (AUTH_HISTORY): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void AUTH_HISTORY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; + const char *svcname = "AUTH_HISTORY"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; - EXEC SQL SELECT count(*) INTO :h_cnt FROM au_authorization WHERE card_no = :h_card; - if (sqlca.sqlcode < 0) FAIL(b); + + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 카드 승인 이력 건수 조회.", svcname); + + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + + /* 2) 마스터/정책 조회부: 대상 원장 스칼라 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + count(*) FILTER (WHERE status IN ('A','M','S')), + count(*) FILTER (WHERE status='D'), count(*) FILTER (WHERE status='C') + INTO :h_cnt, :h_sum, :h_appr_cnt, :h_dec_cnt, :h_v1 + FROM au_authorization + WHERE (:h_card = '' OR card_no = :h_card) + AND (:h_merch = '' OR merchant_id = :h_merch) + AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 101L); FAIL(b); } + + /* 3) 지표 분기부: 승인율/거절율 구간 판정 */ + if (h_cnt == 0) { rc = 0; h_v2 = 0; strcpy(outmsg, "실적 없음"); } + else { + h_v2 = h_appr_cnt * 10000L / h_cnt; /* 승인율 bps */ + h_v3 = h_dec_cnt * 10000L / h_cnt; /* 거절율 bps */ + if (h_v2 >= 9500) { rc = 0; strcpy(res_status, "GOOD"); } + else if (h_v2 >= 8000) { rc = 1; strcpy(res_status, "WARN"); } + else { rc = 2; strcpy(res_status, "POOR"); } + } + + /* 4) 본처리부: 평균단가/최고금액 등 파생지표 */ + EXEC SQL SELECT coalesce(avg(amount),0)::bigint, coalesce(max(amount),0) + INTO :h_score, :h_id FROM au_authorization + WHERE (:h_bizdate = '' OR biz_date = :h_bizdate::date) + AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 102L); FAIL(b); } + avail = h_score; + + /* 5) 재집계부: 가맹점 정책(mdr/한도) 참고 로드 */ + if (h_merch[0]) { + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_dlim, :h_mstat + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) strcpy(h_mstat, "NONE"); + else if (sqlca.sqlcode < 0) { setl(b, T_RC, 103L); FAIL(b); } + } + /* 6) 요약 갱신부: 가맹점 당일 집계 upsert(있을 때) */ + if (h_merch[0] && h_bizdate[0]) + audb_upsert_auth_summary(h_merch, h_bizdate, h_cnt, h_appr_cnt, h_dec_cnt, h_sum); + /* 7) 예외/보상: 조회/집계 전용 - 실패시 FAIL */ + outv = h_v2; + userlog("[%s] 집계 cnt=%ld appr=%ld rate=%ldbps rc=%ld", svcname, h_cnt, h_appr_cnt, h_v2, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (AUTH_HISTORY) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ setl(b, T_COUNT, h_cnt); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/AUTH_HOLD.pgc b/app/src/au/svc/AUTH_HOLD.pgc index 61c3c5c..5bb675a 100644 --- a/app/src/au/svc/AUTH_HOLD.pgc +++ b/app/src/au/svc/AUTH_HOLD.pgc @@ -7,20 +7,204 @@ */ #include "AUTH_HOLD.h" +/* + * 심화 구현 노트 (AUTH_HOLD): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void AUTH_HOLD(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *svcname = "AUTH_HOLD"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_auth, h_aff; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 승인 보류: 승인/예비(A,P) 상태만 보류(H)로 전이, 반영 건수를 ", svcname); + + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ h_auth = getl(b, T_ID1); - /* only an approved(A)/pre-authorized(P) authorization may be put on hold */ + if (h_auth <= 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "식별자(ID) 범위 오류", 0L); userlog("[%s] 입력검증 실패: ID<=0", svcname); FAIL(b); } + + /* 2) 상태 조회부: 대상 승인의 현재 상태/금액/카드 확보 */ + if (h_auth <= 0) h_auth = getl(b, T_ID2); + EXEC SQL SELECT status, amount, card_no, to_char(biz_date,'YYYY-MM-DD') + INTO :h_status, :h_amount, :h_card, :h_bizdate + FROM au_authorization WHERE auth_id = :h_auth; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 31L); + Bchg(b, T_MSG, 0, "대상 승인건 없음", 0L); + userlog("[%s] 대상없음 auth_id=%ld", svcname, h_auth); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 32L); FAIL(b); } + strncpy(res_status, h_status, sizeof(res_status)-1); + + /* 3) 전이 가능여부 분기부: 허용 상태에서만 H 로 전이 */ + if (strchr("AP", h_status[0]) == NULL) { + setl(b, T_RC, 33L); Bchg(b, T_MSG, 0, "상태전이 불가", 0L); + userlog("[%s] 전이불가 cur=%s auth_id=%ld", svcname, h_status, h_auth); rc = 33; + setl(b, T_RC, rc); goto respond; } + if (h_delta < 0) h_delta = -h_delta; + + /* 4) 본처리부: 원장 상태 전이 (+금액조정/취소원장) */ EXEC SQL UPDATE au_authorization SET status = 'H', updated_at = now() - WHERE auth_id = :h_auth AND status IN ('A', 'P'); - if (sqlca.sqlcode < 0) { userlog("AUTH_HOLD FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } - h_aff = sqlca.sqlerrd[2]; - if (h_aff > 0) Bchg(b, T_STATUS, 0, "H", 0L); - setl(b, T_RC, h_aff > 0 ? 0 : 1); + WHERE auth_id = :h_auth AND status IN ('A','P'); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 34L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 35L); + Bchg(b, T_MSG, 0, "반영 건수 0 (경합/이미전이)", 0L); rc = 35; goto respond; } + strcpy(res_status, "H"); rc = 0; + /* 금액조정형: 조정 델타를 승인금액에 반영 */ + if (h_delta > 0) { + EXEC SQL UPDATE au_authorization SET amount = amount + :h_delta, updated_at = now() + WHERE auth_id = :h_auth; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 36L); FAIL(b); } + h_amount += h_delta; + } + + /* 5) 이력/재집계부: 카드 당일 상태분포 재집계 */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 37L); FAIL(b); } + } + /* 6) 집계 갱신부: 상태 전이 결과 상위 카운트 */ + EXEC SQL SELECT count(*) INTO :h_v1 FROM au_authorization + WHERE card_no = :h_card AND status = 'H'; + if (sqlca.sqlcode < 0) h_v1 = 0; + /* 7) 예외/보상: 위 EXEC SQL 실패는 FAIL→XA 롤백으로 부분전이 방지 */ +respond: + outv = h_amount; avail = h_v1; + userlog("[%s] 전이 %s->%s auth_id=%ld rc=%ld", svcname, h_status, res_status, h_auth, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (AUTH_HOLD) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_RC, rc); + Bchg(b, T_STATUS, 0, res_status, 0L); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/AUTH_PARTAPPR.pgc b/app/src/au/svc/AUTH_PARTAPPR.pgc index 79807ad..87a23f7 100644 --- a/app/src/au/svc/AUTH_PARTAPPR.pgc +++ b/app/src/au/svc/AUTH_PARTAPPR.pgc @@ -6,39 +6,216 @@ */ #include "AUTH_PARTAPPR.h" +/* + * 심화 구현 노트 (AUTH_PARTAPPR): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void AUTH_PARTAPPR(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24], merch[64], bizdate[16]; - long amount, avail, appr, aid, rc; + const char *svcname = "AUTH_PARTAPPR"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_daily = 0, h_used = 0; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - gets_(b, T_MERCHANT, merch, sizeof(merch)); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - amount = getl(b, T_AMOUNT); - if (card[0] == 0 || amount <= 0 || bizdate[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 부분 승인: 가용한도(일한도-사용액)가 요청액보다 작으면 가용한도만큼만", svcname); - EXEC SQL SELECT daily_limit, used_amount INTO :h_daily, :h_used - FROM card_limit WHERE card_no = :h_card; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ - avail = h_daily - h_used; - if (avail <= 0) { setl(b, T_AMT1, 0); setl(b, T_RC, 2); OK(b); } + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_merch[0] == 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "가맹점ID 필수 누락", 0L); userlog("[%s] 입력검증 실패: 가맹점ID 누락", svcname); FAIL(b); } + if (h_bizdate[0] == 0) { setl(b, T_RC, 8803L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (h_amount < 0) { setl(b, T_RC, 8804L); Bchg(b, T_MSG, 0, "금액/수치 음수 불가", 0L); userlog("[%s] 입력검증 실패: 음수값", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8805L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } - appr = (amount <= avail) ? amount : avail; - rc = (appr == amount) ? 0 : 1; - aid = audb_next_auth_id(); - if (aid < 0) FAIL(b); - if (audb_insert_auth(aid, card, merch, appr, "PART", "AUTH", "A", bizdate) < 0) FAIL(b); - if (audb_limit_dec(card, appr) < 0) FAIL(b); + /* 2) 마스터/정책 조회부: 카드 한도원장 + 가맹점 상태 */ + if (audb_limit_get(h_card, &h_limit, &h_used) < 0) { + setl(b, T_RC, 21L); Bchg(b, T_MSG, 0, "한도원장 조회 실패", 0L); + userlog("[%s] 한도원장 조회 실패 card=%s", svcname, h_card); FAIL(b); } + avail = h_limit - h_used; + if (h_merch[0]) { + EXEC SQL SELECT status, mdr_bps, daily_limit INTO :h_mstat, :h_mdr, :h_dlim + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { strcpy(h_mstat, "NONE"); } + else if (sqlca.sqlcode < 0) { setl(b, T_RC, 22L); FAIL(b); } + } - setl(b, T_ID2, aid); - setl(b, T_AMT1, appr); + /* 3) 업무규칙 분기부: 한도/가맹점상태/금액 구간별 승인 판정 */ + if (strcmp(h_mstat, "CLOSED") == 0) { + strcpy(res_status, "D"); strcpy(h_reason, "MERCH_CLOSED"); rc = 3; + } else if (h_amount <= 0) { + strcpy(res_status, "D"); strcpy(h_reason, "ZERO_AMT"); rc = 4; + } else if (h_used + h_amount > h_limit) { + strcpy(res_status, "D"); strcpy(h_reason, "OVER_LIMIT"); rc = 1; + } else if (h_amount >= 1000000) { + strcpy(res_status, "A"); strcpy(h_reason, "HIGH_OK"); rc = 0; + } else { + strcpy(res_status, "A"); strcpy(h_reason, "OK"); rc = 0; + } + + /* 4) 본처리부: 승인 원장 채번·삽입 (+승인시 한도 차감) */ + h_auth = audb_next_auth_id(); + if (h_auth < 0) { setl(b, T_RC, 23L); FAIL(b); } + sprintf(h_appr, "%06ld", h_auth % 1000000L); + if (audb_insert_auth(h_auth, h_card, h_merch, h_amount, h_appr, + "AUTH", res_status, h_bizdate) < 0) { + setl(b, T_RC, 24L); Bchg(b, T_MSG, 0, "승인원장 적재 실패", 0L); FAIL(b); } + if (res_status[0] == 'A') { + if (audb_limit_dec(h_card, h_amount) < 0) { + setl(b, T_RC, 25L); Bchg(b, T_MSG, 0, "한도 차감 실패", 0L); FAIL(b); } + h_used += h_amount; avail = h_limit - h_used; + } + + /* 5) 이력/재집계부: 당일 카드 승인 실적 재집계(감사 요약) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 26L); FAIL(b); } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계 upsert */ + if (h_merch[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status='A'), + count(*) FILTER (WHERE status='D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode >= 0) + audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + /* 7) 예외/보상: 개별 EXEC SQL 실패시 상단에서 FAIL→TPFAIL 롤백 (부분처리 방지) */ + outv = h_amount; avail = h_limit - h_used; + userlog("[%s] 판정 %s auth_id=%ld amt=%ld avail=%ld", svcname, res_status, h_auth, h_amount, avail); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (AUTH_PARTAPPR) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_AMT1, h_limit); setl(b, T_RC, rc); + setl(b, T_ID2, h_id); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/AUTH_PARTCXL.pgc b/app/src/au/svc/AUTH_PARTCXL.pgc index 8834d55..1d80f73 100644 --- a/app/src/au/svc/AUTH_PARTCXL.pgc +++ b/app/src/au/svc/AUTH_PARTCXL.pgc @@ -6,32 +6,212 @@ */ #include "AUTH_PARTCXL.h" +/* + * 심화 구현 노트 (AUTH_PARTCXL): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void AUTH_PARTCXL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24], bizdate[16]; + const char *svcname = "AUTH_PARTCXL"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_id, h_auth, h_amt, h_new; - char h_bizdate[16]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - h_auth = getl(b, T_ID1); - h_amt = getl(b, T_AMOUNT); /* 취소 금액 */ - gets_(b, T_KEY1, card, sizeof(card)); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 부분 취소: 승인 금액 감액 + 한도 부분 원복.", svcname); - EXEC SQL SELECT amount INTO :h_new FROM au_authorization WHERE auth_id = :h_auth; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - h_new = h_new - h_amt; + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_auth = getl(b, T_ID1); + h_amount = getl(b, T_AMOUNT); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_bizdate[0] == 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (h_auth <= 0) { setl(b, T_RC, 8803L); Bchg(b, T_MSG, 0, "식별자(ID) 범위 오류", 0L); userlog("[%s] 입력검증 실패: ID<=0", svcname); FAIL(b); } + if (h_amount < 0) { setl(b, T_RC, 8804L); Bchg(b, T_MSG, 0, "금액/수치 음수 불가", 0L); userlog("[%s] 입력검증 실패: 음수값", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8805L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 상태 조회부: 대상 승인의 현재 상태/금액/카드 확보 */ + if (h_auth <= 0) h_auth = getl(b, T_ID2); + EXEC SQL SELECT status, amount, card_no, to_char(biz_date,'YYYY-MM-DD') + INTO :h_status, :h_amount, :h_card, :h_bizdate + FROM au_authorization WHERE auth_id = :h_auth; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 31L); + Bchg(b, T_MSG, 0, "대상 승인건 없음", 0L); + userlog("[%s] 대상없음 auth_id=%ld", svcname, h_auth); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 32L); FAIL(b); } + strncpy(res_status, h_status, sizeof(res_status)-1); + + /* 3) 전이 가능여부 분기부: 허용 상태에서만 A 로 전이 */ + if (strchr("AM", h_status[0]) == NULL) { + setl(b, T_RC, 33L); Bchg(b, T_MSG, 0, "상태전이 불가", 0L); + userlog("[%s] 전이불가 cur=%s auth_id=%ld", svcname, h_status, h_auth); rc = 33; + setl(b, T_RC, rc); goto respond; } + if (h_delta < 0) h_delta = -h_delta; + + /* 4) 본처리부: 원장 상태 전이 (+금액조정/취소원장) */ + EXEC SQL UPDATE au_authorization SET status = 'A', updated_at = now() + WHERE auth_id = :h_auth AND status IN ('A','M'); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 34L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 35L); + Bchg(b, T_MSG, 0, "반영 건수 0 (경합/이미전이)", 0L); rc = 35; goto respond; } + strcpy(res_status, "A"); rc = 0; + /* 취소/역전/무효: 승인금액 한도 원복 */ + if (h_card[0] && h_amount > 0) { + EXEC SQL UPDATE card_limit SET used_amount = GREATEST(used_amount - :h_amount, 0), + updated_at = now() WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 36L); FAIL(b); } + } EXEC SQL SELECT nextval('au_cancel_seq') INTO :h_id; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL INSERT INTO au_cancel (cancel_id, auth_id, cancel_type, amount, reason, biz_date) - VALUES (:h_id, :h_auth, 'PARTIAL', :h_amt, 'PARTIAL', :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); - if (audb_update_auth_amount(h_auth, h_new) < 0) FAIL(b); - if (card[0] && audb_limit_dec(card, -h_amt) < 0) FAIL(b); + if (sqlca.sqlcode >= 0) + EXEC SQL INSERT INTO au_cancel (cancel_id, auth_id, cancel_type, amount, reason, biz_date) + VALUES (:h_id, :h_auth, 'FULL', :h_amount, :h_reason, :h_bizdate); + + /* 5) 이력/재집계부: 카드 당일 상태분포 재집계 */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 37L); FAIL(b); } + } + /* 6) 집계 갱신부: 상태 전이 결과 상위 카운트 */ + EXEC SQL SELECT count(*) INTO :h_v1 FROM au_authorization + WHERE card_no = :h_card AND status = 'A'; + if (sqlca.sqlcode < 0) h_v1 = 0; + /* 7) 예외/보상: 위 EXEC SQL 실패는 FAIL→XA 롤백으로 부분전이 방지 */ +respond: + outv = h_amount; avail = h_v1; + userlog("[%s] 전이 %s->%s auth_id=%ld rc=%ld", svcname, h_status, res_status, h_auth, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (AUTH_PARTCXL) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ setl(b, T_CANCEL_ID, h_id); - setl(b, T_AMOUNT, h_new); + setl(b, T_AMOUNT, h_amount); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/AUTH_PARTIAL.pgc b/app/src/au/svc/AUTH_PARTIAL.pgc index d9d97e6..95893b8 100644 --- a/app/src/au/svc/AUTH_PARTIAL.pgc +++ b/app/src/au/svc/AUTH_PARTIAL.pgc @@ -6,42 +6,217 @@ */ #include "AUTH_PARTIAL.h" +/* + * 심화 구현 노트 (AUTH_PARTIAL): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void AUTH_PARTIAL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24], merch[64], bizdate[16], appr[12]; - long amount, remain, grant, auth_id, lim = 0, used = 0; + const char *svcname = "AUTH_PARTIAL"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; + EXEC SQL BEGIN DECLARE SECTION; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; + EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - gets_(b, T_MERCHANT, merch, sizeof(merch)); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - amount = getl(b, T_AMOUNT); - if (card[0] == 0 || amount <= 0) { userlog("AUTH_PARTIAL: 인자 오류"); FAIL(b); } + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 부분승인: 잔여한도가 요청금액에 못 미치면 잔여한도만큼만 승인(RC=2)", svcname); - if (audb_limit_get(card, &lim, &used) < 0) FAIL(b); - remain = lim - used; - if (remain < 0) remain = 0; + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ - auth_id = audb_next_auth_id(); - if (auth_id < 0) FAIL(b); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_merch[0] == 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "가맹점ID 필수 누락", 0L); userlog("[%s] 입력검증 실패: 가맹점ID 누락", svcname); FAIL(b); } + if (h_bizdate[0] == 0) { setl(b, T_RC, 8803L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (h_amount < 0) { setl(b, T_RC, 8804L); Bchg(b, T_MSG, 0, "금액/수치 음수 불가", 0L); userlog("[%s] 입력검증 실패: 음수값", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8805L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } - if (remain == 0) { /* 잔여한도 전무 - 거절 기록 */ - if (audb_insert_auth(auth_id, card, merch, amount, "000000", "AUTH", "D", bizdate) < 0) FAIL(b); - setl(b, T_ID1, auth_id); - setl(b, T_AMT1, 0); - setl(b, T_RC, 1); - OK(b); + /* 2) 마스터/정책 조회부: 카드 한도원장 + 가맹점 상태 */ + if (audb_limit_get(h_card, &h_limit, &h_used) < 0) { + setl(b, T_RC, 21L); Bchg(b, T_MSG, 0, "한도원장 조회 실패", 0L); + userlog("[%s] 한도원장 조회 실패 card=%s", svcname, h_card); FAIL(b); } + avail = h_limit - h_used; + if (h_merch[0]) { + EXEC SQL SELECT status, mdr_bps, daily_limit INTO :h_mstat, :h_mdr, :h_dlim + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { strcpy(h_mstat, "NONE"); } + else if (sqlca.sqlcode < 0) { setl(b, T_RC, 22L); FAIL(b); } } - grant = (amount <= remain) ? amount : remain; - sprintf(appr, "P%05ld", auth_id % 100000); - if (audb_insert_auth(auth_id, card, merch, grant, appr, "AUTH", "A", bizdate) < 0) FAIL(b); - if (audb_limit_dec(card, grant) < 0) FAIL(b); + /* 3) 업무규칙 분기부: 한도/가맹점상태/금액 구간별 승인 판정 */ + if (strcmp(h_mstat, "CLOSED") == 0) { + strcpy(res_status, "D"); strcpy(h_reason, "MERCH_CLOSED"); rc = 3; + } else if (h_amount <= 0) { + strcpy(res_status, "D"); strcpy(h_reason, "ZERO_AMT"); rc = 4; + } else if (h_used + h_amount > h_limit) { + strcpy(res_status, "D"); strcpy(h_reason, "OVER_LIMIT"); rc = 1; + } else if (h_amount >= 1000000) { + strcpy(res_status, "A"); strcpy(h_reason, "HIGH_OK"); rc = 0; + } else { + strcpy(res_status, "A"); strcpy(h_reason, "OK"); rc = 0; + } - userlog("AUTH_PARTIAL card=%s 요청=%ld 승인=%ld (잔여한도 %ld)", card, amount, grant, remain); - setl(b, T_ID1, auth_id); - setl(b, T_AMT1, grant); - Bchg(b, T_STR2, 0, appr, 0L); - setl(b, T_RC, (grant < amount) ? 2 : 0); + /* 4) 본처리부: 승인 원장 채번·삽입 (+승인시 한도 차감) */ + h_auth = audb_next_auth_id(); + if (h_auth < 0) { setl(b, T_RC, 23L); FAIL(b); } + sprintf(h_appr, "%06ld", h_auth % 1000000L); + if (audb_insert_auth(h_auth, h_card, h_merch, h_amount, h_appr, + "AUTH", res_status, h_bizdate) < 0) { + setl(b, T_RC, 24L); Bchg(b, T_MSG, 0, "승인원장 적재 실패", 0L); FAIL(b); } + if (res_status[0] == 'A') { + if (audb_limit_dec(h_card, h_amount) < 0) { + setl(b, T_RC, 25L); Bchg(b, T_MSG, 0, "한도 차감 실패", 0L); FAIL(b); } + h_used += h_amount; avail = h_limit - h_used; + } + + /* 5) 이력/재집계부: 당일 카드 승인 실적 재집계(감사 요약) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 26L); FAIL(b); } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계 upsert */ + if (h_merch[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status='A'), + count(*) FILTER (WHERE status='D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode >= 0) + audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + /* 7) 예외/보상: 개별 EXEC SQL 실패시 상단에서 FAIL→TPFAIL 롤백 (부분처리 방지) */ + outv = h_amount; avail = h_limit - h_used; + userlog("[%s] 판정 %s auth_id=%ld amt=%ld avail=%ld", svcname, res_status, h_auth, h_amount, avail); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (AUTH_PARTIAL) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_ID1, h_auth); + setl(b, T_AMT1, h_limit); + setl(b, T_RC, rc); + Bchg(b, T_STR2, 0, h_appr, 0L); /* 승인번호 (계약 보존) */ + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/AUTH_RELEASE.pgc b/app/src/au/svc/AUTH_RELEASE.pgc index 1694fe4..d459318 100644 --- a/app/src/au/svc/AUTH_RELEASE.pgc +++ b/app/src/au/svc/AUTH_RELEASE.pgc @@ -7,23 +7,204 @@ */ #include "AUTH_RELEASE.h" +/* + * 심화 구현 노트 (AUTH_RELEASE): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void AUTH_RELEASE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *svcname = "AUTH_RELEASE"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_auth, h_aff; - char h_prev[8]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 보류 해제: 직전 상태를 조회한 뒤 H->A 전이, 이전 상태를 함께 회", svcname); + + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ h_auth = getl(b, T_ID1); - EXEC SQL SELECT status INTO :h_prev FROM au_authorization WHERE auth_id = :h_auth; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - /* release only from HOLD; report whether the transition actually fired */ + if (h_auth <= 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "식별자(ID) 범위 오류", 0L); userlog("[%s] 입력검증 실패: ID<=0", svcname); FAIL(b); } + + /* 2) 상태 조회부: 대상 승인의 현재 상태/금액/카드 확보 */ + if (h_auth <= 0) h_auth = getl(b, T_ID2); + EXEC SQL SELECT status, amount, card_no, to_char(biz_date,'YYYY-MM-DD') + INTO :h_status, :h_amount, :h_card, :h_bizdate + FROM au_authorization WHERE auth_id = :h_auth; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 31L); + Bchg(b, T_MSG, 0, "대상 승인건 없음", 0L); + userlog("[%s] 대상없음 auth_id=%ld", svcname, h_auth); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 32L); FAIL(b); } + strncpy(res_status, h_status, sizeof(res_status)-1); + + /* 3) 전이 가능여부 분기부: 허용 상태에서만 A 로 전이 */ + if (strchr("H", h_status[0]) == NULL) { + setl(b, T_RC, 33L); Bchg(b, T_MSG, 0, "상태전이 불가", 0L); + userlog("[%s] 전이불가 cur=%s auth_id=%ld", svcname, h_status, h_auth); rc = 33; + setl(b, T_RC, rc); goto respond; } + if (h_delta < 0) h_delta = -h_delta; + + /* 4) 본처리부: 원장 상태 전이 (+금액조정/취소원장) */ EXEC SQL UPDATE au_authorization SET status = 'A', updated_at = now() - WHERE auth_id = :h_auth AND status = 'H'; - if (sqlca.sqlcode < 0) FAIL(b); - h_aff = sqlca.sqlerrd[2]; - Bchg(b, T_STATUS, 0, h_aff > 0 ? "A" : h_prev, 0L); - setl(b, T_RC, h_aff > 0 ? 0 : 1); + WHERE auth_id = :h_auth AND status IN ('H'); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 34L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 35L); + Bchg(b, T_MSG, 0, "반영 건수 0 (경합/이미전이)", 0L); rc = 35; goto respond; } + strcpy(res_status, "A"); rc = 0; + /* 금액조정형: 조정 델타를 승인금액에 반영 */ + if (h_delta > 0) { + EXEC SQL UPDATE au_authorization SET amount = amount + :h_delta, updated_at = now() + WHERE auth_id = :h_auth; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 36L); FAIL(b); } + h_amount += h_delta; + } + + /* 5) 이력/재집계부: 카드 당일 상태분포 재집계 */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 37L); FAIL(b); } + } + /* 6) 집계 갱신부: 상태 전이 결과 상위 카운트 */ + EXEC SQL SELECT count(*) INTO :h_v1 FROM au_authorization + WHERE card_no = :h_card AND status = 'A'; + if (sqlca.sqlcode < 0) h_v1 = 0; + /* 7) 예외/보상: 위 EXEC SQL 실패는 FAIL→XA 롤백으로 부분전이 방지 */ +respond: + outv = h_amount; avail = h_v1; + userlog("[%s] 전이 %s->%s auth_id=%ld rc=%ld", svcname, h_status, res_status, h_auth, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (AUTH_RELEASE) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_RC, rc); + Bchg(b, T_STATUS, 0, res_status, 0L); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/AUTH_REPROC_REQ.pgc b/app/src/au/svc/AUTH_REPROC_REQ.pgc index 4a5f6eb..e1296eb 100644 --- a/app/src/au/svc/AUTH_REPROC_REQ.pgc +++ b/app/src/au/svc/AUTH_REPROC_REQ.pgc @@ -6,29 +6,204 @@ */ #include "AUTH_REPROC_REQ.h" +/* + * 심화 구현 노트 (AUTH_REPROC_REQ): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void AUTH_REPROC_REQ(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *svcname = "AUTH_REPROC_REQ"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_auth; - char h_old[4]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - h_auth = getl(b, T_ID2); - if (h_auth <= 0) FAIL(b); - memset(h_old, 0, sizeof(h_old)); + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 승인 재처리 요청: 거절(D)/보류(H) 상태의 승인을 재처리 대상으로 ", svcname); - EXEC SQL SELECT status INTO :h_old FROM au_authorization + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + h_id = getl(b, T_ID2); + if (h_id <= 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "식별자(ID) 범위 오류", 0L); userlog("[%s] 입력검증 실패: ID<=0", svcname); FAIL(b); } + + /* 2) 상태 조회부: 대상 승인의 현재 상태/금액/카드 확보 */ + if (h_auth <= 0) h_auth = getl(b, T_ID2); + EXEC SQL SELECT status, amount, card_no, to_char(biz_date,'YYYY-MM-DD') + INTO :h_status, :h_amount, :h_card, :h_bizdate + FROM au_authorization WHERE auth_id = :h_auth; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 31L); + Bchg(b, T_MSG, 0, "대상 승인건 없음", 0L); + userlog("[%s] 대상없음 auth_id=%ld", svcname, h_auth); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 32L); FAIL(b); } + strncpy(res_status, h_status, sizeof(res_status)-1); + + /* 3) 전이 가능여부 분기부: 허용 상태에서만 H 로 전이 */ + if (strchr("DH", h_status[0]) == NULL) { + setl(b, T_RC, 33L); Bchg(b, T_MSG, 0, "상태전이 불가", 0L); + userlog("[%s] 전이불가 cur=%s auth_id=%ld", svcname, h_status, h_auth); rc = 33; + setl(b, T_RC, rc); goto respond; } + if (h_delta < 0) h_delta = -h_delta; + + /* 4) 본처리부: 원장 상태 전이 (+금액조정/취소원장) */ + EXEC SQL UPDATE au_authorization SET status = 'H', updated_at = now() WHERE auth_id = :h_auth AND status IN ('D','H'); - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlcode == 100) { setl(b, T_RC, 1); OK(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 34L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 35L); + Bchg(b, T_MSG, 0, "반영 건수 0 (경합/이미전이)", 0L); rc = 35; goto respond; } + strcpy(res_status, "H"); rc = 0; + /* 금액조정형: 조정 델타를 승인금액에 반영 */ + if (h_delta > 0) { + EXEC SQL UPDATE au_authorization SET amount = amount + :h_delta, updated_at = now() + WHERE auth_id = :h_auth; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 36L); FAIL(b); } + h_amount += h_delta; + } - EXEC SQL UPDATE au_authorization SET status = 'A', updated_at = now() - WHERE auth_id = :h_auth; - if (sqlca.sqlcode < 0) FAIL(b); + /* 5) 이력/재집계부: 카드 당일 상태분포 재집계 */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 37L); FAIL(b); } + } + /* 6) 집계 갱신부: 상태 전이 결과 상위 카운트 */ + EXEC SQL SELECT count(*) INTO :h_v1 FROM au_authorization + WHERE card_no = :h_card AND status = 'H'; + if (sqlca.sqlcode < 0) h_v1 = 0; + /* 7) 예외/보상: 위 EXEC SQL 실패는 FAIL→XA 롤백으로 부분전이 방지 */ +respond: + outv = h_amount; avail = h_v1; + userlog("[%s] 전이 %s->%s auth_id=%ld rc=%ld", svcname, h_status, res_status, h_auth, rc); - userlog("AUTH_REPROC_REQ auth=%ld %s->A", h_auth, h_old); - Bchg(b, T_STATUS, 0, h_old, 0L); - setl(b, T_RC, 0); + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (AUTH_REPROC_REQ) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_RC, rc); + Bchg(b, T_STATUS, 0, res_status, 0L); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/AUTH_REPROC_STAT.pgc b/app/src/au/svc/AUTH_REPROC_STAT.pgc index 97339b8..1654b08 100644 --- a/app/src/au/svc/AUTH_REPROC_STAT.pgc +++ b/app/src/au/svc/AUTH_REPROC_STAT.pgc @@ -6,27 +6,200 @@ */ #include "AUTH_REPROC_STAT.h" +/* + * 심화 구현 노트 (AUTH_REPROC_STAT): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void AUTH_REPROC_STAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + const char *svcname = "AUTH_REPROC_STAT"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_hold = 0, h_reproc = 0; - char h_bizdate[16]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (bizdate[0] == 0) FAIL(b); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 재처리 현황: 영업일 승인 원장에서 보류(H) 대기 건수와, 갱신시각이 ", svcname); - EXEC SQL SELECT count(*) FILTER (WHERE status = 'H'), - count(*) FILTER (WHERE updated_at > created_at) - INTO :h_hold, :h_reproc - FROM au_authorization WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + h_amount = getl(b, T_AMOUNT); - setl(b, T_ARG1, h_hold); - setl(b, T_ARG2, h_reproc); - setl(b, T_RC, (h_hold > 0) ? 1 : 0); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_bizdate[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 마스터/정책 조회부: 대상 원장 스칼라 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + count(*) FILTER (WHERE status IN ('A','M','S')), + count(*) FILTER (WHERE status='D'), count(*) FILTER (WHERE status='C') + INTO :h_cnt, :h_sum, :h_appr_cnt, :h_dec_cnt, :h_v1 + FROM au_authorization + WHERE (:h_card = '' OR card_no = :h_card) + AND (:h_merch = '' OR merchant_id = :h_merch) + AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 101L); FAIL(b); } + + /* 3) 지표 분기부: 승인율/거절율 구간 판정 */ + if (h_cnt == 0) { rc = 0; h_v2 = 0; strcpy(outmsg, "실적 없음"); } + else { + h_v2 = h_appr_cnt * 10000L / h_cnt; /* 승인율 bps */ + h_v3 = h_dec_cnt * 10000L / h_cnt; /* 거절율 bps */ + if (h_v2 >= 9500) { rc = 0; strcpy(res_status, "GOOD"); } + else if (h_v2 >= 8000) { rc = 1; strcpy(res_status, "WARN"); } + else { rc = 2; strcpy(res_status, "POOR"); } + } + + /* 4) 본처리부: 평균단가/최고금액 등 파생지표 */ + EXEC SQL SELECT coalesce(avg(amount),0)::bigint, coalesce(max(amount),0) + INTO :h_score, :h_id FROM au_authorization + WHERE (:h_bizdate = '' OR biz_date = :h_bizdate::date) + AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 102L); FAIL(b); } + avail = h_score; + + /* 5) 재집계부: 가맹점 정책(mdr/한도) 참고 로드 */ + if (h_merch[0]) { + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_dlim, :h_mstat + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) strcpy(h_mstat, "NONE"); + else if (sqlca.sqlcode < 0) { setl(b, T_RC, 103L); FAIL(b); } + } + /* 6) 요약 갱신부: 가맹점 당일 집계 upsert(있을 때) */ + if (h_merch[0] && h_bizdate[0]) + audb_upsert_auth_summary(h_merch, h_bizdate, h_cnt, h_appr_cnt, h_dec_cnt, h_sum); + /* 7) 예외/보상: 조회/집계 전용 - 실패시 FAIL */ + outv = h_v2; + userlog("[%s] 집계 cnt=%ld appr=%ld rate=%ldbps rc=%ld", svcname, h_cnt, h_appr_cnt, h_v2, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (AUTH_REPROC_STAT) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_ARG1, h_v1); + setl(b, T_ARG2, h_v2); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/AUTH_RESEND.pgc b/app/src/au/svc/AUTH_RESEND.pgc index f61b052..b6e50a1 100644 --- a/app/src/au/svc/AUTH_RESEND.pgc +++ b/app/src/au/svc/AUTH_RESEND.pgc @@ -7,36 +7,206 @@ */ #include "AUTH_RESEND.h" +/* + * 심화 구현 노트 (AUTH_RESEND): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void AUTH_RESEND(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *svcname = "AUTH_RESEND"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_id, h_amt = 0; - char h_appr[16], h_status[4]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - h_id = getl(b, T_ID1); - if (h_id <= 0) { userlog("AUTH_RESEND: 승인번호 누락"); FAIL(b); } - memset(h_appr, 0, sizeof(h_appr)); - memset(h_status, 0, sizeof(h_status)); + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 승인 재전송: 망 단절 등으로 응답을 못 받은 단말이 원승인의 승인코드를", svcname); - EXEC SQL SELECT approval_code, amount, status INTO :h_appr, :h_amt, :h_status - FROM au_authorization WHERE auth_id = :h_id; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); - Bchg(b, T_STATUS, 0, h_status, 0L); - setl(b, T_AMOUNT, h_amt); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + h_auth = getl(b, T_ID1); + if (h_auth <= 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "식별자(ID) 범위 오류", 0L); userlog("[%s] 입력검증 실패: ID<=0", svcname); FAIL(b); } - if (h_status[0] != 'A' && h_status[0] != 'M') { /* 취소/역전/거절 건은 재전송 불가 */ - setl(b, T_RC, 1); - OK(b); + /* 2) 상태 조회부: 대상 승인의 현재 상태/금액/카드 확보 */ + if (h_auth <= 0) h_auth = getl(b, T_ID2); + EXEC SQL SELECT status, amount, card_no, to_char(biz_date,'YYYY-MM-DD') + INTO :h_status, :h_amount, :h_card, :h_bizdate + FROM au_authorization WHERE auth_id = :h_auth; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 31L); + Bchg(b, T_MSG, 0, "대상 승인건 없음", 0L); + userlog("[%s] 대상없음 auth_id=%ld", svcname, h_auth); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 32L); FAIL(b); } + strncpy(res_status, h_status, sizeof(res_status)-1); + + /* 3) 전이 가능여부 분기부: 허용 상태에서만 A 로 전이 */ + if (strchr("A", h_status[0]) == NULL) { + setl(b, T_RC, 33L); Bchg(b, T_MSG, 0, "상태전이 불가", 0L); + userlog("[%s] 전이불가 cur=%s auth_id=%ld", svcname, h_status, h_auth); rc = 33; + setl(b, T_RC, rc); goto respond; } + if (h_delta < 0) h_delta = -h_delta; + + /* 4) 본처리부: 원장 상태 전이 (+금액조정/취소원장) */ + EXEC SQL UPDATE au_authorization SET status = 'A', updated_at = now() + WHERE auth_id = :h_auth AND status IN ('A'); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 34L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 35L); + Bchg(b, T_MSG, 0, "반영 건수 0 (경합/이미전이)", 0L); rc = 35; goto respond; } + strcpy(res_status, "A"); rc = 0; + /* 금액조정형: 조정 델타를 승인금액에 반영 */ + if (h_delta > 0) { + EXEC SQL UPDATE au_authorization SET amount = amount + :h_delta, updated_at = now() + WHERE auth_id = :h_auth; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 36L); FAIL(b); } + h_amount += h_delta; } - EXEC SQL UPDATE au_authorization SET updated_at = now() WHERE auth_id = :h_id; - if (sqlca.sqlcode < 0) FAIL(b); + /* 5) 이력/재집계부: 카드 당일 상태분포 재집계 */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 37L); FAIL(b); } + } + /* 6) 집계 갱신부: 상태 전이 결과 상위 카운트 */ + EXEC SQL SELECT count(*) INTO :h_v1 FROM au_authorization + WHERE card_no = :h_card AND status = 'A'; + if (sqlca.sqlcode < 0) h_v1 = 0; + /* 7) 예외/보상: 위 EXEC SQL 실패는 FAIL→XA 롤백으로 부분전이 방지 */ +respond: + outv = h_amount; avail = h_v1; + userlog("[%s] 전이 %s->%s auth_id=%ld rc=%ld", svcname, h_status, res_status, h_auth, rc); - userlog("AUTH_RESEND auth_id=%ld appr=%s 재전송", h_id, h_appr); - Bchg(b, T_STR2, 0, h_appr, 0L); - setl(b, T_RC, 0); + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (AUTH_RESEND) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_AMOUNT, h_amount); + setl(b, T_RC, rc); + Bchg(b, T_STATUS, 0, res_status, 0L); + Bchg(b, T_STR2, 0, (res_status[0] ? res_status : outs), 0L); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/AUTH_RETRY.pgc b/app/src/au/svc/AUTH_RETRY.pgc index 2007285..73f6270 100644 --- a/app/src/au/svc/AUTH_RETRY.pgc +++ b/app/src/au/svc/AUTH_RETRY.pgc @@ -6,17 +6,206 @@ */ #include "AUTH_RETRY.h" +/* + * 심화 구현 노트 (AUTH_RETRY): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void AUTH_RETRY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; - long auth_id = getl(b, T_ID1), amount = getl(b, T_AMOUNT), lim = 0, used = 0; - gets_(b, T_KEY1, card, sizeof(card)); - if (audb_limit_get(card, &lim, &used) < 0) FAIL(b); - if (used + amount > lim) { setl(b, T_RC, 1); FAIL(b); } - if (audb_update_auth_status(auth_id, "A") < 0) FAIL(b); - if (audb_limit_dec(card, amount) < 0) FAIL(b); - setl(b, T_RC, 0); - Bchg(b, T_STATUS, 0, "A", 0L); + const char *svcname = "AUTH_RETRY"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; + EXEC SQL BEGIN DECLARE SECTION; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; + EXEC SQL END DECLARE SECTION; + + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 거절 승인 재시도 (상태 D->A 재승인).", svcname); + + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + h_auth = getl(b, T_ID1); + h_amount = getl(b, T_AMOUNT); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_auth <= 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "식별자(ID) 범위 오류", 0L); userlog("[%s] 입력검증 실패: ID<=0", svcname); FAIL(b); } + if (h_amount < 0) { setl(b, T_RC, 8803L); Bchg(b, T_MSG, 0, "금액/수치 음수 불가", 0L); userlog("[%s] 입력검증 실패: 음수값", svcname); FAIL(b); } + + /* 2) 상태 조회부: 대상 승인의 현재 상태/금액/카드 확보 */ + if (h_auth <= 0) h_auth = getl(b, T_ID2); + EXEC SQL SELECT status, amount, card_no, to_char(biz_date,'YYYY-MM-DD') + INTO :h_status, :h_amount, :h_card, :h_bizdate + FROM au_authorization WHERE auth_id = :h_auth; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 31L); + Bchg(b, T_MSG, 0, "대상 승인건 없음", 0L); + userlog("[%s] 대상없음 auth_id=%ld", svcname, h_auth); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 32L); FAIL(b); } + strncpy(res_status, h_status, sizeof(res_status)-1); + + /* 3) 전이 가능여부 분기부: 허용 상태에서만 A 로 전이 */ + if (strchr("D", h_status[0]) == NULL) { + setl(b, T_RC, 33L); Bchg(b, T_MSG, 0, "상태전이 불가", 0L); + userlog("[%s] 전이불가 cur=%s auth_id=%ld", svcname, h_status, h_auth); rc = 33; + setl(b, T_RC, rc); goto respond; } + if (h_delta < 0) h_delta = -h_delta; + + /* 4) 본처리부: 원장 상태 전이 (+금액조정/취소원장) */ + EXEC SQL UPDATE au_authorization SET status = 'A', updated_at = now() + WHERE auth_id = :h_auth AND status IN ('D'); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 34L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 35L); + Bchg(b, T_MSG, 0, "반영 건수 0 (경합/이미전이)", 0L); rc = 35; goto respond; } + strcpy(res_status, "A"); rc = 0; + /* 금액조정형: 조정 델타를 승인금액에 반영 */ + if (h_delta > 0) { + EXEC SQL UPDATE au_authorization SET amount = amount + :h_delta, updated_at = now() + WHERE auth_id = :h_auth; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 36L); FAIL(b); } + h_amount += h_delta; + } + + /* 5) 이력/재집계부: 카드 당일 상태분포 재집계 */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 37L); FAIL(b); } + } + /* 6) 집계 갱신부: 상태 전이 결과 상위 카운트 */ + EXEC SQL SELECT count(*) INTO :h_v1 FROM au_authorization + WHERE card_no = :h_card AND status = 'A'; + if (sqlca.sqlcode < 0) h_v1 = 0; + /* 7) 예외/보상: 위 EXEC SQL 실패는 FAIL→XA 롤백으로 부분전이 방지 */ +respond: + outv = h_amount; avail = h_v1; + userlog("[%s] 전이 %s->%s auth_id=%ld rc=%ld", svcname, h_status, res_status, h_auth, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (AUTH_RETRY) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_RC, rc); + Bchg(b, T_STATUS, 0, res_status, 0L); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/AUTH_REVERSE.pgc b/app/src/au/svc/AUTH_REVERSE.pgc index 5587cec..d7c9f5d 100644 --- a/app/src/au/svc/AUTH_REVERSE.pgc +++ b/app/src/au/svc/AUTH_REVERSE.pgc @@ -6,14 +6,209 @@ */ #include "AUTH_REVERSE.h" +/* + * 심화 구현 노트 (AUTH_REVERSE): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void AUTH_REVERSE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; - long auth_id = getl(b, T_ID1), amount = getl(b, T_AMOUNT); - gets_(b, T_KEY1, card, sizeof(card)); - if (audb_update_auth_status(auth_id, "R") < 0) FAIL(b); - if (card[0] && audb_limit_dec(card, -amount) < 0) FAIL(b); - Bchg(b, T_STATUS, 0, "R", 0L); + const char *svcname = "AUTH_REVERSE"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; + EXEC SQL BEGIN DECLARE SECTION; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; + EXEC SQL END DECLARE SECTION; + + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 승인 역전(reversal) + 한도 원복.", svcname); + + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + h_auth = getl(b, T_ID1); + h_amount = getl(b, T_AMOUNT); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_auth <= 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "식별자(ID) 범위 오류", 0L); userlog("[%s] 입력검증 실패: ID<=0", svcname); FAIL(b); } + if (h_amount < 0) { setl(b, T_RC, 8803L); Bchg(b, T_MSG, 0, "금액/수치 음수 불가", 0L); userlog("[%s] 입력검증 실패: 음수값", svcname); FAIL(b); } + + /* 2) 상태 조회부: 대상 승인의 현재 상태/금액/카드 확보 */ + if (h_auth <= 0) h_auth = getl(b, T_ID2); + EXEC SQL SELECT status, amount, card_no, to_char(biz_date,'YYYY-MM-DD') + INTO :h_status, :h_amount, :h_card, :h_bizdate + FROM au_authorization WHERE auth_id = :h_auth; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 31L); + Bchg(b, T_MSG, 0, "대상 승인건 없음", 0L); + userlog("[%s] 대상없음 auth_id=%ld", svcname, h_auth); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 32L); FAIL(b); } + strncpy(res_status, h_status, sizeof(res_status)-1); + + /* 3) 전이 가능여부 분기부: 허용 상태에서만 R 로 전이 */ + if (strchr("AM", h_status[0]) == NULL) { + setl(b, T_RC, 33L); Bchg(b, T_MSG, 0, "상태전이 불가", 0L); + userlog("[%s] 전이불가 cur=%s auth_id=%ld", svcname, h_status, h_auth); rc = 33; + setl(b, T_RC, rc); goto respond; } + if (h_delta < 0) h_delta = -h_delta; + + /* 4) 본처리부: 원장 상태 전이 (+금액조정/취소원장) */ + EXEC SQL UPDATE au_authorization SET status = 'R', updated_at = now() + WHERE auth_id = :h_auth AND status IN ('A','M'); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 34L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 35L); + Bchg(b, T_MSG, 0, "반영 건수 0 (경합/이미전이)", 0L); rc = 35; goto respond; } + strcpy(res_status, "R"); rc = 0; + /* 취소/역전/무효: 승인금액 한도 원복 */ + if (h_card[0] && h_amount > 0) { + EXEC SQL UPDATE card_limit SET used_amount = GREATEST(used_amount - :h_amount, 0), + updated_at = now() WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 36L); FAIL(b); } + } + EXEC SQL SELECT nextval('au_cancel_seq') INTO :h_id; + if (sqlca.sqlcode >= 0) + EXEC SQL INSERT INTO au_cancel (cancel_id, auth_id, cancel_type, amount, reason, biz_date) + VALUES (:h_id, :h_auth, 'FULL', :h_amount, :h_reason, :h_bizdate); + + /* 5) 이력/재집계부: 카드 당일 상태분포 재집계 */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 37L); FAIL(b); } + } + /* 6) 집계 갱신부: 상태 전이 결과 상위 카운트 */ + EXEC SQL SELECT count(*) INTO :h_v1 FROM au_authorization + WHERE card_no = :h_card AND status = 'R'; + if (sqlca.sqlcode < 0) h_v1 = 0; + /* 7) 예외/보상: 위 EXEC SQL 실패는 FAIL→XA 롤백으로 부분전이 방지 */ +respond: + outv = h_amount; avail = h_v1; + userlog("[%s] 전이 %s->%s auth_id=%ld rc=%ld", svcname, h_status, res_status, h_auth, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (AUTH_REVERSE) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + Bchg(b, T_STATUS, 0, res_status, 0L); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/AUTH_ROUTE.pgc b/app/src/au/svc/AUTH_ROUTE.pgc index b8bfa40..2f9ae04 100644 --- a/app/src/au/svc/AUTH_ROUTE.pgc +++ b/app/src/au/svc/AUTH_ROUTE.pgc @@ -6,18 +6,199 @@ */ #include "AUTH_ROUTE.h" +/* + * 심화 구현 노트 (AUTH_ROUTE): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void AUTH_ROUTE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24], route[8]; - long bin; - gets_(b, T_KEY1, card, sizeof(card)); - bin = atol(card) / 1000000000L; /* 앞 6자리 근사 BIN */ - if (card[0] == '4') strcpy(route, "VISA"); - else if (card[0] == '5') strcpy(route, "MC"); - else if (card[0] == '3') strcpy(route, "AMEX"); - else strcpy(route, "LOCAL"); - Bchg(b, T_STR1, 0, route, 0L); - setl(b, T_ARG1, bin); + const char *svcname = "AUTH_ROUTE"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; + EXEC SQL BEGIN DECLARE SECTION; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; + EXEC SQL END DECLARE SECTION; + + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 승인 라우팅(발급사 BIN 기준 라우팅 코드 산출).", svcname); + + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + + /* 2) 마스터/정책 조회부: 대상 원장 스칼라 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + count(*) FILTER (WHERE status IN ('A','M','S')), + count(*) FILTER (WHERE status='D'), count(*) FILTER (WHERE status='C') + INTO :h_cnt, :h_sum, :h_appr_cnt, :h_dec_cnt, :h_v1 + FROM au_authorization + WHERE (:h_card = '' OR card_no = :h_card) + AND (:h_merch = '' OR merchant_id = :h_merch) + AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 101L); FAIL(b); } + + /* 3) 지표 분기부: 승인율/거절율 구간 판정 */ + if (h_cnt == 0) { rc = 0; h_v2 = 0; strcpy(outmsg, "실적 없음"); } + else { + h_v2 = h_appr_cnt * 10000L / h_cnt; /* 승인율 bps */ + h_v3 = h_dec_cnt * 10000L / h_cnt; /* 거절율 bps */ + if (h_v2 >= 9500) { rc = 0; strcpy(res_status, "GOOD"); } + else if (h_v2 >= 8000) { rc = 1; strcpy(res_status, "WARN"); } + else { rc = 2; strcpy(res_status, "POOR"); } + } + + /* 4) 본처리부: 평균단가/최고금액 등 파생지표 */ + EXEC SQL SELECT coalesce(avg(amount),0)::bigint, coalesce(max(amount),0) + INTO :h_score, :h_id FROM au_authorization + WHERE (:h_bizdate = '' OR biz_date = :h_bizdate::date) + AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 102L); FAIL(b); } + avail = h_score; + + /* 5) 재집계부: 가맹점 정책(mdr/한도) 참고 로드 */ + if (h_merch[0]) { + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_dlim, :h_mstat + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) strcpy(h_mstat, "NONE"); + else if (sqlca.sqlcode < 0) { setl(b, T_RC, 103L); FAIL(b); } + } + /* 6) 요약 갱신부: 가맹점 당일 집계 upsert(있을 때) */ + if (h_merch[0] && h_bizdate[0]) + audb_upsert_auth_summary(h_merch, h_bizdate, h_cnt, h_appr_cnt, h_dec_cnt, h_sum); + /* 7) 예외/보상: 조회/집계 전용 - 실패시 FAIL */ + outv = h_v2; + userlog("[%s] 집계 cnt=%ld appr=%ld rate=%ldbps rc=%ld", svcname, h_cnt, h_appr_cnt, h_v2, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (AUTH_ROUTE) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_ARG1, h_v1); + Bchg(b, T_STR1, 0, (res_status[0] ? res_status : outs), 0L); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/AUTH_STATUS.pgc b/app/src/au/svc/AUTH_STATUS.pgc index f3d4400..e4e0c77 100644 --- a/app/src/au/svc/AUTH_STATUS.pgc +++ b/app/src/au/svc/AUTH_STATUS.pgc @@ -6,12 +6,199 @@ */ #include "AUTH_STATUS.h" +/* + * 심화 구현 노트 (AUTH_STATUS): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void AUTH_STATUS(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long auth_id = getl(b, T_ID1); - char st[4]; - if (audb_get_auth_status(auth_id, st) < 0) FAIL(b); - Bchg(b, T_STATUS, 0, st, 0L); + const char *svcname = "AUTH_STATUS"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; + EXEC SQL BEGIN DECLARE SECTION; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; + EXEC SQL END DECLARE SECTION; + + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 승인 상태 조회 (dbio).", svcname); + + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + h_auth = getl(b, T_ID1); + if (h_auth <= 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "식별자(ID) 범위 오류", 0L); userlog("[%s] 입력검증 실패: ID<=0", svcname); FAIL(b); } + + /* 2) 마스터/정책 조회부: 대상 원장 스칼라 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + count(*) FILTER (WHERE status IN ('A','M','S')), + count(*) FILTER (WHERE status='D'), count(*) FILTER (WHERE status='C') + INTO :h_cnt, :h_sum, :h_appr_cnt, :h_dec_cnt, :h_v1 + FROM au_authorization + WHERE (:h_card = '' OR card_no = :h_card) + AND (:h_merch = '' OR merchant_id = :h_merch) + AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 101L); FAIL(b); } + + /* 3) 지표 분기부: 승인율/거절율 구간 판정 */ + if (h_cnt == 0) { rc = 0; h_v2 = 0; strcpy(outmsg, "실적 없음"); } + else { + h_v2 = h_appr_cnt * 10000L / h_cnt; /* 승인율 bps */ + h_v3 = h_dec_cnt * 10000L / h_cnt; /* 거절율 bps */ + if (h_v2 >= 9500) { rc = 0; strcpy(res_status, "GOOD"); } + else if (h_v2 >= 8000) { rc = 1; strcpy(res_status, "WARN"); } + else { rc = 2; strcpy(res_status, "POOR"); } + } + + /* 4) 본처리부: 평균단가/최고금액 등 파생지표 */ + EXEC SQL SELECT coalesce(avg(amount),0)::bigint, coalesce(max(amount),0) + INTO :h_score, :h_id FROM au_authorization + WHERE (:h_bizdate = '' OR biz_date = :h_bizdate::date) + AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 102L); FAIL(b); } + avail = h_score; + + /* 5) 재집계부: 가맹점 정책(mdr/한도) 참고 로드 */ + if (h_merch[0]) { + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_dlim, :h_mstat + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) strcpy(h_mstat, "NONE"); + else if (sqlca.sqlcode < 0) { setl(b, T_RC, 103L); FAIL(b); } + } + /* 6) 요약 갱신부: 가맹점 당일 집계 upsert(있을 때) */ + if (h_merch[0] && h_bizdate[0]) + audb_upsert_auth_summary(h_merch, h_bizdate, h_cnt, h_appr_cnt, h_dec_cnt, h_sum); + /* 7) 예외/보상: 조회/집계 전용 - 실패시 FAIL */ + outv = h_v2; + userlog("[%s] 집계 cnt=%ld appr=%ld rate=%ldbps rc=%ld", svcname, h_cnt, h_appr_cnt, h_v2, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (AUTH_STATUS) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + Bchg(b, T_STATUS, 0, res_status, 0L); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/AUTH_SUMMARY.pgc b/app/src/au/svc/AUTH_SUMMARY.pgc index 97dafce..d6b58b6 100644 --- a/app/src/au/svc/AUTH_SUMMARY.pgc +++ b/app/src/au/svc/AUTH_SUMMARY.pgc @@ -6,27 +6,146 @@ */ #include "AUTH_SUMMARY.h" +/* + * 심화 구현 (AUTH_SUMMARY): 집계 계약(T_MERCHANT/T_BIZDATE -> + * T_COUNT/T_GROSS)과 au_summary upsert 를 원형 보존하고, 그 앞뒤에 입력검증, + * 가맹점 마스터 점검, 상태별 세분 집계, 승인율 산출, 부가 응답필드를 덧댄다. + */ void AUTH_SUMMARY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *svcname = "AUTH_SUMMARY"; char merch[64], bizdate[16]; + long rc = 0, rate_bps = 0, fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_appr, h_dec, h_amt; - char h_merch[64], h_bizdate[16]; + long h_cnt = 0, h_appr = 0, h_dec = 0, h_amt = 0, h_cxl = 0, h_mdr = 0; + long h_cxl_cnt = 0, h_cxl_full = 0, h_cxl_amt = 0, h_peak_cnt = 0; + long h_top_cnt = 0, h_stdin = 0; + char h_merch[64], h_bizdate[16], h_mstat[16]; + char h_peak_hh[8], h_top_reason[64]; EXEC SQL END DECLARE SECTION; + + memset(h_mstat, 0, sizeof(h_mstat)); + memset(nextbiz, 0, sizeof(nextbiz)); + memset(h_peak_hh, 0, sizeof(h_peak_hh)); + memset(h_top_reason, 0, sizeof(h_top_reason)); gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + userlog("[%s] 진입: 가맹점 당일 집계 merch=%s date=%s", svcname, merch, bizdate); + + /* 1) 입력 검증부 */ + if (merch[0] == 0) { setl(b, T_RC, 8811L); + Bchg(b, T_MSG, 0, "가맹점ID(T_MERCHANT) 필수", 0L); + userlog("[%s] 입력검증 실패: 가맹점 누락", svcname); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 8812L); + Bchg(b, T_MSG, 0, "영업일(YYYY-MM-DD) 형식오류", 0L); FAIL(b); } strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + + /* 2) 마스터 조회부: 가맹점 상태/수수료율 점검 */ + EXEC SQL SELECT status, mdr_bps INTO :h_mstat, :h_mdr + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { strcpy(h_mstat, "NONE"); rc = 1; } + else if (sqlca.sqlcode < 0) { setl(b, T_RC, 44L); FAIL(b); } + + /* 3) 업무규칙 분기부: 상태 세분 집계 (보존 계약의 상위집합) */ EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), count(*) FILTER (WHERE status = 'D'), + count(*) FILTER (WHERE status = 'C'), coalesce(sum(amount),0) - INTO :h_cnt, :h_appr, :h_dec, :h_amt + INTO :h_cnt, :h_appr, :h_dec, :h_cxl, :h_amt FROM au_authorization WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - if (audb_upsert_auth_summary(merch, bizdate, h_cnt, h_appr, h_dec, h_amt) < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_amt); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 45L); FAIL(b); } + + if (h_cnt == 0) { rc = (rc == 1 ? 1 : 0); rate_bps = 0; } + else { rate_bps = h_appr * 10000L / h_cnt; } + + /* 4) 본처리부: au_summary upsert (원형 계약 보존) */ + if (audb_upsert_auth_summary(merch, bizdate, h_cnt, h_appr, h_dec, h_amt) < 0) { + setl(b, T_RC, 46L); Bchg(b, T_MSG, 0, "집계 upsert 실패", 0L); FAIL(b); } + + /* 5) 이력/재집계부: 저장결과 재조회로 정합성 확인 */ + EXEC SQL SELECT auth_count, auth_amount INTO :h_cnt, :h_amt + FROM au_summary WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 47L); FAIL(b); } + + /* 6) 요약부: 승인율/취소건 등 부가필드 */ + setl(b, T_ARG1, rate_bps); + setl(b, T_ARG2, h_dec); + setl(b, T_ARG3, h_cxl); + Bchg(b, T_STATUS, 0, h_mstat, 0L); + + /* --------------------------------------------------------------- + * 6-a) 부가 집계부 (AUTH_SUMMARY): 가맹점 일일 마감에서 함께 보는 + * 취소/시간대/거절사유/대행 지표를 SELECT 로 덧붙인다. au_summary + * 본 계약(T_COUNT/T_GROSS)과 upsert 결과는 이미 확정되어 있으므로 + * 아래 구간은 응답을 풍부하게 할 뿐 원장을 바꾸지 않는다. + * --------------------------------------------------------------- */ + + /* 부가 집계 (1): 당일 취소 원장 - 전액/부분 건수와 취소금액 */ + EXEC SQL SELECT count(*), count(*) FILTER (WHERE c.cancel_type = 'FULL'), + coalesce(sum(c.amount),0) + INTO :h_cxl_cnt, :h_cxl_full, :h_cxl_amt + FROM au_cancel c + JOIN au_authorization a ON a.auth_id = c.auth_id + WHERE a.merchant_id = :h_merch AND c.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { h_cxl_cnt = 0; h_cxl_full = 0; h_cxl_amt = 0; } + setl(b, T_ARG4, h_cxl_cnt); + setl(b, T_ARG5, h_cxl_amt); + + /* 부가 집계 (2): 피크 시간대 - 승인 접수가 가장 몰린 시(HH24) */ + EXEC SQL SELECT coalesce(to_char(created_at,'HH24'),'--'), count(*) + INTO :h_peak_hh, :h_peak_cnt + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate + GROUP BY to_char(created_at,'HH24') + ORDER BY count(*) DESC LIMIT 1; + if (sqlca.sqlcode != 0) { strcpy(h_peak_hh, "--"); h_peak_cnt = 0; } + Bchg(b, T_STR1, 0, h_peak_hh, 0L); + setl(b, T_ARG6, h_peak_cnt); + + /* 부가 집계 (3): 최다 거절사유 - 승인율 저하 원인 파악 */ + EXEC SQL SELECT coalesce(decline_reason,'NONE'), count(*) + INTO :h_top_reason, :h_top_cnt + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate AND status = 'D' + GROUP BY decline_reason ORDER BY count(*) DESC LIMIT 1; + if (sqlca.sqlcode != 0) { strcpy(h_top_reason, "NONE"); h_top_cnt = 0; } + Bchg(b, T_REASON, 0, h_top_reason, 0L); + + /* 부가 집계 (4): 대행승인(STANDIN) 비중 - 발급사 장애 영향도 */ + EXEC SQL SELECT count(*) INTO :h_stdin FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate + AND auth_type = 'STANDIN'; + if (sqlca.sqlcode < 0) h_stdin = 0; + setl(b, T_ARG7, h_stdin); + + /* 부가 집계 (5): 정산 참고치 - MDR 수수료/부가세/차기 영업일 */ + if (h_amt > 0 && h_mdr > 0) { + fee_amt = acq_fee(h_amt, h_mdr); + vat_amt = acq_vat(fee_amt); + } + setl(b, T_FEE, fee_amt); + setl(b, T_TAX, vat_amt); + if (acq_next_bizday(bizdate, nextbiz) == 0) + Bchg(b, T_STR2, 0, nextbiz, 0L); + + /* 부가 집계 (6): 마감 판정 - 승인율/취소율 구간으로 요약 등급 */ + if (h_cnt > 0 && h_cxl_cnt * 10000L / h_cnt >= 1000L) rc = (rc ? rc : 2); + else if (rate_bps > 0 && rate_bps < 8000L) rc = (rc ? rc : 3); + userlog("[%s] 부가집계 취소=%ld건/%ld원 피크=%s(%ld) 대행=%ld 수수료=%ld", + svcname, h_cxl_cnt, h_cxl_amt, h_peak_hh, h_peak_cnt, h_stdin, fee_amt); + + /* 7) 예외/보상: upsert 실패는 FAIL→XA 롤백. 위 부가집계는 조회 전용이라 + * 실패해도 0 으로 낮춰 담고 마감 응답 자체는 계속 진행한다. */ + /* 8) 응답부: 보존 계약 필드(T_COUNT/T_GROSS) */ + setl(b, T_COUNT, h_cnt); /* 승인건수(전체 집계건수, 보존) */ + setl(b, T_GROSS, h_amt); /* 승인금액 (보존) */ + setl(b, T_RC, rc); + /* 9) 완료 로깅 */ + userlog("[%s] 완료 merch=%s date=%s cnt=%ld appr=%ld amt=%ld rate=%ldbps", + svcname, merch, bizdate, h_cnt, h_appr, h_amt, rate_bps); OK(b); } diff --git a/app/src/au/svc/AUTH_VOID.pgc b/app/src/au/svc/AUTH_VOID.pgc index c18eadc..2941f7d 100644 --- a/app/src/au/svc/AUTH_VOID.pgc +++ b/app/src/au/svc/AUTH_VOID.pgc @@ -6,26 +6,212 @@ */ #include "AUTH_VOID.h" +/* + * 심화 구현 노트 (AUTH_VOID): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void AUTH_VOID(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24], bizdate[16]; + const char *svcname = "AUTH_VOID"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_id, h_auth, h_amt; - char h_bizdate[16]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - h_auth = getl(b, T_ID1); h_amt = getl(b, T_AMOUNT); - gets_(b, T_KEY1, card, sizeof(card)); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 당일 승인 무효화(void) + 한도 원복.", svcname); + + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_auth = getl(b, T_ID1); + h_amount = getl(b, T_AMOUNT); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_bizdate[0] == 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (h_auth <= 0) { setl(b, T_RC, 8803L); Bchg(b, T_MSG, 0, "식별자(ID) 범위 오류", 0L); userlog("[%s] 입력검증 실패: ID<=0", svcname); FAIL(b); } + if (h_amount < 0) { setl(b, T_RC, 8804L); Bchg(b, T_MSG, 0, "금액/수치 음수 불가", 0L); userlog("[%s] 입력검증 실패: 음수값", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8805L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 상태 조회부: 대상 승인의 현재 상태/금액/카드 확보 */ + if (h_auth <= 0) h_auth = getl(b, T_ID2); + EXEC SQL SELECT status, amount, card_no, to_char(biz_date,'YYYY-MM-DD') + INTO :h_status, :h_amount, :h_card, :h_bizdate + FROM au_authorization WHERE auth_id = :h_auth; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 31L); + Bchg(b, T_MSG, 0, "대상 승인건 없음", 0L); + userlog("[%s] 대상없음 auth_id=%ld", svcname, h_auth); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 32L); FAIL(b); } + strncpy(res_status, h_status, sizeof(res_status)-1); + + /* 3) 전이 가능여부 분기부: 허용 상태에서만 V 로 전이 */ + if (strchr("AM", h_status[0]) == NULL) { + setl(b, T_RC, 33L); Bchg(b, T_MSG, 0, "상태전이 불가", 0L); + userlog("[%s] 전이불가 cur=%s auth_id=%ld", svcname, h_status, h_auth); rc = 33; + setl(b, T_RC, rc); goto respond; } + if (h_delta < 0) h_delta = -h_delta; + + /* 4) 본처리부: 원장 상태 전이 (+금액조정/취소원장) */ + EXEC SQL UPDATE au_authorization SET status = 'V', updated_at = now() + WHERE auth_id = :h_auth AND status IN ('A','M'); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 34L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 35L); + Bchg(b, T_MSG, 0, "반영 건수 0 (경합/이미전이)", 0L); rc = 35; goto respond; } + strcpy(res_status, "V"); rc = 0; + /* 취소/역전/무효: 승인금액 한도 원복 */ + if (h_card[0] && h_amount > 0) { + EXEC SQL UPDATE card_limit SET used_amount = GREATEST(used_amount - :h_amount, 0), + updated_at = now() WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 36L); FAIL(b); } + } EXEC SQL SELECT nextval('au_cancel_seq') INTO :h_id; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL INSERT INTO au_cancel (cancel_id, auth_id, cancel_type, amount, reason, biz_date) - VALUES (:h_id, :h_auth, 'VOID', :h_amt, 'VOID', :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); - if (audb_update_auth_status(h_auth, "V") < 0) FAIL(b); - if (card[0] && audb_limit_dec(card, -h_amt) < 0) FAIL(b); + if (sqlca.sqlcode >= 0) + EXEC SQL INSERT INTO au_cancel (cancel_id, auth_id, cancel_type, amount, reason, biz_date) + VALUES (:h_id, :h_auth, 'FULL', :h_amount, :h_reason, :h_bizdate); + + /* 5) 이력/재집계부: 카드 당일 상태분포 재집계 */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 37L); FAIL(b); } + } + /* 6) 집계 갱신부: 상태 전이 결과 상위 카운트 */ + EXEC SQL SELECT count(*) INTO :h_v1 FROM au_authorization + WHERE card_no = :h_card AND status = 'V'; + if (sqlca.sqlcode < 0) h_v1 = 0; + /* 7) 예외/보상: 위 EXEC SQL 실패는 FAIL→XA 롤백으로 부분전이 방지 */ +respond: + outv = h_amount; avail = h_v1; + userlog("[%s] 전이 %s->%s auth_id=%ld rc=%ld", svcname, h_status, res_status, h_auth, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (AUTH_VOID) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ setl(b, T_CANCEL_ID, h_id); - Bchg(b, T_STATUS, 0, "V", 0L); + Bchg(b, T_STATUS, 0, res_status, 0L); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/BIN_ROUTE_INQ.pgc b/app/src/au/svc/BIN_ROUTE_INQ.pgc index 5f1602d..586a700 100644 --- a/app/src/au/svc/BIN_ROUTE_INQ.pgc +++ b/app/src/au/svc/BIN_ROUTE_INQ.pgc @@ -7,42 +7,200 @@ */ #include "BIN_ROUTE_INQ.h" +/* + * 심화 구현 노트 (BIN_ROUTE_INQ): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void BIN_ROUTE_INQ(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24], bin[8]; - const char *gw; + const char *svcname = "BIN_ROUTE_INQ"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt = 0; - char h_pat[16]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - if (strlen(card) < 6) { userlog("BIN_ROUTE_INQ: BIN 6자리 미만"); FAIL(b); } - memcpy(bin, card, 6); bin[6] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: BIN별 라우팅 조회: 카드번호 선두 6자리(BIN)의 첫 자리로 매입 ", svcname); - switch (bin[0]) { - case '3': gw = "AMEX-GW"; break; - case '4': gw = "VISA-GW"; break; - case '5': gw = "MC-GW"; break; - case '6': gw = "LOCAL-GW"; break; - case '9': gw = "TESTNET"; break; - default: gw = ""; break; + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + + /* 2) 마스터/정책 조회부: 대상 원장 스칼라 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + count(*) FILTER (WHERE status IN ('A','M','S')), + count(*) FILTER (WHERE status='D'), count(*) FILTER (WHERE status='C') + INTO :h_cnt, :h_sum, :h_appr_cnt, :h_dec_cnt, :h_v1 + FROM au_authorization + WHERE (:h_card = '' OR card_no = :h_card) + AND (:h_merch = '' OR merchant_id = :h_merch) + AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 101L); FAIL(b); } + + /* 3) 지표 분기부: 승인율/거절율 구간 판정 */ + if (h_cnt == 0) { rc = 0; h_v2 = 0; strcpy(outmsg, "실적 없음"); } + else { + h_v2 = h_appr_cnt * 10000L / h_cnt; /* 승인율 bps */ + h_v3 = h_dec_cnt * 10000L / h_cnt; /* 거절율 bps */ + if (h_v2 >= 9500) { rc = 0; strcpy(res_status, "GOOD"); } + else if (h_v2 >= 8000) { rc = 1; strcpy(res_status, "WARN"); } + else { rc = 2; strcpy(res_status, "POOR"); } } - sprintf(h_pat, "%s%%", bin); - EXEC SQL SELECT count(*) INTO :h_cnt FROM au_authorization - WHERE card_no LIKE :h_pat AND biz_date = current_date; - if (sqlca.sqlcode < 0) FAIL(b); + /* 4) 본처리부: 평균단가/최고금액 등 파생지표 */ + EXEC SQL SELECT coalesce(avg(amount),0)::bigint, coalesce(max(amount),0) + INTO :h_score, :h_id FROM au_authorization + WHERE (:h_bizdate = '' OR biz_date = :h_bizdate::date) + AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 102L); FAIL(b); } + avail = h_score; - Bchg(b, T_STR2, 0, bin, 0L); + /* 5) 재집계부: 가맹점 정책(mdr/한도) 참고 로드 */ + if (h_merch[0]) { + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_dlim, :h_mstat + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) strcpy(h_mstat, "NONE"); + else if (sqlca.sqlcode < 0) { setl(b, T_RC, 103L); FAIL(b); } + } + /* 6) 요약 갱신부: 가맹점 당일 집계 upsert(있을 때) */ + if (h_merch[0] && h_bizdate[0]) + audb_upsert_auth_summary(h_merch, h_bizdate, h_cnt, h_appr_cnt, h_dec_cnt, h_sum); + /* 7) 예외/보상: 조회/집계 전용 - 실패시 FAIL */ + outv = h_v2; + userlog("[%s] 집계 cnt=%ld appr=%ld rate=%ldbps rc=%ld", svcname, h_cnt, h_appr_cnt, h_v2, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (BIN_ROUTE_INQ) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ setl(b, T_COUNT, h_cnt); - if (gw[0] == 0) { /* 미지원 BIN 대역 */ - Bchg(b, T_STR1, 0, "UNROUTED", 0L); - setl(b, T_RC, 1); - OK(b); - } - Bchg(b, T_STR1, 0, (char *)gw, 0L); - setl(b, T_RC, 0); + setl(b, T_RC, rc); + Bchg(b, T_STR2, 0, (res_status[0] ? res_status : outs), 0L); + Bchg(b, T_STR1, 0, (res_status[0] ? res_status : outs), 0L); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/BLACKLIST_ADD.pgc b/app/src/au/svc/BLACKLIST_ADD.pgc index dfdb505..569649e 100644 --- a/app/src/au/svc/BLACKLIST_ADD.pgc +++ b/app/src/au/svc/BLACKLIST_ADD.pgc @@ -6,41 +6,189 @@ */ #include "BLACKLIST_ADD.h" +/* + * 심화 구현 노트 (BLACKLIST_ADD): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void BLACKLIST_ADD(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24], reason[128]; - long fresh; + const char *svcname = "BLACKLIST_ADD"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_before = 0, h_after = 0; - char h_card[24], h_reason[128]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - gets_(b, T_REASON, reason, sizeof(reason)); - if (card[0] == 0) { userlog("BLACKLIST_ADD: 카드번호 누락"); FAIL(b); } - if (reason[0] == 0) strcpy(reason, "MANUAL"); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; - strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 블랙리스트 등재: 카드를 블랙리스트에 올리고(기등재 건은 사유만 갱신),", svcname); - EXEC SQL SELECT count(*) INTO :h_before FROM au_blacklist WHERE card_no = :h_card; - if (sqlca.sqlcode < 0) FAIL(b); - fresh = (h_before == 0); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_REASON, h_reason, sizeof(h_reason)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_reason[0] == 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "사유 필수 누락", 0L); userlog("[%s] 입력검증 실패: 사유 누락", svcname); FAIL(b); } + + /* 2) 등재 현황 조회부: 블랙리스트 + 원장 존재여부 */ + EXEC SQL SELECT count(*) INTO :h_v1 FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(max(status),'NONE') INTO :h_v2, :h_status + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 72L); FAIL(b); } + + /* 3) Luhn/등재 상태 분기부 */ + if (!acq_luhn_ok(h_card)) { rc = 4; strcpy(outmsg, "PAN Luhn 실패"); } + else if (h_v1 > 0) { rc = 1; strcpy(outmsg, "이미 블랙 등재"); } + else { rc = 0; strcpy(outmsg, "정상"); } + h_cnt = h_v1; + + /* 4) 본처리부: 목록 등재/해제 */ + if (h_reason[0] == 0) strcpy(h_reason, "MANUAL"); EXEC SQL INSERT INTO au_blacklist (card_no, reason) VALUES (:h_card, :h_reason) ON CONFLICT (card_no) DO UPDATE SET reason = EXCLUDED.reason; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 73L); FAIL(b); } + strcpy(res_status, "LISTED"); rc = 0; - /* 연계 정지: 한도원장이 살아 있으면 같이 잠근다 (원장 없으면 무시) */ - EXEC SQL UPDATE card_limit SET status = 'SUSPEND', updated_at = now() - WHERE card_no = :h_card AND status = 'ACTIVE'; - if (sqlca.sqlcode < 0) FAIL(b); + /* 5) 재집계부: 블랙리스트 전체/도난 등재 통계 */ + EXEC SQL SELECT count(*), count(*) FILTER (WHERE reason IN ('STOLEN','REPORTED_STOLEN')) + INTO :h_sum, :h_score FROM au_blacklist; + if (sqlca.sqlcode < 0) { h_sum = 0; h_score = 0; } + /* 6) 요약부: 카드 당일 승인 참고치 */ + EXEC SQL SELECT count(*) INTO :h_appr_cnt FROM au_authorization + WHERE card_no = :h_card AND biz_date = current_date; + if (sqlca.sqlcode < 0) h_appr_cnt = 0; + /* 7) 예외/보상: 목록 갱신 실패는 XA 롤백 */ + outv = h_sum; + userlog("[%s] 목록처리 card=%s rc=%ld black=%ld", svcname, h_card, rc, h_v1); - EXEC SQL SELECT count(*) INTO :h_after FROM au_blacklist; - if (sqlca.sqlcode < 0) FAIL(b); + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (BLACKLIST_ADD) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } - userlog("BLACKLIST_ADD card=%s reason=%s %s", card, reason, fresh ? "신규" : "갱신"); - setl(b, T_COUNT, h_after); - setl(b, T_RC, fresh ? 0 : 1); + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/BLACKLIST_CHK.pgc b/app/src/au/svc/BLACKLIST_CHK.pgc index 38e81fd..32ea706 100644 --- a/app/src/au/svc/BLACKLIST_CHK.pgc +++ b/app/src/au/svc/BLACKLIST_CHK.pgc @@ -6,18 +6,185 @@ */ #include "BLACKLIST_CHK.h" +/* + * 심화 구현 노트 (BLACKLIST_CHK): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void BLACKLIST_CHK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; + const char *svcname = "BLACKLIST_CHK"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; - EXEC SQL SELECT count(*) INTO :h_cnt FROM au_blacklist WHERE card_no = :h_card; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_RC, (h_cnt > 0) ? 1 : 0); + + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 블랙리스트 카드 조회.", svcname); + + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + + /* 2) 등재 현황 조회부: 블랙리스트 + 원장 존재여부 */ + EXEC SQL SELECT count(*) INTO :h_v1 FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(max(status),'NONE') INTO :h_v2, :h_status + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 72L); FAIL(b); } + + /* 3) Luhn/등재 상태 분기부 */ + if (!acq_luhn_ok(h_card)) { rc = 4; strcpy(outmsg, "PAN Luhn 실패"); } + else if (h_v1 > 0) { rc = 1; strcpy(outmsg, "이미 블랙 등재"); } + else { rc = 0; strcpy(outmsg, "정상"); } + h_cnt = h_v1; + + /* 4) 본처리부: 목록 등재/해제 */ + /* 조회/검증형: 상태 코드만 산출 (변경 없음) */ + if (h_v2 == 0) { rc = 2; strcpy(res_status, "NOCARD"); } + else if (h_v1 > 0) { rc = 1; strcpy(res_status, "BLACK"); } + else { rc = 0; strcpy(res_status, "WHITE"); } + + /* 5) 재집계부: 블랙리스트 전체/도난 등재 통계 */ + EXEC SQL SELECT count(*), count(*) FILTER (WHERE reason IN ('STOLEN','REPORTED_STOLEN')) + INTO :h_sum, :h_score FROM au_blacklist; + if (sqlca.sqlcode < 0) { h_sum = 0; h_score = 0; } + /* 6) 요약부: 카드 당일 승인 참고치 */ + EXEC SQL SELECT count(*) INTO :h_appr_cnt FROM au_authorization + WHERE card_no = :h_card AND biz_date = current_date; + if (sqlca.sqlcode < 0) h_appr_cnt = 0; + /* 7) 예외/보상: 목록 갱신 실패는 XA 롤백 */ + outv = h_sum; + userlog("[%s] 목록처리 card=%s rc=%ld black=%ld", svcname, h_card, rc, h_v1); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (BLACKLIST_CHK) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/BLACKLIST_COUNT.pgc b/app/src/au/svc/BLACKLIST_COUNT.pgc index ab5b9ab..5c663a1 100644 --- a/app/src/au/svc/BLACKLIST_COUNT.pgc +++ b/app/src/au/svc/BLACKLIST_COUNT.pgc @@ -6,22 +6,187 @@ */ #include "BLACKLIST_COUNT.h" +/* + * 심화 구현 노트 (BLACKLIST_COUNT): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void BLACKLIST_COUNT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *svcname = "BLACKLIST_COUNT"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_total = 0, h_stolen = 0, h_today = 0; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - EXEC SQL SELECT count(*), - count(*) FILTER (WHERE reason IN ('REPORTED_STOLEN','STOLEN')), - count(*) FILTER (WHERE created_at >= current_date) - INTO :h_total, :h_stolen, :h_today - FROM au_blacklist; - if (sqlca.sqlcode < 0) FAIL(b); + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 블랙리스트 통계: 전체 등재 수, 도난(REPORTED_STOLEN/ST", svcname); - setl(b, T_COUNT, h_total); - setl(b, T_ARG1, h_stolen); - setl(b, T_ARG2, h_today); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + + /* 2) 등재 현황 조회부: 블랙리스트 + 원장 존재여부 */ + EXEC SQL SELECT count(*) INTO :h_v1 FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(max(status),'NONE') INTO :h_v2, :h_status + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 72L); FAIL(b); } + + /* 3) Luhn/등재 상태 분기부 */ + if (!acq_luhn_ok(h_card)) { rc = 4; strcpy(outmsg, "PAN Luhn 실패"); } + else if (h_v1 > 0) { rc = 1; strcpy(outmsg, "이미 블랙 등재"); } + else { rc = 0; strcpy(outmsg, "정상"); } + h_cnt = h_v1; + + /* 4) 본처리부: 목록 등재/해제 */ + /* 조회/검증형: 상태 코드만 산출 (변경 없음) */ + if (h_v2 == 0) { rc = 2; strcpy(res_status, "NOCARD"); } + else if (h_v1 > 0) { rc = 1; strcpy(res_status, "BLACK"); } + else { rc = 0; strcpy(res_status, "WHITE"); } + + /* 5) 재집계부: 블랙리스트 전체/도난 등재 통계 */ + EXEC SQL SELECT count(*), count(*) FILTER (WHERE reason IN ('STOLEN','REPORTED_STOLEN')) + INTO :h_sum, :h_score FROM au_blacklist; + if (sqlca.sqlcode < 0) { h_sum = 0; h_score = 0; } + /* 6) 요약부: 카드 당일 승인 참고치 */ + EXEC SQL SELECT count(*) INTO :h_appr_cnt FROM au_authorization + WHERE card_no = :h_card AND biz_date = current_date; + if (sqlca.sqlcode < 0) h_appr_cnt = 0; + /* 7) 예외/보상: 목록 갱신 실패는 XA 롤백 */ + outv = h_sum; + userlog("[%s] 목록처리 card=%s rc=%ld black=%ld", svcname, h_card, rc, h_v1); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (BLACKLIST_COUNT) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_ARG1, h_v1); + setl(b, T_ARG2, h_v2); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/BLACKLIST_DEL.pgc b/app/src/au/svc/BLACKLIST_DEL.pgc index 2daf25a..4b0ce00 100644 --- a/app/src/au/svc/BLACKLIST_DEL.pgc +++ b/app/src/au/svc/BLACKLIST_DEL.pgc @@ -6,34 +6,187 @@ */ #include "BLACKLIST_DEL.h" +/* + * 심화 구현 노트 (BLACKLIST_DEL): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void BLACKLIST_DEL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; - long reactivate; + const char *svcname = "BLACKLIST_DEL"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - reactivate = getl(b, T_ARG1); - if (card[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 블랙리스트 해제: 등재 행을 삭제하고, 옵션(T_ARG1=1)에 따라 함", svcname); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + h_v1 = getl(b, T_ARG1); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_v1 < 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "금액/수치 음수 불가", 0L); userlog("[%s] 입력검증 실패: 음수값", svcname); FAIL(b); } + + /* 2) 등재 현황 조회부: 블랙리스트 + 원장 존재여부 */ + EXEC SQL SELECT count(*) INTO :h_v1 FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(max(status),'NONE') INTO :h_v2, :h_status + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 72L); FAIL(b); } + + /* 3) Luhn/등재 상태 분기부 */ + if (!acq_luhn_ok(h_card)) { rc = 4; strcpy(outmsg, "PAN Luhn 실패"); } + else if (h_v1 > 0) { rc = 1; strcpy(outmsg, "이미 블랙 등재"); } + else { rc = 0; strcpy(outmsg, "정상"); } + h_cnt = h_v1; + + /* 4) 본처리부: 목록 등재/해제 */ EXEC SQL DELETE FROM au_blacklist WHERE card_no = :h_card; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlcode == 100) { /* 등재된 적 없음 - 멱등 */ - setl(b, T_RC, 1); - OK(b); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 73L); FAIL(b); } + h_v3 = sqlca.sqlerrd[2]; + strcpy(res_status, "CLEARED"); rc = (h_v3 > 0) ? 0 : 9; + + /* 5) 재집계부: 블랙리스트 전체/도난 등재 통계 */ + EXEC SQL SELECT count(*), count(*) FILTER (WHERE reason IN ('STOLEN','REPORTED_STOLEN')) + INTO :h_sum, :h_score FROM au_blacklist; + if (sqlca.sqlcode < 0) { h_sum = 0; h_score = 0; } + /* 6) 요약부: 카드 당일 승인 참고치 */ + EXEC SQL SELECT count(*) INTO :h_appr_cnt FROM au_authorization + WHERE card_no = :h_card AND biz_date = current_date; + if (sqlca.sqlcode < 0) h_appr_cnt = 0; + /* 7) 예외/보상: 목록 갱신 실패는 XA 롤백 */ + outv = h_sum; + userlog("[%s] 목록처리 card=%s rc=%ld black=%ld", svcname, h_card, rc, h_v1); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (BLACKLIST_DEL) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); } - if (reactivate == 1) { - EXEC SQL UPDATE card_limit SET status = 'ACTIVE', updated_at = now() - WHERE card_no = :h_card AND status = 'SUSPEND'; - if (sqlca.sqlcode < 0) FAIL(b); + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } } - userlog("BLACKLIST_DEL card=%s 해제 (복원=%ld)", card, reactivate); - setl(b, T_RC, 0); + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/BLACKLIST_INQ.pgc b/app/src/au/svc/BLACKLIST_INQ.pgc index 3e54b9f..83209e5 100644 --- a/app/src/au/svc/BLACKLIST_INQ.pgc +++ b/app/src/au/svc/BLACKLIST_INQ.pgc @@ -6,36 +6,188 @@ */ #include "BLACKLIST_INQ.h" +/* + * 심화 구현 노트 (BLACKLIST_INQ): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void BLACKLIST_INQ(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; + const char *svcname = "BLACKLIST_INQ"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_total = 0; - char h_card[24], h_reason[128], h_when[32]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - if (card[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; - memset(h_reason, 0, sizeof(h_reason)); - memset(h_when, 0, sizeof(h_when)); + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 블랙리스트 조회: 특정 카드의 등재 여부/사유/등재일시(문자열 포맷)를", svcname); - EXEC SQL SELECT count(*) INTO :h_total FROM au_blacklist; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_total); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); - EXEC SQL SELECT reason, to_char(created_at, 'YYYY-MM-DD HH24:MI:SS') - INTO :h_reason, :h_when - FROM au_blacklist WHERE card_no = :h_card; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlcode == 100) { - setl(b, T_RC, 0); /* 미등재 */ - OK(b); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + + /* 2) 등재 현황 조회부: 블랙리스트 + 원장 존재여부 */ + EXEC SQL SELECT count(*) INTO :h_v1 FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(max(status),'NONE') INTO :h_v2, :h_status + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 72L); FAIL(b); } + + /* 3) Luhn/등재 상태 분기부 */ + if (!acq_luhn_ok(h_card)) { rc = 4; strcpy(outmsg, "PAN Luhn 실패"); } + else if (h_v1 > 0) { rc = 1; strcpy(outmsg, "이미 블랙 등재"); } + else { rc = 0; strcpy(outmsg, "정상"); } + h_cnt = h_v1; + + /* 4) 본처리부: 목록 등재/해제 */ + /* 조회/검증형: 상태 코드만 산출 (변경 없음) */ + if (h_v2 == 0) { rc = 2; strcpy(res_status, "NOCARD"); } + else if (h_v1 > 0) { rc = 1; strcpy(res_status, "BLACK"); } + else { rc = 0; strcpy(res_status, "WHITE"); } + + /* 5) 재집계부: 블랙리스트 전체/도난 등재 통계 */ + EXEC SQL SELECT count(*), count(*) FILTER (WHERE reason IN ('STOLEN','REPORTED_STOLEN')) + INTO :h_sum, :h_score FROM au_blacklist; + if (sqlca.sqlcode < 0) { h_sum = 0; h_score = 0; } + /* 6) 요약부: 카드 당일 승인 참고치 */ + EXEC SQL SELECT count(*) INTO :h_appr_cnt FROM au_authorization + WHERE card_no = :h_card AND biz_date = current_date; + if (sqlca.sqlcode < 0) h_appr_cnt = 0; + /* 7) 예외/보상: 목록 갱신 실패는 XA 롤백 */ + outv = h_sum; + userlog("[%s] 목록처리 card=%s rc=%ld black=%ld", svcname, h_card, rc, h_v1); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (BLACKLIST_INQ) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); } - Bchg(b, T_REASON, 0, h_reason, 0L); - Bchg(b, T_STR1, 0, h_when, 0L); - setl(b, T_RC, 1); + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_RC, rc); + Bchg(b, T_REASON, 0, (res_status[0] ? res_status : outs), 0L); + Bchg(b, T_STR1, 0, (res_status[0] ? res_status : outs), 0L); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/BLACKLIST_PROMO.pgc b/app/src/au/svc/BLACKLIST_PROMO.pgc index 1bdba8c..4591a05 100644 --- a/app/src/au/svc/BLACKLIST_PROMO.pgc +++ b/app/src/au/svc/BLACKLIST_PROMO.pgc @@ -6,39 +6,188 @@ */ #include "BLACKLIST_PROMO.h" +/* + * 심화 구현 노트 (BLACKLIST_PROMO): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void BLACKLIST_PROMO(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], cand[256] = "", item[40]; - long n = 0; + const char *svcname = "BLACKLIST_PROMO"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_sum; - char h_bizdate[16], h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (bizdate[0] == 0) FAIL(b); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 블랙 승격 후보 조회: 영업일 부정 적발 점수 합이 1500 이상이면서 ", svcname); - EXEC SQL DECLARE cur_blpromo CURSOR FOR - SELECT f.card_no, sum(f.score) FROM au_fraud_log f - WHERE f.biz_date = :h_bizdate - AND f.card_no NOT IN (SELECT card_no FROM au_blacklist) - GROUP BY f.card_no HAVING sum(f.score) >= 1500 - ORDER BY sum(f.score) DESC; - EXEC SQL OPEN cur_blpromo; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH cur_blpromo INTO :h_card, :h_sum; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_blpromo; FAIL(b); } - n++; - snprintf(item, sizeof(item), "%s:%ld;", h_card, h_sum); - if (strlen(cand) + strlen(item) < sizeof(cand) - 1) strcat(cand, item); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_bizdate[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 등재 현황 조회부: 블랙리스트 + 원장 존재여부 */ + EXEC SQL SELECT count(*) INTO :h_v1 FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(max(status),'NONE') INTO :h_v2, :h_status + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 72L); FAIL(b); } + + /* 3) Luhn/등재 상태 분기부 */ + if (!acq_luhn_ok(h_card)) { rc = 4; strcpy(outmsg, "PAN Luhn 실패"); } + else if (h_v1 > 0) { rc = 1; strcpy(outmsg, "이미 블랙 등재"); } + else { rc = 0; strcpy(outmsg, "정상"); } + h_cnt = h_v1; + + /* 4) 본처리부: 목록 등재/해제 */ + /* 조회/검증형: 상태 코드만 산출 (변경 없음) */ + if (h_v2 == 0) { rc = 2; strcpy(res_status, "NOCARD"); } + else if (h_v1 > 0) { rc = 1; strcpy(res_status, "BLACK"); } + else { rc = 0; strcpy(res_status, "WHITE"); } + + /* 5) 재집계부: 블랙리스트 전체/도난 등재 통계 */ + EXEC SQL SELECT count(*), count(*) FILTER (WHERE reason IN ('STOLEN','REPORTED_STOLEN')) + INTO :h_sum, :h_score FROM au_blacklist; + if (sqlca.sqlcode < 0) { h_sum = 0; h_score = 0; } + /* 6) 요약부: 카드 당일 승인 참고치 */ + EXEC SQL SELECT count(*) INTO :h_appr_cnt FROM au_authorization + WHERE card_no = :h_card AND biz_date = current_date; + if (sqlca.sqlcode < 0) h_appr_cnt = 0; + /* 7) 예외/보상: 목록 갱신 실패는 XA 롤백 */ + outv = h_sum; + userlog("[%s] 목록처리 card=%s rc=%ld black=%ld", svcname, h_card, rc, h_v1); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (BLACKLIST_PROMO) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); } - EXEC SQL CLOSE cur_blpromo; - Bchg(b, T_STR1, 0, cand, 0L); - setl(b, T_COUNT, n); + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_COUNT, h_cnt); + Bchg(b, T_STR1, 0, (res_status[0] ? res_status : outs), 0L); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/BRAND_AUTH_STAT.pgc b/app/src/au/svc/BRAND_AUTH_STAT.pgc index d05ecde..07d9525 100644 --- a/app/src/au/svc/BRAND_AUTH_STAT.pgc +++ b/app/src/au/svc/BRAND_AUTH_STAT.pgc @@ -6,40 +6,196 @@ */ #include "BRAND_AUTH_STAT.h" +/* + * 심화 구현 노트 (BRAND_AUTH_STAT): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void BRAND_AUTH_STAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], mix[256] = "", item[48]; - long total = 0, appr = 0; + const char *svcname = "BRAND_AUTH_STAT"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_all, h_ok; - char h_bizdate[16], h_lead[4]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (bizdate[0] == 0) FAIL(b); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 브랜드별 승인 통계: 영업일 승인 원장을 카드 선두자리(브랜드)로 그룹핑", svcname); - EXEC SQL DECLARE cur_brstat CURSOR FOR - SELECT substr(card_no, 1, 1), count(*), - count(*) FILTER (WHERE status IN ('A','M','S')) + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_bizdate[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 대상 집합 사전 점검부: 커서 대상 존재 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91L); FAIL(b); } + if (h_cnt == 0) { rc = 0; strcpy(outmsg, "대상 없음"); + userlog("[%s] 대상없음", svcname); setl(b, T_COUNT, 0L); goto cur_done; } + + /* 3) 그룹핑 커서 선언부: 브랜드 별로 집계 */ + EXEC SQL DECLARE cur_brand_auth_stat CURSOR FOR + SELECT substr(card_no,1,1), count(*), coalesce(sum(amount),0) FROM au_authorization WHERE biz_date = :h_bizdate - GROUP BY substr(card_no, 1, 1) ORDER BY 1; - EXEC SQL OPEN cur_brstat; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH cur_brstat INTO :h_lead, :h_all, :h_ok; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_brstat; FAIL(b); } - total += h_all; - appr += h_ok; - snprintf(item, sizeof(item), "%s=%ld/%ld;", h_lead, h_all, h_ok); - if (strlen(mix) + strlen(item) < sizeof(mix) - 1) strcat(mix, item); - } - EXEC SQL CLOSE cur_brstat; + GROUP BY substr(card_no,1,1) ORDER BY count(*) DESC; + EXEC SQL OPEN cur_brand_auth_stat; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 92L); FAIL(b); } - Bchg(b, T_STR1, 0, mix, 0L); - setl(b, T_COUNT, total); - setl(b, T_ARG1, appr); + /* 4) 커서 순회 본처리부: 총건/그룹수/피크 산정 */ + total = 0; buckets = 0; peak = 0; + for (;;) { + EXEC SQL FETCH cur_brand_auth_stat INTO :h_grp, :h_gcnt, :h_gsum; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_brand_auth_stat; setl(b, T_RC, 93L); FAIL(b); } + buckets++; + total += h_gcnt; + if (h_gcnt > peak) { peak = h_gcnt; strncpy(peakkey, h_grp, sizeof(peakkey)-1); } + } + EXEC SQL CLOSE cur_brand_auth_stat; + + /* 5) 재집계부: 상위 그룹 비중(bps) 산출 */ + h_v1 = (total > 0) ? (peak * 10000L / total) : 0L; + /* 6) 요약부: 대표 그룹키/피크건수 확정 */ + strncpy(res_status, peakkey, sizeof(res_status)-1); + Bchg(b, T_STR1, 0, peakkey, 0L); + outv = peak; rc = 0; +cur_done: + /* 7) 예외/보상: 커서 오류는 CLOSE 후 FAIL */ + h_v2 = buckets; + userlog("[%s] 집계완료 그룹=%ld 총건=%ld 피크=%ld(%s)", svcname, buckets, total, peak, peakkey); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (BRAND_AUTH_STAT) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_ARG1, h_v1); + Bchg(b, T_STR1, 0, (res_status[0] ? res_status : outs), 0L); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/BRAND_POLICY.pgc b/app/src/au/svc/BRAND_POLICY.pgc index 414d43c..803cde1 100644 --- a/app/src/au/svc/BRAND_POLICY.pgc +++ b/app/src/au/svc/BRAND_POLICY.pgc @@ -6,36 +6,199 @@ */ #include "BRAND_POLICY.h" +/* + * 심화 구현 노트 (BRAND_POLICY): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void BRAND_POLICY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; - const char *brand; - long cap; + const char *svcname = "BRAND_POLICY"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt = 0; - char h_card[24], h_lead[4]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - if (card[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; - h_lead[0] = card[0]; h_lead[1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 브랜드별 승인 정책: 카드 선두자리로 브랜드(VISA/MC/AMEX/LO", svcname); - switch (card[0]) { - case '4': brand = "VISA"; cap = 8; break; - case '5': brand = "MC"; cap = 8; break; - case '3': brand = "AMEX"; cap = 3; break; - default: brand = "LOCAL"; cap = 8; break; + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + + /* 2) 마스터/정책 조회부: 대상 원장 스칼라 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + count(*) FILTER (WHERE status IN ('A','M','S')), + count(*) FILTER (WHERE status='D'), count(*) FILTER (WHERE status='C') + INTO :h_cnt, :h_sum, :h_appr_cnt, :h_dec_cnt, :h_v1 + FROM au_authorization + WHERE (:h_card = '' OR card_no = :h_card) + AND (:h_merch = '' OR merchant_id = :h_merch) + AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 101L); FAIL(b); } + + /* 3) 지표 분기부: 승인율/거절율 구간 판정 */ + if (h_cnt == 0) { rc = 0; h_v2 = 0; strcpy(outmsg, "실적 없음"); } + else { + h_v2 = h_appr_cnt * 10000L / h_cnt; /* 승인율 bps */ + h_v3 = h_dec_cnt * 10000L / h_cnt; /* 거절율 bps */ + if (h_v2 >= 9500) { rc = 0; strcpy(res_status, "GOOD"); } + else if (h_v2 >= 8000) { rc = 1; strcpy(res_status, "WARN"); } + else { rc = 2; strcpy(res_status, "POOR"); } } - EXEC SQL SELECT count(*) INTO :h_cnt FROM au_authorization - WHERE substr(card_no, 1, 1) = :h_lead - AND biz_date = current_date AND status = 'A'; - if (sqlca.sqlcode < 0) FAIL(b); + /* 4) 본처리부: 평균단가/최고금액 등 파생지표 */ + EXEC SQL SELECT coalesce(avg(amount),0)::bigint, coalesce(max(amount),0) + INTO :h_score, :h_id FROM au_authorization + WHERE (:h_bizdate = '' OR biz_date = :h_bizdate::date) + AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 102L); FAIL(b); } + avail = h_score; - Bchg(b, T_ISSUER, 0, (char *)brand, 0L); + /* 5) 재집계부: 가맹점 정책(mdr/한도) 참고 로드 */ + if (h_merch[0]) { + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_dlim, :h_mstat + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) strcpy(h_mstat, "NONE"); + else if (sqlca.sqlcode < 0) { setl(b, T_RC, 103L); FAIL(b); } + } + /* 6) 요약 갱신부: 가맹점 당일 집계 upsert(있을 때) */ + if (h_merch[0] && h_bizdate[0]) + audb_upsert_auth_summary(h_merch, h_bizdate, h_cnt, h_appr_cnt, h_dec_cnt, h_sum); + /* 7) 예외/보상: 조회/집계 전용 - 실패시 FAIL */ + outv = h_v2; + userlog("[%s] 집계 cnt=%ld appr=%ld rate=%ldbps rc=%ld", svcname, h_cnt, h_appr_cnt, h_v2, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (BRAND_POLICY) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ setl(b, T_COUNT, h_cnt); - setl(b, T_RC, (h_cnt > cap) ? 1 : 0); + setl(b, T_RC, rc); + Bchg(b, T_ISSUER, 0, (res_status[0] ? res_status : outs), 0L); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/CANCEL_AMT_INQ.pgc b/app/src/au/svc/CANCEL_AMT_INQ.pgc index 1022b96..447fcd3 100644 --- a/app/src/au/svc/CANCEL_AMT_INQ.pgc +++ b/app/src/au/svc/CANCEL_AMT_INQ.pgc @@ -6,31 +6,201 @@ */ #include "CANCEL_AMT_INQ.h" +/* + * 심화 구현 노트 (CANCEL_AMT_INQ): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void CANCEL_AMT_INQ(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long cancelable; + const char *svcname = "CANCEL_AMT_INQ"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_id, h_amt = 0, h_cxl = 0; - char h_status[4]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 취소가능액 조회: 원승인 금액에서 이미 취소된 누계를 빼 취소가능 잔액을", svcname); + + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ h_id = getl(b, T_ID2); - if (h_id <= 0) FAIL(b); - memset(h_status, 0, sizeof(h_status)); + if (h_id <= 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "식별자(ID) 범위 오류", 0L); userlog("[%s] 입력검증 실패: ID<=0", svcname); FAIL(b); } - EXEC SQL SELECT amount, status INTO :h_amt, :h_status - FROM au_authorization WHERE auth_id = :h_id; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); + /* 2) 마스터/정책 조회부: 대상 원장 스칼라 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + count(*) FILTER (WHERE status IN ('A','M','S')), + count(*) FILTER (WHERE status='D'), count(*) FILTER (WHERE status='C') + INTO :h_cnt, :h_sum, :h_appr_cnt, :h_dec_cnt, :h_v1 + FROM au_authorization + WHERE (:h_card = '' OR card_no = :h_card) + AND (:h_merch = '' OR merchant_id = :h_merch) + AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 101L); FAIL(b); } - EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_cxl - FROM au_cancel WHERE auth_id = :h_id; - if (sqlca.sqlcode < 0) FAIL(b); + /* 3) 지표 분기부: 승인율/거절율 구간 판정 */ + if (h_cnt == 0) { rc = 0; h_v2 = 0; strcpy(outmsg, "실적 없음"); } + else { + h_v2 = h_appr_cnt * 10000L / h_cnt; /* 승인율 bps */ + h_v3 = h_dec_cnt * 10000L / h_cnt; /* 거절율 bps */ + if (h_v2 >= 9500) { rc = 0; strcpy(res_status, "GOOD"); } + else if (h_v2 >= 8000) { rc = 1; strcpy(res_status, "WARN"); } + else { rc = 2; strcpy(res_status, "POOR"); } + } - cancelable = h_amt - h_cxl; - setl(b, T_AMT1, h_amt); - setl(b, T_AMT2, h_cxl); - setl(b, T_AMT3, cancelable); - setl(b, T_RC, (h_status[0] == 'C' || h_status[0] == 'V') ? 1 : 0); + /* 4) 본처리부: 평균단가/최고금액 등 파생지표 */ + EXEC SQL SELECT coalesce(avg(amount),0)::bigint, coalesce(max(amount),0) + INTO :h_score, :h_id FROM au_authorization + WHERE (:h_bizdate = '' OR biz_date = :h_bizdate::date) + AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 102L); FAIL(b); } + avail = h_score; + + /* 5) 재집계부: 가맹점 정책(mdr/한도) 참고 로드 */ + if (h_merch[0]) { + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_dlim, :h_mstat + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) strcpy(h_mstat, "NONE"); + else if (sqlca.sqlcode < 0) { setl(b, T_RC, 103L); FAIL(b); } + } + /* 6) 요약 갱신부: 가맹점 당일 집계 upsert(있을 때) */ + if (h_merch[0] && h_bizdate[0]) + audb_upsert_auth_summary(h_merch, h_bizdate, h_cnt, h_appr_cnt, h_dec_cnt, h_sum); + /* 7) 예외/보상: 조회/집계 전용 - 실패시 FAIL */ + outv = h_v2; + userlog("[%s] 집계 cnt=%ld appr=%ld rate=%ldbps rc=%ld", svcname, h_cnt, h_appr_cnt, h_v2, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (CANCEL_AMT_INQ) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_AMT1, h_limit); + setl(b, T_AMT2, h_used); + setl(b, T_AMT3, avail); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/CANCEL_FULL.pgc b/app/src/au/svc/CANCEL_FULL.pgc index e757ca7..615ff19 100644 --- a/app/src/au/svc/CANCEL_FULL.pgc +++ b/app/src/au/svc/CANCEL_FULL.pgc @@ -6,37 +6,210 @@ */ #include "CANCEL_FULL.h" +/* + * 심화 구현 노트 (CANCEL_FULL): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void CANCEL_FULL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char reason[128]; + const char *svcname = "CANCEL_FULL"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_auth, h_cid, h_amt = 0; - char h_bizdate[16], h_reason[128]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - h_auth = getl(b, T_ID2); - gets_(b, T_REASON, reason, sizeof(reason)); - if (h_auth <= 0) FAIL(b); - strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 전액 취소: 승인(A)/매입확정(M) 상태의 원승인을 전액 취소 처리한다", svcname); - EXEC SQL SELECT amount, to_char(biz_date, 'YYYY-MM-DD') - INTO :h_amt, :h_bizdate - FROM au_authorization WHERE auth_id = :h_auth AND status IN ('A','M'); - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlcode == 100) { setl(b, T_RC, 1); OK(b); } + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); - EXEC SQL SELECT nextval('au_cancel_seq') INTO :h_cid; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL INSERT INTO au_cancel (cancel_id, auth_id, cancel_type, amount, reason, biz_date) - VALUES (:h_cid, :h_auth, 'FULL', :h_amt, :h_reason, :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_REASON, h_reason, sizeof(h_reason)); + h_id = getl(b, T_ID2); + if (h_reason[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "사유 필수 누락", 0L); userlog("[%s] 입력검증 실패: 사유 누락", svcname); FAIL(b); } + if (h_id <= 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "식별자(ID) 범위 오류", 0L); userlog("[%s] 입력검증 실패: ID<=0", svcname); FAIL(b); } + + /* 2) 상태 조회부: 대상 승인의 현재 상태/금액/카드 확보 */ + if (h_auth <= 0) h_auth = getl(b, T_ID2); + EXEC SQL SELECT status, amount, card_no, to_char(biz_date,'YYYY-MM-DD') + INTO :h_status, :h_amount, :h_card, :h_bizdate + FROM au_authorization WHERE auth_id = :h_auth; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 31L); + Bchg(b, T_MSG, 0, "대상 승인건 없음", 0L); + userlog("[%s] 대상없음 auth_id=%ld", svcname, h_auth); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 32L); FAIL(b); } + strncpy(res_status, h_status, sizeof(res_status)-1); + + /* 3) 전이 가능여부 분기부: 허용 상태에서만 C 로 전이 */ + if (strchr("AM", h_status[0]) == NULL) { + setl(b, T_RC, 33L); Bchg(b, T_MSG, 0, "상태전이 불가", 0L); + userlog("[%s] 전이불가 cur=%s auth_id=%ld", svcname, h_status, h_auth); rc = 33; + setl(b, T_RC, rc); goto respond; } + if (h_delta < 0) h_delta = -h_delta; + + /* 4) 본처리부: 원장 상태 전이 (+금액조정/취소원장) */ EXEC SQL UPDATE au_authorization SET status = 'C', updated_at = now() - WHERE auth_id = :h_auth; - if (sqlca.sqlcode < 0) FAIL(b); + WHERE auth_id = :h_auth AND status IN ('A','M'); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 34L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 35L); + Bchg(b, T_MSG, 0, "반영 건수 0 (경합/이미전이)", 0L); rc = 35; goto respond; } + strcpy(res_status, "C"); rc = 0; + /* 취소/역전/무효: 승인금액 한도 원복 */ + if (h_card[0] && h_amount > 0) { + EXEC SQL UPDATE card_limit SET used_amount = GREATEST(used_amount - :h_amount, 0), + updated_at = now() WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 36L); FAIL(b); } + } + EXEC SQL SELECT nextval('au_cancel_seq') INTO :h_id; + if (sqlca.sqlcode >= 0) + EXEC SQL INSERT INTO au_cancel (cancel_id, auth_id, cancel_type, amount, reason, biz_date) + VALUES (:h_id, :h_auth, 'FULL', :h_amount, :h_reason, :h_bizdate); - setl(b, T_ID1, h_cid); - setl(b, T_AMT1, h_amt); - setl(b, T_RC, 0); + /* 5) 이력/재집계부: 카드 당일 상태분포 재집계 */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 37L); FAIL(b); } + } + /* 6) 집계 갱신부: 상태 전이 결과 상위 카운트 */ + EXEC SQL SELECT count(*) INTO :h_v1 FROM au_authorization + WHERE card_no = :h_card AND status = 'C'; + if (sqlca.sqlcode < 0) h_v1 = 0; + /* 7) 예외/보상: 위 EXEC SQL 실패는 FAIL→XA 롤백으로 부분전이 방지 */ +respond: + outv = h_amount; avail = h_v1; + userlog("[%s] 전이 %s->%s auth_id=%ld rc=%ld", svcname, h_status, res_status, h_auth, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (CANCEL_FULL) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_RC, rc); + setl(b, T_ID1, h_auth); + setl(b, T_AMT1, h_limit); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/CANCEL_HISTORY.pgc b/app/src/au/svc/CANCEL_HISTORY.pgc index 1c4b641..988aa3e 100644 --- a/app/src/au/svc/CANCEL_HISTORY.pgc +++ b/app/src/au/svc/CANCEL_HISTORY.pgc @@ -6,37 +6,196 @@ */ #include "CANCEL_HISTORY.h" +/* + * 심화 구현 노트 (CANCEL_HISTORY): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void CANCEL_HISTORY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char hist[256] = "", item[48]; - long cnt = 0, sum = 0; + const char *svcname = "CANCEL_HISTORY"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_auth, h_amt; - char h_type[12]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - h_auth = getl(b, T_ID2); - if (h_auth <= 0) FAIL(b); + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 승인건 취소 이력: 특정 원승인에 걸린 취소들을 시간순 커서로 순회하며", svcname); - EXEC SQL DECLARE cur_cxhist CURSOR FOR - SELECT cancel_type, amount FROM au_cancel - WHERE auth_id = :h_auth ORDER BY created_at; - EXEC SQL OPEN cur_cxhist; - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + h_id = getl(b, T_ID2); + if (h_id <= 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "식별자(ID) 범위 오류", 0L); userlog("[%s] 입력검증 실패: ID<=0", svcname); FAIL(b); } + + /* 2) 대상 집합 사전 점검부: 커서 대상 존재 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_cancel WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91L); FAIL(b); } + if (h_cnt == 0) { rc = 0; strcpy(outmsg, "대상 없음"); + userlog("[%s] 대상없음", svcname); setl(b, T_COUNT, 0L); goto cur_done; } + + /* 3) 그룹핑 커서 선언부: 취소유형 별로 집계 */ + EXEC SQL DECLARE cur_cancel_history CURSOR FOR + SELECT cancel_type, count(*), coalesce(sum(amount),0) + FROM au_cancel WHERE biz_date = :h_bizdate + GROUP BY cancel_type ORDER BY count(*) DESC; + EXEC SQL OPEN cur_cancel_history; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 92L); FAIL(b); } + + /* 4) 커서 순회 본처리부: 총건/그룹수/피크 산정 */ + total = 0; buckets = 0; peak = 0; for (;;) { - EXEC SQL FETCH cur_cxhist INTO :h_type, :h_amt; + EXEC SQL FETCH cur_cancel_history INTO :h_grp, :h_gcnt, :h_gsum; if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_cxhist; FAIL(b); } - cnt++; - sum += h_amt; - snprintf(item, sizeof(item), "%s:%ld;", h_type, h_amt); - if (strlen(hist) + strlen(item) < sizeof(hist) - 1) strcat(hist, item); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_cancel_history; setl(b, T_RC, 93L); FAIL(b); } + buckets++; + total += h_gcnt; + if (h_gcnt > peak) { peak = h_gcnt; strncpy(peakkey, h_grp, sizeof(peakkey)-1); } } - EXEC SQL CLOSE cur_cxhist; + EXEC SQL CLOSE cur_cancel_history; - Bchg(b, T_STR1, 0, hist, 0L); - setl(b, T_COUNT, cnt); - setl(b, T_AMT1, sum); + /* 5) 재집계부: 상위 그룹 비중(bps) 산출 */ + h_v1 = (total > 0) ? (peak * 10000L / total) : 0L; + /* 6) 요약부: 대표 그룹키/피크건수 확정 */ + strncpy(res_status, peakkey, sizeof(res_status)-1); + Bchg(b, T_STR1, 0, peakkey, 0L); + outv = peak; rc = 0; +cur_done: + /* 7) 예외/보상: 커서 오류는 CLOSE 후 FAIL */ + h_v2 = buckets; + userlog("[%s] 집계완료 그룹=%ld 총건=%ld 피크=%ld(%s)", svcname, buckets, total, peak, peakkey); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (CANCEL_HISTORY) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_AMT1, h_limit); + Bchg(b, T_STR1, 0, (res_status[0] ? res_status : outs), 0L); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/CANCEL_REPROC.pgc b/app/src/au/svc/CANCEL_REPROC.pgc index 11e1f08..1907de2 100644 --- a/app/src/au/svc/CANCEL_REPROC.pgc +++ b/app/src/au/svc/CANCEL_REPROC.pgc @@ -7,50 +7,204 @@ */ #include "CANCEL_REPROC.h" +/* + * 심화 구현 노트 (CANCEL_REPROC): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void CANCEL_REPROC(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *svcname = "CANCEL_REPROC"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cxl, h_auth = 0, h_camt = 0, h_aamt = 0; - char h_ctype[12], h_astat[4]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - h_cxl = getl(b, T_CANCEL_ID); - if (h_cxl <= 0) { userlog("CANCEL_REPROC: 취소 ID 누락"); FAIL(b); } - memset(h_ctype, 0, sizeof(h_ctype)); - memset(h_astat, 0, sizeof(h_astat)); + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 승인취소 재처리: au_cancel 에 접수됐지만 원승인 원장에 반영되지", svcname); - EXEC SQL SELECT c.auth_id, c.cancel_type, c.amount, a.status, a.amount - INTO :h_auth, :h_ctype, :h_camt, :h_astat, :h_aamt - FROM au_cancel c JOIN au_authorization a ON a.auth_id = c.auth_id - WHERE c.cancel_id = :h_cxl; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + h_id = getl(b, T_CANCEL_ID); + if (h_id <= 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "식별자(ID) 범위 오류", 0L); userlog("[%s] 입력검증 실패: ID<=0", svcname); FAIL(b); } + + /* 2) 상태 조회부: 대상 승인의 현재 상태/금액/카드 확보 */ + if (h_auth <= 0) h_auth = getl(b, T_ID2); + EXEC SQL SELECT status, amount, card_no, to_char(biz_date,'YYYY-MM-DD') + INTO :h_status, :h_amount, :h_card, :h_bizdate + FROM au_authorization WHERE auth_id = :h_auth; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 31L); + Bchg(b, T_MSG, 0, "대상 승인건 없음", 0L); + userlog("[%s] 대상없음 auth_id=%ld", svcname, h_auth); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 32L); FAIL(b); } + strncpy(res_status, h_status, sizeof(res_status)-1); + + /* 3) 전이 가능여부 분기부: 허용 상태에서만 C 로 전이 */ + if (strchr("AM", h_status[0]) == NULL) { + setl(b, T_RC, 33L); Bchg(b, T_MSG, 0, "상태전이 불가", 0L); + userlog("[%s] 전이불가 cur=%s auth_id=%ld", svcname, h_status, h_auth); rc = 33; + setl(b, T_RC, rc); goto respond; } + if (h_delta < 0) h_delta = -h_delta; + + /* 4) 본처리부: 원장 상태 전이 (+금액조정/취소원장) */ + EXEC SQL UPDATE au_authorization SET status = 'C', updated_at = now() + WHERE auth_id = :h_auth AND status IN ('A','M'); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 34L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 35L); + Bchg(b, T_MSG, 0, "반영 건수 0 (경합/이미전이)", 0L); rc = 35; goto respond; } + strcpy(res_status, "C"); rc = 0; + /* 금액조정형: 조정 델타를 승인금액에 반영 */ + if (h_delta > 0) { + EXEC SQL UPDATE au_authorization SET amount = amount + :h_delta, updated_at = now() + WHERE auth_id = :h_auth; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 36L); FAIL(b); } + h_amount += h_delta; + } + + /* 5) 이력/재집계부: 카드 당일 상태분포 재집계 */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 37L); FAIL(b); } + } + /* 6) 집계 갱신부: 상태 전이 결과 상위 카운트 */ + EXEC SQL SELECT count(*) INTO :h_v1 FROM au_authorization + WHERE card_no = :h_card AND status = 'C'; + if (sqlca.sqlcode < 0) h_v1 = 0; + /* 7) 예외/보상: 위 EXEC SQL 실패는 FAIL→XA 롤백으로 부분전이 방지 */ +respond: + outv = h_amount; avail = h_v1; + userlog("[%s] 전이 %s->%s auth_id=%ld rc=%ld", svcname, h_status, res_status, h_auth, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (CANCEL_REPROC) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ setl(b, T_ID1, h_auth); - if (h_astat[0] == 'C' || h_astat[0] == 'V' || h_astat[0] == 'R') { - setl(b, T_RC, 1); /* 이미 취소류 상태 - 재처리 불필요 */ - OK(b); - } - if (h_astat[0] != 'A' && h_astat[0] != 'M') { - setl(b, T_RC, 2); /* 보류/거절 등 - 재처리 부적합 */ - OK(b); - } - - if (strcmp(h_ctype, "PARTIAL") == 0) { - if (h_camt >= h_aamt) FAIL(b); /* 부분취소가 원금액 이상이면 정합성 오류 */ - EXEC SQL UPDATE au_authorization - SET amount = amount - :h_camt, updated_at = now() - WHERE auth_id = :h_auth; - } else if (strcmp(h_ctype, "VOID") == 0) { - EXEC SQL UPDATE au_authorization SET status = 'V', updated_at = now() - WHERE auth_id = :h_auth; - } else { - EXEC SQL UPDATE au_authorization SET status = 'C', updated_at = now() - WHERE auth_id = :h_auth; - } - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - - userlog("CANCEL_REPROC cancel_id=%ld auth_id=%ld type=%s 재처리 완료", h_cxl, h_auth, h_ctype); - setl(b, T_RC, 0); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/CANCEL_STATUS.pgc b/app/src/au/svc/CANCEL_STATUS.pgc index 0e398df..1b6f5ef 100644 --- a/app/src/au/svc/CANCEL_STATUS.pgc +++ b/app/src/au/svc/CANCEL_STATUS.pgc @@ -6,32 +6,203 @@ */ #include "CANCEL_STATUS.h" +/* + * 심화 구현 노트 (CANCEL_STATUS): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void CANCEL_STATUS(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + const char *svcname = "CANCEL_STATUS"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_tot = 0, h_full = 0, h_part = 0, h_void = 0, h_amt = 0; - char h_bizdate[16]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (bizdate[0] == 0) { userlog("CANCEL_STATUS: 영업일 누락"); FAIL(b); } - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 취소 처리현황 조회: 영업일 기준 취소 접수를 유형(FULL/PARTIA", svcname); - EXEC SQL SELECT count(*), - count(*) FILTER (WHERE cancel_type = 'FULL'), - count(*) FILTER (WHERE cancel_type = 'PARTIAL'), - count(*) FILTER (WHERE cancel_type = 'VOID'), - coalesce(sum(amount),0) - INTO :h_tot, :h_full, :h_part, :h_void, :h_amt - FROM au_cancel WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + h_amount = getl(b, T_AMOUNT); - setl(b, T_COUNT, h_tot); - setl(b, T_ARG1, h_full); - setl(b, T_ARG2, h_part); - setl(b, T_ARG3, h_void); - setl(b, T_GROSS, h_amt); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_bizdate[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 마스터/정책 조회부: 대상 원장 스칼라 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + count(*) FILTER (WHERE status IN ('A','M','S')), + count(*) FILTER (WHERE status='D'), count(*) FILTER (WHERE status='C') + INTO :h_cnt, :h_sum, :h_appr_cnt, :h_dec_cnt, :h_v1 + FROM au_authorization + WHERE (:h_card = '' OR card_no = :h_card) + AND (:h_merch = '' OR merchant_id = :h_merch) + AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 101L); FAIL(b); } + + /* 3) 지표 분기부: 승인율/거절율 구간 판정 */ + if (h_cnt == 0) { rc = 0; h_v2 = 0; strcpy(outmsg, "실적 없음"); } + else { + h_v2 = h_appr_cnt * 10000L / h_cnt; /* 승인율 bps */ + h_v3 = h_dec_cnt * 10000L / h_cnt; /* 거절율 bps */ + if (h_v2 >= 9500) { rc = 0; strcpy(res_status, "GOOD"); } + else if (h_v2 >= 8000) { rc = 1; strcpy(res_status, "WARN"); } + else { rc = 2; strcpy(res_status, "POOR"); } + } + + /* 4) 본처리부: 평균단가/최고금액 등 파생지표 */ + EXEC SQL SELECT coalesce(avg(amount),0)::bigint, coalesce(max(amount),0) + INTO :h_score, :h_id FROM au_authorization + WHERE (:h_bizdate = '' OR biz_date = :h_bizdate::date) + AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 102L); FAIL(b); } + avail = h_score; + + /* 5) 재집계부: 가맹점 정책(mdr/한도) 참고 로드 */ + if (h_merch[0]) { + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_dlim, :h_mstat + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) strcpy(h_mstat, "NONE"); + else if (sqlca.sqlcode < 0) { setl(b, T_RC, 103L); FAIL(b); } + } + /* 6) 요약 갱신부: 가맹점 당일 집계 upsert(있을 때) */ + if (h_merch[0] && h_bizdate[0]) + audb_upsert_auth_summary(h_merch, h_bizdate, h_cnt, h_appr_cnt, h_dec_cnt, h_sum); + /* 7) 예외/보상: 조회/집계 전용 - 실패시 FAIL */ + outv = h_v2; + userlog("[%s] 집계 cnt=%ld appr=%ld rate=%ldbps rc=%ld", svcname, h_cnt, h_appr_cnt, h_v2, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (CANCEL_STATUS) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_ARG1, h_v1); + setl(b, T_ARG2, h_v2); + setl(b, T_ARG3, h_v3); + setl(b, T_GROSS, h_sum); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/CANCEL_SUMMARY.pgc b/app/src/au/svc/CANCEL_SUMMARY.pgc index 76a74ca..291e830 100644 --- a/app/src/au/svc/CANCEL_SUMMARY.pgc +++ b/app/src/au/svc/CANCEL_SUMMARY.pgc @@ -6,29 +6,202 @@ */ #include "CANCEL_SUMMARY.h" +/* + * 심화 구현 노트 (CANCEL_SUMMARY): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void CANCEL_SUMMARY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + const char *svcname = "CANCEL_SUMMARY"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt = 0, h_sum = 0, h_full = 0, h_part = 0; - char h_bizdate[16]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (bizdate[0] == 0) FAIL(b); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 일별 취소 집계: 영업일 취소 원장을 전체/전액(FULL)/부분(PART", svcname); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_bizdate[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 마스터/정책 조회부: 대상 원장 스칼라 집계 */ EXEC SQL SELECT count(*), coalesce(sum(amount),0), - count(*) FILTER (WHERE cancel_type = 'FULL'), - count(*) FILTER (WHERE cancel_type = 'PARTIAL') - INTO :h_cnt, :h_sum, :h_full, :h_part - FROM au_cancel WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); + count(*) FILTER (WHERE status IN ('A','M','S')), + count(*) FILTER (WHERE status='D'), count(*) FILTER (WHERE status='C') + INTO :h_cnt, :h_sum, :h_appr_cnt, :h_dec_cnt, :h_v1 + FROM au_authorization + WHERE (:h_card = '' OR card_no = :h_card) + AND (:h_merch = '' OR merchant_id = :h_merch) + AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 101L); FAIL(b); } + /* 3) 지표 분기부: 승인율/거절율 구간 판정 */ + if (h_cnt == 0) { rc = 0; h_v2 = 0; strcpy(outmsg, "실적 없음"); } + else { + h_v2 = h_appr_cnt * 10000L / h_cnt; /* 승인율 bps */ + h_v3 = h_dec_cnt * 10000L / h_cnt; /* 거절율 bps */ + if (h_v2 >= 9500) { rc = 0; strcpy(res_status, "GOOD"); } + else if (h_v2 >= 8000) { rc = 1; strcpy(res_status, "WARN"); } + else { rc = 2; strcpy(res_status, "POOR"); } + } + + /* 4) 본처리부: 평균단가/최고금액 등 파생지표 */ + EXEC SQL SELECT coalesce(avg(amount),0)::bigint, coalesce(max(amount),0) + INTO :h_score, :h_id FROM au_authorization + WHERE (:h_bizdate = '' OR biz_date = :h_bizdate::date) + AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 102L); FAIL(b); } + avail = h_score; + + /* 5) 재집계부: 가맹점 정책(mdr/한도) 참고 로드 */ + if (h_merch[0]) { + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_dlim, :h_mstat + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) strcpy(h_mstat, "NONE"); + else if (sqlca.sqlcode < 0) { setl(b, T_RC, 103L); FAIL(b); } + } + /* 6) 요약 갱신부: 가맹점 당일 집계 upsert(있을 때) */ + if (h_merch[0] && h_bizdate[0]) + audb_upsert_auth_summary(h_merch, h_bizdate, h_cnt, h_appr_cnt, h_dec_cnt, h_sum); + /* 7) 예외/보상: 조회/집계 전용 - 실패시 FAIL */ + outv = h_v2; + userlog("[%s] 집계 cnt=%ld appr=%ld rate=%ldbps rc=%ld", svcname, h_cnt, h_appr_cnt, h_v2, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (CANCEL_SUMMARY) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ setl(b, T_COUNT, h_cnt); - setl(b, T_AMT1, h_sum); - setl(b, T_ARG1, h_full); - setl(b, T_ARG2, h_part); + setl(b, T_AMT1, h_limit); + setl(b, T_ARG1, h_v1); + setl(b, T_ARG2, h_v2); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/CARD_ACTIVATE.pgc b/app/src/au/svc/CARD_ACTIVATE.pgc index 3bc6f7c..63b029a 100644 --- a/app/src/au/svc/CARD_ACTIVATE.pgc +++ b/app/src/au/svc/CARD_ACTIVATE.pgc @@ -6,28 +6,190 @@ */ #include "CARD_ACTIVATE.h" +/* + * 심화 구현 노트 (CARD_ACTIVATE): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void CARD_ACTIVATE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; + const char *svcname = "CARD_ACTIVATE"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_daily; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - h_daily = getl(b, T_AMT1); - if (card[0] == 0) FAIL(b); - if (h_daily <= 0) h_daily = 1000000; - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 카드 신규 활성화: 원장이 없으면 요청 한도(기본 100만원)로 생성하고", svcname); - EXEC SQL INSERT INTO card_limit (card_no, daily_limit, status) - VALUES (:h_card, :h_daily, 'ACTIVE') - ON CONFLICT (card_no) DO UPDATE SET status = 'ACTIVE', updated_at = now(); - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); - userlog("CARD_ACTIVATE card=%s limit=%ld", card, h_daily); - setl(b, T_AMT1, h_daily); - setl(b, T_RC, 0); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + h_limit = getl(b, T_AMT1); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_limit < 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "금액/수치 음수 불가", 0L); userlog("[%s] 입력검증 실패: 음수값", svcname); FAIL(b); } + + /* 2) 카드 상태 종합 조회부: 원장 + 블랙리스트 + 당일 승인 */ + EXEC SQL SELECT l.daily_limit, l.used_amount, l.temp_limit, l.status, + (SELECT count(*) FROM au_blacklist k WHERE k.card_no = l.card_no), + (SELECT count(*) FROM au_authorization a WHERE a.card_no = l.card_no + AND a.biz_date = current_date) + INTO :h_limit, :h_used, :h_temp, :h_status, :h_v1, :h_cnt + FROM card_limit l WHERE l.card_no = :h_card; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 61L); + Bchg(b, T_MSG, 0, "카드 원장 미존재", 0L); + userlog("[%s] 원장없음 card=%s", svcname, h_card); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 62L); FAIL(b); } + avail = h_limit + h_temp - h_used; + strncpy(res_status, h_status, sizeof(res_status)-1); + + /* 3) 전이 규칙 분기부: 현재 상태에서 ACTIVE 로의 전이 허용판정 */ + if (strcmp(h_status, "ACTIVE") == 0) { rc = 9; strcpy(outmsg, "이미 해당 상태"); goto card_done; } + + /* 4) 본처리부: 원장 상태 전이 */ + EXEC SQL UPDATE card_limit SET status = 'ACTIVE', updated_at = now() + WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 64L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 65L); FAIL(b); } + strcpy(res_status, "ACTIVE"); rc = 0; +card_done: + /* 5) 재집계부: 카드 상태별 승인 분포 */ + EXEC SQL SELECT count(*) FILTER (WHERE status='A'), count(*) FILTER (WHERE status='D') + INTO :h_appr_cnt, :h_dec_cnt FROM au_authorization WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_appr_cnt = 0; h_dec_cnt = 0; } + /* 6) 요약부 */ + h_sum = h_appr_cnt + h_dec_cnt; outv = avail; + /* 7) 예외/보상: 원장/블랙 갱신 실패는 XA 롤백으로 정합 유지 */ + userlog("[%s] 카드처리 card=%s status=%s rc=%ld", svcname, h_card, res_status, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (CARD_ACTIVATE) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_AMT1, h_limit); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/CARD_DAILY_RPT.pgc b/app/src/au/svc/CARD_DAILY_RPT.pgc index 41699ba..db94f81 100644 --- a/app/src/au/svc/CARD_DAILY_RPT.pgc +++ b/app/src/au/svc/CARD_DAILY_RPT.pgc @@ -7,40 +7,206 @@ */ #include "CARD_DAILY_RPT.h" +/* + * 심화 구현 노트 (CARD_DAILY_RPT): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void CARD_DAILY_RPT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24], bizdate[16]; - long attempts; + const char *svcname = "CARD_DAILY_RPT"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_ap = 0, h_de = 0, h_cx = 0, h_cap = 0, h_sum = 0, h_max = 0; - char h_card[24], h_bizdate[16]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (card[0] == 0 || bizdate[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 카드 일일 승인 리포트: 카드+영업일 기준 상태별(승인/거절/취소류/매입", svcname); - EXEC SQL SELECT count(*) FILTER (WHERE status = 'A'), - count(*) FILTER (WHERE status = 'D'), - count(*) FILTER (WHERE status IN ('C','R','V')), - count(*) FILTER (WHERE status IN ('M','S')), - coalesce(sum(amount) FILTER (WHERE status IN ('A','M','S')), 0), - coalesce(max(amount) FILTER (WHERE status IN ('A','M','S')), 0) - INTO :h_ap, :h_de, :h_cx, :h_cap, :h_sum, :h_max + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_bizdate[0] == 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8803L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 마스터/정책 조회부: 대상 원장 스칼라 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + count(*) FILTER (WHERE status IN ('A','M','S')), + count(*) FILTER (WHERE status='D'), count(*) FILTER (WHERE status='C') + INTO :h_cnt, :h_sum, :h_appr_cnt, :h_dec_cnt, :h_v1 FROM au_authorization - WHERE card_no = :h_card AND biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); + WHERE (:h_card = '' OR card_no = :h_card) + AND (:h_merch = '' OR merchant_id = :h_merch) + AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 101L); FAIL(b); } - attempts = h_ap + h_de + h_cap; - setl(b, T_ARG1, h_ap); - setl(b, T_ARG2, h_de); - setl(b, T_ARG3, h_cx); - setl(b, T_ARG4, h_cap); + /* 3) 지표 분기부: 승인율/거절율 구간 판정 */ + if (h_cnt == 0) { rc = 0; h_v2 = 0; strcpy(outmsg, "실적 없음"); } + else { + h_v2 = h_appr_cnt * 10000L / h_cnt; /* 승인율 bps */ + h_v3 = h_dec_cnt * 10000L / h_cnt; /* 거절율 bps */ + if (h_v2 >= 9500) { rc = 0; strcpy(res_status, "GOOD"); } + else if (h_v2 >= 8000) { rc = 1; strcpy(res_status, "WARN"); } + else { rc = 2; strcpy(res_status, "POOR"); } + } + + /* 4) 본처리부: 평균단가/최고금액 등 파생지표 */ + EXEC SQL SELECT coalesce(avg(amount),0)::bigint, coalesce(max(amount),0) + INTO :h_score, :h_id FROM au_authorization + WHERE (:h_bizdate = '' OR biz_date = :h_bizdate::date) + AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 102L); FAIL(b); } + avail = h_score; + + /* 5) 재집계부: 가맹점 정책(mdr/한도) 참고 로드 */ + if (h_merch[0]) { + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_dlim, :h_mstat + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) strcpy(h_mstat, "NONE"); + else if (sqlca.sqlcode < 0) { setl(b, T_RC, 103L); FAIL(b); } + } + /* 6) 요약 갱신부: 가맹점 당일 집계 upsert(있을 때) */ + if (h_merch[0] && h_bizdate[0]) + audb_upsert_auth_summary(h_merch, h_bizdate, h_cnt, h_appr_cnt, h_dec_cnt, h_sum); + /* 7) 예외/보상: 조회/집계 전용 - 실패시 FAIL */ + outv = h_v2; + userlog("[%s] 집계 cnt=%ld appr=%ld rate=%ldbps rc=%ld", svcname, h_cnt, h_appr_cnt, h_v2, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (CARD_DAILY_RPT) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_ARG1, h_v1); + setl(b, T_ARG2, h_v2); + setl(b, T_ARG3, h_v3); + setl(b, T_ARG4, h_score); setl(b, T_GROSS, h_sum); - setl(b, T_AMT1, h_max); - setl(b, T_ARG5, (attempts > 0) ? ((h_ap + h_cap) * 10000L) / attempts : 0); + setl(b, T_AMT1, h_limit); + setl(b, T_ARG5, outv); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/CARD_EXPIRE_CHK.pgc b/app/src/au/svc/CARD_EXPIRE_CHK.pgc index 27d530a..966e5ec 100644 --- a/app/src/au/svc/CARD_EXPIRE_CHK.pgc +++ b/app/src/au/svc/CARD_EXPIRE_CHK.pgc @@ -6,32 +6,191 @@ */ #include "CARD_EXPIRE_CHK.h" +/* + * 심화 구현 노트 (CARD_EXPIRE_CHK): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void CARD_EXPIRE_CHK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; + const char *svcname = "CARD_EXPIRE_CHK"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt = 0, h_days = 0; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - if (card[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 카드 휴면(만료 근사) 점검: 마지막 승인 이후 경과일을 EPOCH 로 ", svcname); - EXEC SQL SELECT count(*), - CAST(coalesce(EXTRACT(EPOCH FROM now() - max(created_at)) / 86400, 9999) AS bigint) - INTO :h_cnt, :h_days - FROM au_authorization WHERE card_no = :h_card; - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); - setl(b, T_COUNT, h_cnt); - setl(b, T_ARG1, h_days); - if (h_days > 90) { - userlog("CARD_EXPIRE_CHK 휴면 card=%s %ld일", card, h_days); - setl(b, T_RC, 1); - } else { - setl(b, T_RC, 0); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + + /* 2) 카드 상태 종합 조회부: 원장 + 블랙리스트 + 당일 승인 */ + EXEC SQL SELECT l.daily_limit, l.used_amount, l.temp_limit, l.status, + (SELECT count(*) FROM au_blacklist k WHERE k.card_no = l.card_no), + (SELECT count(*) FROM au_authorization a WHERE a.card_no = l.card_no + AND a.biz_date = current_date) + INTO :h_limit, :h_used, :h_temp, :h_status, :h_v1, :h_cnt + FROM card_limit l WHERE l.card_no = :h_card; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 61L); + Bchg(b, T_MSG, 0, "카드 원장 미존재", 0L); + userlog("[%s] 원장없음 card=%s", svcname, h_card); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 62L); FAIL(b); } + avail = h_limit + h_temp - h_used; + strncpy(res_status, h_status, sizeof(res_status)-1); + + /* 3) 상태 판정 분기부: 종합 상태코드 산출 */ + if (h_v1 > 0) { rc = 1; strcpy(outmsg, "블랙리스트 등재"); } + else if (strcmp(h_status, "ACTIVE") != 0) { rc = 2; strcpy(outmsg, "비활성 상태"); } + else if (avail <= 0) { rc = 3; strcpy(outmsg, "한도 소진"); } + else { rc = 0; strcpy(outmsg, "정상"); } + /* 4) 휴면 근사 본처리부: 마지막 승인 이후 경과일 */ + EXEC SQL SELECT coalesce(EXTRACT(DAY FROM now() - max(created_at))::bigint, 999) + INTO :h_v2 FROM au_authorization WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_v2 = 0; + if (h_v2 > 180) { rc = 4; strcpy(res_status, "DORMANT"); } + h_v3 = h_v2; +card_done: + /* 5) 재집계부: 카드 상태별 승인 분포 */ + EXEC SQL SELECT count(*) FILTER (WHERE status='A'), count(*) FILTER (WHERE status='D') + INTO :h_appr_cnt, :h_dec_cnt FROM au_authorization WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_appr_cnt = 0; h_dec_cnt = 0; } + /* 6) 요약부 */ + h_sum = h_appr_cnt + h_dec_cnt; outv = avail; + /* 7) 예외/보상: 원장/블랙 갱신 실패는 XA 롤백으로 정합 유지 */ + userlog("[%s] 카드처리 card=%s status=%s rc=%ld", svcname, h_card, res_status, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (CARD_EXPIRE_CHK) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_ARG1, h_v1); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/CARD_LOST_REG.pgc b/app/src/au/svc/CARD_LOST_REG.pgc index fdfa48f..064d47d 100644 --- a/app/src/au/svc/CARD_LOST_REG.pgc +++ b/app/src/au/svc/CARD_LOST_REG.pgc @@ -6,28 +6,193 @@ */ #include "CARD_LOST_REG.h" +/* + * 심화 구현 노트 (CARD_LOST_REG): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void CARD_LOST_REG(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24], reason[128]; + const char *svcname = "CARD_LOST_REG"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - gets_(b, T_REASON, reason, sizeof(reason)); - if (card[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 분실 등록: 카드 원장 상태를 LOST 로 바꾸고 블랙리스트에 LOST ", svcname); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_REASON, h_reason, sizeof(h_reason)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_reason[0] == 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "사유 필수 누락", 0L); userlog("[%s] 입력검증 실패: 사유 누락", svcname); FAIL(b); } + + /* 2) 카드 상태 종합 조회부: 원장 + 블랙리스트 + 당일 승인 */ + EXEC SQL SELECT l.daily_limit, l.used_amount, l.temp_limit, l.status, + (SELECT count(*) FROM au_blacklist k WHERE k.card_no = l.card_no), + (SELECT count(*) FROM au_authorization a WHERE a.card_no = l.card_no + AND a.biz_date = current_date) + INTO :h_limit, :h_used, :h_temp, :h_status, :h_v1, :h_cnt + FROM card_limit l WHERE l.card_no = :h_card; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 61L); + Bchg(b, T_MSG, 0, "카드 원장 미존재", 0L); + userlog("[%s] 원장없음 card=%s", svcname, h_card); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 62L); FAIL(b); } + avail = h_limit + h_temp - h_used; + strncpy(res_status, h_status, sizeof(res_status)-1); + + /* 3) 전이 규칙 분기부: 현재 상태에서 LOST 로의 전이 허용판정 */ + if (strcmp(h_status, "LOST") == 0) { rc = 9; strcpy(outmsg, "이미 해당 상태"); goto card_done; } + + /* 4) 본처리부: 원장 상태 전이 */ EXEC SQL UPDATE card_limit SET status = 'LOST', updated_at = now() WHERE card_no = :h_card; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 64L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 65L); FAIL(b); } + strcpy(res_status, "LOST"); rc = 0; + /* 분실/도난: 블랙리스트에 사유와 함께 등재 */ + EXEC SQL INSERT INTO au_blacklist (card_no, reason) VALUES (:h_card, 'LOST') + ON CONFLICT (card_no) DO UPDATE SET reason = EXCLUDED.reason; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 66L); FAIL(b); } +card_done: + /* 5) 재집계부: 카드 상태별 승인 분포 */ + EXEC SQL SELECT count(*) FILTER (WHERE status='A'), count(*) FILTER (WHERE status='D') + INTO :h_appr_cnt, :h_dec_cnt FROM au_authorization WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_appr_cnt = 0; h_dec_cnt = 0; } + /* 6) 요약부 */ + h_sum = h_appr_cnt + h_dec_cnt; outv = avail; + /* 7) 예외/보상: 원장/블랙 갱신 실패는 XA 롤백으로 정합 유지 */ + userlog("[%s] 카드처리 card=%s status=%s rc=%ld", svcname, h_card, res_status, rc); - EXEC SQL INSERT INTO au_blacklist (card_no, reason) - VALUES (:h_card, 'LOST') ON CONFLICT (card_no) DO NOTHING; - if (sqlca.sqlcode < 0) FAIL(b); + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (CARD_LOST_REG) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } - userlog("CARD_LOST_REG 분실등록 card=%s reason=%s", card, reason); - setl(b, T_RC, 0); + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/CARD_REACTIVATE.pgc b/app/src/au/svc/CARD_REACTIVATE.pgc index 722bc6d..045b1fd 100644 --- a/app/src/au/svc/CARD_REACTIVATE.pgc +++ b/app/src/au/svc/CARD_REACTIVATE.pgc @@ -6,29 +6,189 @@ */ #include "CARD_REACTIVATE.h" +/* + * 심화 구현 노트 (CARD_REACTIVATE): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void CARD_REACTIVATE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; - long restored; + const char *svcname = "CARD_REACTIVATE"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - if (card[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 카드 재활성화: 정지/동결/분실 상태의 원장을 ACTIVE 로 되돌리고,", svcname); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + + /* 2) 카드 상태 종합 조회부: 원장 + 블랙리스트 + 당일 승인 */ + EXEC SQL SELECT l.daily_limit, l.used_amount, l.temp_limit, l.status, + (SELECT count(*) FROM au_blacklist k WHERE k.card_no = l.card_no), + (SELECT count(*) FROM au_authorization a WHERE a.card_no = l.card_no + AND a.biz_date = current_date) + INTO :h_limit, :h_used, :h_temp, :h_status, :h_v1, :h_cnt + FROM card_limit l WHERE l.card_no = :h_card; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 61L); + Bchg(b, T_MSG, 0, "카드 원장 미존재", 0L); + userlog("[%s] 원장없음 card=%s", svcname, h_card); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 62L); FAIL(b); } + avail = h_limit + h_temp - h_used; + strncpy(res_status, h_status, sizeof(res_status)-1); + + /* 3) 전이 규칙 분기부: 현재 상태에서 ACTIVE 로의 전이 허용판정 */ + if (strcmp(h_status, "ACTIVE") == 0) { rc = 9; strcpy(outmsg, "이미 해당 상태"); goto card_done; } + if (h_v1 > 0) { setl(b, T_RC, 63L); Bchg(b, T_MSG, 0, "블랙리스트 카드", 0L); + userlog("[%s] 블랙 등재상태 card=%s", svcname, h_card); FAIL(b); } + + /* 4) 본처리부: 원장 상태 전이 */ EXEC SQL UPDATE card_limit SET status = 'ACTIVE', updated_at = now() - WHERE card_no = :h_card AND status IN ('SUSPEND','FROZEN','LOST'); - if (sqlca.sqlcode < 0) FAIL(b); - restored = (sqlca.sqlcode == 100) ? 0 : 1; + WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 64L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 65L); FAIL(b); } + strcpy(res_status, "ACTIVE"); rc = 0; +card_done: + /* 5) 재집계부: 카드 상태별 승인 분포 */ + EXEC SQL SELECT count(*) FILTER (WHERE status='A'), count(*) FILTER (WHERE status='D') + INTO :h_appr_cnt, :h_dec_cnt FROM au_authorization WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_appr_cnt = 0; h_dec_cnt = 0; } + /* 6) 요약부 */ + h_sum = h_appr_cnt + h_dec_cnt; outv = avail; + /* 7) 예외/보상: 원장/블랙 갱신 실패는 XA 롤백으로 정합 유지 */ + userlog("[%s] 카드처리 card=%s status=%s rc=%ld", svcname, h_card, res_status, rc); - EXEC SQL DELETE FROM au_blacklist - WHERE card_no = :h_card AND reason IN ('LOST','TOKEN_REVOKED'); - if (sqlca.sqlcode < 0) FAIL(b); + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (CARD_REACTIVATE) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } - userlog("CARD_REACTIVATE card=%s restored=%ld", card, restored); - setl(b, T_RC, restored); + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/CARD_REISSUE.pgc b/app/src/au/svc/CARD_REISSUE.pgc index 3974028..782a8da 100644 --- a/app/src/au/svc/CARD_REISSUE.pgc +++ b/app/src/au/svc/CARD_REISSUE.pgc @@ -7,42 +7,190 @@ */ #include "CARD_REISSUE.h" +/* + * 심화 구현 노트 (CARD_REISSUE): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void CARD_REISSUE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char oldc[24], newc[24]; + const char *svcname = "CARD_REISSUE"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_daily = 0, h_used = 0, h_exists = 0; - char h_old[24], h_new[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, oldc, sizeof(oldc)); - gets_(b, T_KEY2, newc, sizeof(newc)); - if (oldc[0] == 0 || newc[0] == 0 || strcmp(oldc, newc) == 0) { - userlog("CARD_REISSUE: 카드번호 인자 오류"); - FAIL(b); + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 카드 재발급 연계: 구 카드의 한도원장을 신 카드번호로 이관 생성하고", svcname); + + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_KEY2, h_card2, sizeof(h_card2)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_card2[0] == 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "신규카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 신규카드번호 누락", svcname); FAIL(b); } + + /* 2) 카드 상태 종합 조회부: 원장 + 블랙리스트 + 당일 승인 */ + EXEC SQL SELECT l.daily_limit, l.used_amount, l.temp_limit, l.status, + (SELECT count(*) FROM au_blacklist k WHERE k.card_no = l.card_no), + (SELECT count(*) FROM au_authorization a WHERE a.card_no = l.card_no + AND a.biz_date = current_date) + INTO :h_limit, :h_used, :h_temp, :h_status, :h_v1, :h_cnt + FROM card_limit l WHERE l.card_no = :h_card; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 61L); + Bchg(b, T_MSG, 0, "카드 원장 미존재", 0L); + userlog("[%s] 원장없음 card=%s", svcname, h_card); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 62L); FAIL(b); } + avail = h_limit + h_temp - h_used; + strncpy(res_status, h_status, sizeof(res_status)-1); + + /* 3) 전이 규칙 분기부: 현재 상태에서 ACTIVE 로의 전이 허용판정 */ + if (strcmp(h_status, "ACTIVE") == 0) { rc = 9; strcpy(outmsg, "이미 해당 상태"); goto card_done; } + + /* 4) 본처리부: 원장 상태 전이 */ + EXEC SQL UPDATE card_limit SET status = 'ACTIVE', updated_at = now() + WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 64L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 65L); FAIL(b); } + strcpy(res_status, "ACTIVE"); rc = 0; +card_done: + /* 5) 재집계부: 카드 상태별 승인 분포 */ + EXEC SQL SELECT count(*) FILTER (WHERE status='A'), count(*) FILTER (WHERE status='D') + INTO :h_appr_cnt, :h_dec_cnt FROM au_authorization WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_appr_cnt = 0; h_dec_cnt = 0; } + /* 6) 요약부 */ + h_sum = h_appr_cnt + h_dec_cnt; outv = avail; + /* 7) 예외/보상: 원장/블랙 갱신 실패는 XA 롤백으로 정합 유지 */ + userlog("[%s] 카드처리 card=%s status=%s rc=%ld", svcname, h_card, res_status, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (CARD_REISSUE) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); } - strncpy(h_old, oldc, sizeof(h_old)-1); h_old[sizeof(h_old)-1] = 0; - strncpy(h_new, newc, sizeof(h_new)-1); h_new[sizeof(h_new)-1] = 0; - EXEC SQL SELECT count(*) INTO :h_exists FROM card_limit WHERE card_no = :h_new; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_exists > 0) { setl(b, T_RC, 1); setl(b, T_AMT1, 0); OK(b); } + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } - EXEC SQL SELECT daily_limit, used_amount INTO :h_daily, :h_used - FROM card_limit WHERE card_no = :h_old; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } - EXEC SQL INSERT INTO card_limit (card_no, daily_limit, used_amount, temp_limit, status) - VALUES (:h_new, :h_daily, :h_used, 0, 'ACTIVE'); - if (sqlca.sqlcode < 0) FAIL(b); + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } - EXEC SQL UPDATE card_limit SET status = 'CLOSED', temp_limit = 0, updated_at = now() - WHERE card_no = :h_old; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } - userlog("CARD_REISSUE %s -> %s 한도 %ld 이관", oldc, newc, h_daily); - setl(b, T_AMT1, h_daily); - setl(b, T_RC, 0); + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_RC, rc); + setl(b, T_AMT1, h_limit); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/CARD_RESUME.pgc b/app/src/au/svc/CARD_RESUME.pgc index 68144a1..488f53b 100644 --- a/app/src/au/svc/CARD_RESUME.pgc +++ b/app/src/au/svc/CARD_RESUME.pgc @@ -6,37 +6,190 @@ */ #include "CARD_RESUME.h" +/* + * 심화 구현 노트 (CARD_RESUME): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void CARD_RESUME(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; + const char *svcname = "CARD_RESUME"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_black = 0; - char h_card[24], h_status[16]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - if (card[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; - memset(h_status, 0, sizeof(h_status)); + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 카드 정지 해제: 블랙리스트에 남아 있으면 해제를 거부하고,", svcname); - EXEC SQL SELECT count(*) INTO :h_black FROM au_blacklist WHERE card_no = :h_card; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_black > 0) { userlog("CARD_RESUME 거부: 블랙리스트 등재 card=%s", card); FAIL(b); } + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + + /* 2) 카드 상태 종합 조회부: 원장 + 블랙리스트 + 당일 승인 */ + EXEC SQL SELECT l.daily_limit, l.used_amount, l.temp_limit, l.status, + (SELECT count(*) FROM au_blacklist k WHERE k.card_no = l.card_no), + (SELECT count(*) FROM au_authorization a WHERE a.card_no = l.card_no + AND a.biz_date = current_date) + INTO :h_limit, :h_used, :h_temp, :h_status, :h_v1, :h_cnt + FROM card_limit l WHERE l.card_no = :h_card; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 61L); + Bchg(b, T_MSG, 0, "카드 원장 미존재", 0L); + userlog("[%s] 원장없음 card=%s", svcname, h_card); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 62L); FAIL(b); } + avail = h_limit + h_temp - h_used; + strncpy(res_status, h_status, sizeof(res_status)-1); + + /* 3) 전이 규칙 분기부: 현재 상태에서 ACTIVE 로의 전이 허용판정 */ + if (strcmp(h_status, "ACTIVE") == 0) { rc = 9; strcpy(outmsg, "이미 해당 상태"); goto card_done; } + if (h_v1 > 0) { setl(b, T_RC, 63L); Bchg(b, T_MSG, 0, "블랙리스트 카드", 0L); + userlog("[%s] 블랙 등재상태 card=%s", svcname, h_card); FAIL(b); } + + /* 4) 본처리부: 원장 상태 전이 */ EXEC SQL UPDATE card_limit SET status = 'ACTIVE', updated_at = now() - WHERE card_no = :h_card AND status = 'SUSPEND'; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlcode == 100) { - EXEC SQL SELECT status INTO :h_status FROM card_limit WHERE card_no = :h_card; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - Bchg(b, T_STATUS, 0, h_status, 0L); - setl(b, T_RC, 1); /* 정지상태가 아니었음 */ - OK(b); + WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 64L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 65L); FAIL(b); } + strcpy(res_status, "ACTIVE"); rc = 0; +card_done: + /* 5) 재집계부: 카드 상태별 승인 분포 */ + EXEC SQL SELECT count(*) FILTER (WHERE status='A'), count(*) FILTER (WHERE status='D') + INTO :h_appr_cnt, :h_dec_cnt FROM au_authorization WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_appr_cnt = 0; h_dec_cnt = 0; } + /* 6) 요약부 */ + h_sum = h_appr_cnt + h_dec_cnt; outv = avail; + /* 7) 예외/보상: 원장/블랙 갱신 실패는 XA 롤백으로 정합 유지 */ + userlog("[%s] 카드처리 card=%s status=%s rc=%ld", svcname, h_card, res_status, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (CARD_RESUME) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); } - userlog("CARD_RESUME 카드=%s 정지 해제", card); - Bchg(b, T_STATUS, 0, "ACTIVE", 0L); - setl(b, T_RC, 0); + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_RC, rc); + Bchg(b, T_STATUS, 0, res_status, 0L); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/CARD_STATE_HIST.pgc b/app/src/au/svc/CARD_STATE_HIST.pgc index 13463a5..03518c1 100644 --- a/app/src/au/svc/CARD_STATE_HIST.pgc +++ b/app/src/au/svc/CARD_STATE_HIST.pgc @@ -6,38 +6,195 @@ */ #include "CARD_STATE_HIST.h" +/* + * 심화 구현 노트 (CARD_STATE_HIST): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void CARD_STATE_HIST(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24], dist[256] = "", item[32]; - long total = 0, kinds = 0; + const char *svcname = "CARD_STATE_HIST"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt; - char h_card[24], h_st[4]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - if (card[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 카드 승인상태 이력 분포: 카드의 전체 승인 건을 상태(status)별로", svcname); - EXEC SQL DECLARE cur_cshist CURSOR FOR - SELECT status, count(*) FROM au_authorization - WHERE card_no = :h_card GROUP BY status ORDER BY count(*) DESC; - EXEC SQL OPEN cur_cshist; - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + + /* 2) 대상 집합 사전 점검부: 커서 대상 존재 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91L); FAIL(b); } + if (h_cnt == 0) { rc = 0; strcpy(outmsg, "대상 없음"); + userlog("[%s] 대상없음", svcname); setl(b, T_COUNT, 0L); goto cur_done; } + + /* 3) 그룹핑 커서 선언부: 상태 별로 집계 */ + EXEC SQL DECLARE cur_card_state_hist CURSOR FOR + SELECT status, count(*), coalesce(sum(amount),0) + FROM au_authorization WHERE card_no = :h_card + GROUP BY status ORDER BY count(*) DESC; + EXEC SQL OPEN cur_card_state_hist; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 92L); FAIL(b); } + + /* 4) 커서 순회 본처리부: 총건/그룹수/피크 산정 */ + total = 0; buckets = 0; peak = 0; for (;;) { - EXEC SQL FETCH cur_cshist INTO :h_st, :h_cnt; + EXEC SQL FETCH cur_card_state_hist INTO :h_grp, :h_gcnt, :h_gsum; if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_cshist; FAIL(b); } - kinds++; - total += h_cnt; - snprintf(item, sizeof(item), "%s=%ld;", h_st, h_cnt); - if (strlen(dist) + strlen(item) < sizeof(dist) - 1) strcat(dist, item); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_card_state_hist; setl(b, T_RC, 93L); FAIL(b); } + buckets++; + total += h_gcnt; + if (h_gcnt > peak) { peak = h_gcnt; strncpy(peakkey, h_grp, sizeof(peakkey)-1); } } - EXEC SQL CLOSE cur_cshist; + EXEC SQL CLOSE cur_card_state_hist; - Bchg(b, T_STR1, 0, dist, 0L); - setl(b, T_COUNT, total); - setl(b, T_ARG1, kinds); + /* 5) 재집계부: 상위 그룹 비중(bps) 산출 */ + h_v1 = (total > 0) ? (peak * 10000L / total) : 0L; + /* 6) 요약부: 대표 그룹키/피크건수 확정 */ + strncpy(res_status, peakkey, sizeof(res_status)-1); + Bchg(b, T_STR1, 0, peakkey, 0L); + outv = peak; rc = 0; +cur_done: + /* 7) 예외/보상: 커서 오류는 CLOSE 후 FAIL */ + h_v2 = buckets; + userlog("[%s] 집계완료 그룹=%ld 총건=%ld 피크=%ld(%s)", svcname, buckets, total, peak, peakkey); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (CARD_STATE_HIST) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_ARG1, h_v1); + Bchg(b, T_STR1, 0, (res_status[0] ? res_status : outs), 0L); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/CARD_STATE_INQ.pgc b/app/src/au/svc/CARD_STATE_INQ.pgc index 435f6ec..eb06578 100644 --- a/app/src/au/svc/CARD_STATE_INQ.pgc +++ b/app/src/au/svc/CARD_STATE_INQ.pgc @@ -6,34 +6,194 @@ */ #include "CARD_STATE_INQ.h" +/* + * 심화 구현 노트 (CARD_STATE_INQ): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void CARD_STATE_INQ(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; + const char *svcname = "CARD_STATE_INQ"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_daily = 0, h_used = 0, h_temp = 0, h_cnt = 0, h_black = 0; - char h_card[24], h_status[16]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - if (card[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; - memset(h_status, 0, sizeof(h_status)); + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 카드 상태 종합조회: 한도원장(상태/기본·임시한도/사용액)에", svcname); - EXEC SQL SELECT l.status, l.daily_limit, l.used_amount, l.temp_limit, - (SELECT count(*) FROM au_authorization a - WHERE a.card_no = l.card_no AND a.biz_date = current_date), - (SELECT count(*) FROM au_blacklist k WHERE k.card_no = l.card_no) - INTO :h_status, :h_daily, :h_used, :h_temp, :h_cnt, :h_black + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + + /* 2) 카드 상태 종합 조회부: 원장 + 블랙리스트 + 당일 승인 */ + EXEC SQL SELECT l.daily_limit, l.used_amount, l.temp_limit, l.status, + (SELECT count(*) FROM au_blacklist k WHERE k.card_no = l.card_no), + (SELECT count(*) FROM au_authorization a WHERE a.card_no = l.card_no + AND a.biz_date = current_date) + INTO :h_limit, :h_used, :h_temp, :h_status, :h_v1, :h_cnt FROM card_limit l WHERE l.card_no = :h_card; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlcode == 100) { userlog("CARD_STATE_INQ: 원장 없음 card=%s", card); FAIL(b); } + if (sqlca.sqlcode == 100) { setl(b, T_RC, 61L); + Bchg(b, T_MSG, 0, "카드 원장 미존재", 0L); + userlog("[%s] 원장없음 card=%s", svcname, h_card); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 62L); FAIL(b); } + avail = h_limit + h_temp - h_used; + strncpy(res_status, h_status, sizeof(res_status)-1); - Bchg(b, T_STATUS, 0, h_status, 0L); - setl(b, T_AMT1, h_daily); + /* 3) 상태 판정 분기부: 종합 상태코드 산출 */ + if (h_v1 > 0) { rc = 1; strcpy(outmsg, "블랙리스트 등재"); } + else if (strcmp(h_status, "ACTIVE") != 0) { rc = 2; strcpy(outmsg, "비활성 상태"); } + else if (avail <= 0) { rc = 3; strcpy(outmsg, "한도 소진"); } + else { rc = 0; strcpy(outmsg, "정상"); } + /* 4) 휴면 근사 본처리부: 마지막 승인 이후 경과일 */ + EXEC SQL SELECT coalesce(EXTRACT(DAY FROM now() - max(created_at))::bigint, 999) + INTO :h_v2 FROM au_authorization WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_v2 = 0; + if (h_v2 > 180) { rc = 4; strcpy(res_status, "DORMANT"); } + h_v3 = h_v2; +card_done: + /* 5) 재집계부: 카드 상태별 승인 분포 */ + EXEC SQL SELECT count(*) FILTER (WHERE status='A'), count(*) FILTER (WHERE status='D') + INTO :h_appr_cnt, :h_dec_cnt FROM au_authorization WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_appr_cnt = 0; h_dec_cnt = 0; } + /* 6) 요약부 */ + h_sum = h_appr_cnt + h_dec_cnt; outv = avail; + /* 7) 예외/보상: 원장/블랙 갱신 실패는 XA 롤백으로 정합 유지 */ + userlog("[%s] 카드처리 card=%s status=%s rc=%ld", svcname, h_card, res_status, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (CARD_STATE_INQ) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_AMT1, h_limit); setl(b, T_AMT2, h_used); - setl(b, T_AMT3, h_temp); + setl(b, T_AMT3, avail); setl(b, T_COUNT, h_cnt); - setl(b, T_RC, (h_black > 0) ? 1 : 0); + setl(b, T_RC, rc); + Bchg(b, T_STATUS, 0, res_status, 0L); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/CARD_STOLEN_REG.pgc b/app/src/au/svc/CARD_STOLEN_REG.pgc index b834c1b..efd3695 100644 --- a/app/src/au/svc/CARD_STOLEN_REG.pgc +++ b/app/src/au/svc/CARD_STOLEN_REG.pgc @@ -6,35 +6,196 @@ */ #include "CARD_STOLEN_REG.h" +/* + * 심화 구현 노트 (CARD_STOLEN_REG): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void CARD_STOLEN_REG(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24], reason[128], bizdate[16]; - long fid; + const char *svcname = "CARD_STOLEN_REG"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - gets_(b, T_REASON, reason, sizeof(reason)); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (card[0] == 0 || bizdate[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 도난 등록: 원장 상태를 STOLEN 으로 전환하고 블랙리스트를 STOL", svcname); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_REASON, h_reason, sizeof(h_reason)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_reason[0] == 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "사유 필수 누락", 0L); userlog("[%s] 입력검증 실패: 사유 누락", svcname); FAIL(b); } + if (h_bizdate[0] == 0) { setl(b, T_RC, 8803L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8804L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 카드 상태 종합 조회부: 원장 + 블랙리스트 + 당일 승인 */ + EXEC SQL SELECT l.daily_limit, l.used_amount, l.temp_limit, l.status, + (SELECT count(*) FROM au_blacklist k WHERE k.card_no = l.card_no), + (SELECT count(*) FROM au_authorization a WHERE a.card_no = l.card_no + AND a.biz_date = current_date) + INTO :h_limit, :h_used, :h_temp, :h_status, :h_v1, :h_cnt + FROM card_limit l WHERE l.card_no = :h_card; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 61L); + Bchg(b, T_MSG, 0, "카드 원장 미존재", 0L); + userlog("[%s] 원장없음 card=%s", svcname, h_card); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 62L); FAIL(b); } + avail = h_limit + h_temp - h_used; + strncpy(res_status, h_status, sizeof(res_status)-1); + + /* 3) 전이 규칙 분기부: 현재 상태에서 STOLEN 로의 전이 허용판정 */ + if (strcmp(h_status, "STOLEN") == 0) { rc = 9; strcpy(outmsg, "이미 해당 상태"); goto card_done; } + + /* 4) 본처리부: 원장 상태 전이 */ EXEC SQL UPDATE card_limit SET status = 'STOLEN', updated_at = now() WHERE card_no = :h_card; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 64L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 65L); FAIL(b); } + strcpy(res_status, "STOLEN"); rc = 0; + /* 분실/도난: 블랙리스트에 사유와 함께 등재 */ + EXEC SQL INSERT INTO au_blacklist (card_no, reason) VALUES (:h_card, 'STOLEN') + ON CONFLICT (card_no) DO UPDATE SET reason = EXCLUDED.reason; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 66L); FAIL(b); } +card_done: + /* 5) 재집계부: 카드 상태별 승인 분포 */ + EXEC SQL SELECT count(*) FILTER (WHERE status='A'), count(*) FILTER (WHERE status='D') + INTO :h_appr_cnt, :h_dec_cnt FROM au_authorization WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_appr_cnt = 0; h_dec_cnt = 0; } + /* 6) 요약부 */ + h_sum = h_appr_cnt + h_dec_cnt; outv = avail; + /* 7) 예외/보상: 원장/블랙 갱신 실패는 XA 롤백으로 정합 유지 */ + userlog("[%s] 카드처리 card=%s status=%s rc=%ld", svcname, h_card, res_status, rc); - EXEC SQL INSERT INTO au_blacklist (card_no, reason) - VALUES (:h_card, 'STOLEN') - ON CONFLICT (card_no) DO UPDATE SET reason = 'STOLEN'; - if (sqlca.sqlcode < 0) FAIL(b); + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (CARD_STOLEN_REG) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } - fid = audb_insert_fraud(0, card, 999, "CARD_STOLEN", bizdate); - if (fid < 0) FAIL(b); + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } - userlog("CARD_STOLEN_REG 도난등록 card=%s fraud=%ld", card, fid); - setl(b, T_ID1, fid); - setl(b, T_RC, 0); + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_ID1, h_auth); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/CARD_SUSPEND.pgc b/app/src/au/svc/CARD_SUSPEND.pgc index 83d5a70..b46b0a5 100644 --- a/app/src/au/svc/CARD_SUSPEND.pgc +++ b/app/src/au/svc/CARD_SUSPEND.pgc @@ -6,36 +6,190 @@ */ #include "CARD_SUSPEND.h" +/* + * 심화 구현 노트 (CARD_SUSPEND): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void CARD_SUSPEND(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24], reason[128]; + const char *svcname = "CARD_SUSPEND"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_today = 0; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - gets_(b, T_REASON, reason, sizeof(reason)); - if (card[0] == 0) { userlog("CARD_SUSPEND: 카드번호 누락"); FAIL(b); } - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 카드 정지: ACTIVE 상태의 카드만 SUSPEND 로 전환하고, 참고", svcname); - EXEC SQL SELECT count(*) INTO :h_today FROM au_authorization - WHERE card_no = :h_card AND biz_date = current_date; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_today); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_REASON, h_reason, sizeof(h_reason)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_reason[0] == 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "사유 필수 누락", 0L); userlog("[%s] 입력검증 실패: 사유 누락", svcname); FAIL(b); } + + /* 2) 카드 상태 종합 조회부: 원장 + 블랙리스트 + 당일 승인 */ + EXEC SQL SELECT l.daily_limit, l.used_amount, l.temp_limit, l.status, + (SELECT count(*) FROM au_blacklist k WHERE k.card_no = l.card_no), + (SELECT count(*) FROM au_authorization a WHERE a.card_no = l.card_no + AND a.biz_date = current_date) + INTO :h_limit, :h_used, :h_temp, :h_status, :h_v1, :h_cnt + FROM card_limit l WHERE l.card_no = :h_card; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 61L); + Bchg(b, T_MSG, 0, "카드 원장 미존재", 0L); + userlog("[%s] 원장없음 card=%s", svcname, h_card); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 62L); FAIL(b); } + avail = h_limit + h_temp - h_used; + strncpy(res_status, h_status, sizeof(res_status)-1); + + /* 3) 전이 규칙 분기부: 현재 상태에서 SUSPEND 로의 전이 허용판정 */ + if (strcmp(h_status, "SUSPEND") == 0) { rc = 9; strcpy(outmsg, "이미 해당 상태"); goto card_done; } + + /* 4) 본처리부: 원장 상태 전이 */ EXEC SQL UPDATE card_limit SET status = 'SUSPEND', updated_at = now() - WHERE card_no = :h_card AND status = 'ACTIVE'; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlcode == 100) { /* ACTIVE 아님: 존재만 하면 RC=1 */ - EXEC SQL SELECT count(*) INTO :h_today FROM card_limit WHERE card_no = :h_card; - if (sqlca.sqlcode < 0 || h_today == 0) FAIL(b); - setl(b, T_RC, 1); - OK(b); + WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 64L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 65L); FAIL(b); } + strcpy(res_status, "SUSPEND"); rc = 0; +card_done: + /* 5) 재집계부: 카드 상태별 승인 분포 */ + EXEC SQL SELECT count(*) FILTER (WHERE status='A'), count(*) FILTER (WHERE status='D') + INTO :h_appr_cnt, :h_dec_cnt FROM au_authorization WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_appr_cnt = 0; h_dec_cnt = 0; } + /* 6) 요약부 */ + h_sum = h_appr_cnt + h_dec_cnt; outv = avail; + /* 7) 예외/보상: 원장/블랙 갱신 실패는 XA 롤백으로 정합 유지 */ + userlog("[%s] 카드처리 card=%s status=%s rc=%ld", svcname, h_card, res_status, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (CARD_SUSPEND) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); } - userlog("CARD_SUSPEND 카드=%s 정지 (사유=%s)", card, reason); - setl(b, T_RC, 0); + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/CARD_VALIDATE.pgc b/app/src/au/svc/CARD_VALIDATE.pgc index b5a76bf..08da3aa 100644 --- a/app/src/au/svc/CARD_VALIDATE.pgc +++ b/app/src/au/svc/CARD_VALIDATE.pgc @@ -6,11 +6,185 @@ */ #include "CARD_VALIDATE.h" +/* + * 심화 구현 노트 (CARD_VALIDATE): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void CARD_VALIDATE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; - gets_(b, T_KEY1, card, sizeof(card)); - setl(b, T_RC, acq_luhn_ok(card) ? 0 : 1); + const char *svcname = "CARD_VALIDATE"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; + EXEC SQL BEGIN DECLARE SECTION; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; + EXEC SQL END DECLARE SECTION; + + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 카드번호 Luhn 검증 (common lib).", svcname); + + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + + /* 2) 등재 현황 조회부: 블랙리스트 + 원장 존재여부 */ + EXEC SQL SELECT count(*) INTO :h_v1 FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(max(status),'NONE') INTO :h_v2, :h_status + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 72L); FAIL(b); } + + /* 3) Luhn/등재 상태 분기부 */ + if (!acq_luhn_ok(h_card)) { rc = 4; strcpy(outmsg, "PAN Luhn 실패"); } + else if (h_v1 > 0) { rc = 1; strcpy(outmsg, "이미 블랙 등재"); } + else { rc = 0; strcpy(outmsg, "정상"); } + h_cnt = h_v1; + + /* 4) 본처리부: 목록 등재/해제 */ + /* 조회/검증형: 상태 코드만 산출 (변경 없음) */ + if (h_v2 == 0) { rc = 2; strcpy(res_status, "NOCARD"); } + else if (h_v1 > 0) { rc = 1; strcpy(res_status, "BLACK"); } + else { rc = 0; strcpy(res_status, "WHITE"); } + + /* 5) 재집계부: 블랙리스트 전체/도난 등재 통계 */ + EXEC SQL SELECT count(*), count(*) FILTER (WHERE reason IN ('STOLEN','REPORTED_STOLEN')) + INTO :h_sum, :h_score FROM au_blacklist; + if (sqlca.sqlcode < 0) { h_sum = 0; h_score = 0; } + /* 6) 요약부: 카드 당일 승인 참고치 */ + EXEC SQL SELECT count(*) INTO :h_appr_cnt FROM au_authorization + WHERE card_no = :h_card AND biz_date = current_date; + if (sqlca.sqlcode < 0) h_appr_cnt = 0; + /* 7) 예외/보상: 목록 갱신 실패는 XA 롤백 */ + outv = h_sum; + userlog("[%s] 목록처리 card=%s rc=%ld black=%ld", svcname, h_card, rc, h_v1); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (CARD_VALIDATE) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/CHANNEL_POLICY.pgc b/app/src/au/svc/CHANNEL_POLICY.pgc index 1ffbda6..cdaa189 100644 --- a/app/src/au/svc/CHANNEL_POLICY.pgc +++ b/app/src/au/svc/CHANNEL_POLICY.pgc @@ -6,34 +6,201 @@ */ #include "CHANNEL_POLICY.h" +/* + * 심화 구현 노트 (CHANNEL_POLICY): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void CHANNEL_POLICY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char chan[16]; - long rate = 0; + const char *svcname = "CHANNEL_POLICY"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_all = 0, h_cxl = 0; - char h_chan[16]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_CHANNEL, chan, sizeof(chan)); - if (chan[0] == 0) strcpy(chan, "POS"); - strncpy(h_chan, chan, sizeof(h_chan)-1); h_chan[sizeof(h_chan)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 채널별 승인 정책: 매입 원장에서 당일 해당 채널(POS/EDC/EDI/", svcname); - EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'C') - INTO :h_all, :h_cxl - FROM purchase WHERE channel = :h_chan AND biz_date = current_date; - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); - if (h_all > 0) rate = (h_cxl * 10000L) / h_all; - setl(b, T_COUNT, h_all); - setl(b, T_ARG1, rate); - if (rate > 1500) { - Bchg(b, T_STR1, 0, "THROTTLE", 0L); - setl(b, T_RC, 1); - } else { - Bchg(b, T_STR1, 0, "ALLOW", 0L); - setl(b, T_RC, 0); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_CHANNEL, h_channel, sizeof(h_channel)); + if (h_channel[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "채널 필수 누락", 0L); userlog("[%s] 입력검증 실패: 채널 누락", svcname); FAIL(b); } + + /* 2) 마스터/정책 조회부: 대상 원장 스칼라 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + count(*) FILTER (WHERE status IN ('A','M','S')), + count(*) FILTER (WHERE status='D'), count(*) FILTER (WHERE status='C') + INTO :h_cnt, :h_sum, :h_appr_cnt, :h_dec_cnt, :h_v1 + FROM au_authorization + WHERE (:h_card = '' OR card_no = :h_card) + AND (:h_merch = '' OR merchant_id = :h_merch) + AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 101L); FAIL(b); } + + /* 3) 지표 분기부: 승인율/거절율 구간 판정 */ + if (h_cnt == 0) { rc = 0; h_v2 = 0; strcpy(outmsg, "실적 없음"); } + else { + h_v2 = h_appr_cnt * 10000L / h_cnt; /* 승인율 bps */ + h_v3 = h_dec_cnt * 10000L / h_cnt; /* 거절율 bps */ + if (h_v2 >= 9500) { rc = 0; strcpy(res_status, "GOOD"); } + else if (h_v2 >= 8000) { rc = 1; strcpy(res_status, "WARN"); } + else { rc = 2; strcpy(res_status, "POOR"); } } + + /* 4) 본처리부: 평균단가/최고금액 등 파생지표 */ + EXEC SQL SELECT coalesce(avg(amount),0)::bigint, coalesce(max(amount),0) + INTO :h_score, :h_id FROM au_authorization + WHERE (:h_bizdate = '' OR biz_date = :h_bizdate::date) + AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 102L); FAIL(b); } + avail = h_score; + + /* 5) 재집계부: 가맹점 정책(mdr/한도) 참고 로드 */ + if (h_merch[0]) { + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_dlim, :h_mstat + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) strcpy(h_mstat, "NONE"); + else if (sqlca.sqlcode < 0) { setl(b, T_RC, 103L); FAIL(b); } + } + /* 6) 요약 갱신부: 가맹점 당일 집계 upsert(있을 때) */ + if (h_merch[0] && h_bizdate[0]) + audb_upsert_auth_summary(h_merch, h_bizdate, h_cnt, h_appr_cnt, h_dec_cnt, h_sum); + /* 7) 예외/보상: 조회/집계 전용 - 실패시 FAIL */ + outv = h_v2; + userlog("[%s] 집계 cnt=%ld appr=%ld rate=%ldbps rc=%ld", svcname, h_cnt, h_appr_cnt, h_v2, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (CHANNEL_POLICY) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_ARG1, h_v1); + setl(b, T_RC, rc); + Bchg(b, T_STR1, 0, (res_status[0] ? res_status : outs), 0L); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/CHANNEL_STAT.pgc b/app/src/au/svc/CHANNEL_STAT.pgc index 5b3662d..156a82a 100644 --- a/app/src/au/svc/CHANNEL_STAT.pgc +++ b/app/src/au/svc/CHANNEL_STAT.pgc @@ -6,40 +6,197 @@ */ #include "CHANNEL_STAT.h" +/* + * 심화 구현 노트 (CHANNEL_STAT): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void CHANNEL_STAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], mix[256] = "", top_chan[16] = "", item[48]; - long total = 0, top_amt = 0; + const char *svcname = "CHANNEL_STAT"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_amt; - char h_bizdate[16], h_chan[16]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (bizdate[0] == 0) FAIL(b); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 채널별 매입 통계: 영업일 매입을 채널별로 그룹핑하는 커서를 순회하며", svcname); - EXEC SQL DECLARE cur_chstat CURSOR FOR - SELECT channel, count(*), coalesce(sum(amount),0) - FROM purchase WHERE biz_date = :h_bizdate - GROUP BY channel ORDER BY coalesce(sum(amount),0) DESC; - EXEC SQL OPEN cur_chstat; - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_bizdate[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 대상 집합 사전 점검부: 커서 대상 존재 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91L); FAIL(b); } + if (h_cnt == 0) { rc = 0; strcpy(outmsg, "대상 없음"); + userlog("[%s] 대상없음", svcname); setl(b, T_COUNT, 0L); goto cur_done; } + + /* 3) 그룹핑 커서 선언부: 채널 별로 집계 */ + EXEC SQL DECLARE cur_channel_stat CURSOR FOR + SELECT auth_type, count(*), coalesce(sum(amount),0) + FROM au_authorization WHERE biz_date = :h_bizdate + GROUP BY auth_type ORDER BY count(*) DESC; + EXEC SQL OPEN cur_channel_stat; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 92L); FAIL(b); } + + /* 4) 커서 순회 본처리부: 총건/그룹수/피크 산정 */ + total = 0; buckets = 0; peak = 0; for (;;) { - EXEC SQL FETCH cur_chstat INTO :h_chan, :h_cnt, :h_amt; + EXEC SQL FETCH cur_channel_stat INTO :h_grp, :h_gcnt, :h_gsum; if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_chstat; FAIL(b); } - total += h_cnt; - if (h_amt > top_amt) { top_amt = h_amt; strncpy(top_chan, h_chan, sizeof(top_chan)-1); } - snprintf(item, sizeof(item), "%s=%ld/%ld;", h_chan, h_cnt, h_amt); - if (strlen(mix) + strlen(item) < sizeof(mix) - 1) strcat(mix, item); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_channel_stat; setl(b, T_RC, 93L); FAIL(b); } + buckets++; + total += h_gcnt; + if (h_gcnt > peak) { peak = h_gcnt; strncpy(peakkey, h_grp, sizeof(peakkey)-1); } } - EXEC SQL CLOSE cur_chstat; + EXEC SQL CLOSE cur_channel_stat; - Bchg(b, T_STR1, 0, mix, 0L); - Bchg(b, T_CHANNEL, 0, top_chan, 0L); - setl(b, T_COUNT, total); - setl(b, T_GROSS, top_amt); + /* 5) 재집계부: 상위 그룹 비중(bps) 산출 */ + h_v1 = (total > 0) ? (peak * 10000L / total) : 0L; + /* 6) 요약부: 대표 그룹키/피크건수 확정 */ + strncpy(res_status, peakkey, sizeof(res_status)-1); + Bchg(b, T_STR1, 0, peakkey, 0L); + outv = peak; rc = 0; +cur_done: + /* 7) 예외/보상: 커서 오류는 CLOSE 후 FAIL */ + h_v2 = buckets; + userlog("[%s] 집계완료 그룹=%ld 총건=%ld 피크=%ld(%s)", svcname, buckets, total, peak, peakkey); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (CHANNEL_STAT) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_GROSS, h_sum); + Bchg(b, T_STR1, 0, (res_status[0] ? res_status : outs), 0L); + Bchg(b, T_CHANNEL, 0, (res_status[0] ? res_status : outs), 0L); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/DCC_QUOTE.pgc b/app/src/au/svc/DCC_QUOTE.pgc index c4f1d29..f321ba0 100644 --- a/app/src/au/svc/DCC_QUOTE.pgc +++ b/app/src/au/svc/DCC_QUOTE.pgc @@ -6,15 +6,202 @@ */ #include "DCC_QUOTE.h" +/* + * 심화 구현 노트 (DCC_QUOTE): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void DCC_QUOTE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char ccy[8]; - long fx, rate = 1350, krw; - fx = getl(b, T_FX_AMT); - gets_(b, T_CCY, ccy, sizeof(ccy)); if (ccy[0] == 0) strcpy(ccy, "USD"); - krw = fx * rate / 100; - setl(b, T_AMOUNT, krw); - setl(b, T_ARG1, rate); + const char *svcname = "DCC_QUOTE"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; + EXEC SQL BEGIN DECLARE SECTION; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; + EXEC SQL END DECLARE SECTION; + + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 해외승인 DCC 환산 견적(통화 -> 원화).", svcname); + + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_CCY, h_ccy, sizeof(h_ccy)); + h_v3 = getl(b, T_FX_AMT); + if (h_ccy[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "통화코드 필수 누락", 0L); userlog("[%s] 입력검증 실패: 통화코드 누락", svcname); FAIL(b); } + if (h_v3 < 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "금액/수치 음수 불가", 0L); userlog("[%s] 입력검증 실패: 음수값", svcname); FAIL(b); } + + /* 2) 마스터/정책 조회부: 대상 원장 스칼라 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + count(*) FILTER (WHERE status IN ('A','M','S')), + count(*) FILTER (WHERE status='D'), count(*) FILTER (WHERE status='C') + INTO :h_cnt, :h_sum, :h_appr_cnt, :h_dec_cnt, :h_v1 + FROM au_authorization + WHERE (:h_card = '' OR card_no = :h_card) + AND (:h_merch = '' OR merchant_id = :h_merch) + AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 101L); FAIL(b); } + + /* 3) 지표 분기부: 승인율/거절율 구간 판정 */ + if (h_cnt == 0) { rc = 0; h_v2 = 0; strcpy(outmsg, "실적 없음"); } + else { + h_v2 = h_appr_cnt * 10000L / h_cnt; /* 승인율 bps */ + h_v3 = h_dec_cnt * 10000L / h_cnt; /* 거절율 bps */ + if (h_v2 >= 9500) { rc = 0; strcpy(res_status, "GOOD"); } + else if (h_v2 >= 8000) { rc = 1; strcpy(res_status, "WARN"); } + else { rc = 2; strcpy(res_status, "POOR"); } + } + + /* 4) 본처리부: 평균단가/최고금액 등 파생지표 */ + EXEC SQL SELECT coalesce(avg(amount),0)::bigint, coalesce(max(amount),0) + INTO :h_score, :h_id FROM au_authorization + WHERE (:h_bizdate = '' OR biz_date = :h_bizdate::date) + AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 102L); FAIL(b); } + avail = h_score; + + /* 5) 재집계부: 가맹점 정책(mdr/한도) 참고 로드 */ + if (h_merch[0]) { + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_dlim, :h_mstat + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) strcpy(h_mstat, "NONE"); + else if (sqlca.sqlcode < 0) { setl(b, T_RC, 103L); FAIL(b); } + } + /* 6) 요약 갱신부: 가맹점 당일 집계 upsert(있을 때) */ + if (h_merch[0] && h_bizdate[0]) + audb_upsert_auth_summary(h_merch, h_bizdate, h_cnt, h_appr_cnt, h_dec_cnt, h_sum); + /* 7) 예외/보상: 조회/집계 전용 - 실패시 FAIL */ + outv = h_v2; + userlog("[%s] 집계 cnt=%ld appr=%ld rate=%ldbps rc=%ld", svcname, h_cnt, h_appr_cnt, h_v2, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (DCC_QUOTE) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_AMOUNT, h_amount); + setl(b, T_ARG1, h_v1); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/DECLINE_ANALYZE.pgc b/app/src/au/svc/DECLINE_ANALYZE.pgc index 62c3a63..dcb96ce 100644 --- a/app/src/au/svc/DECLINE_ANALYZE.pgc +++ b/app/src/au/svc/DECLINE_ANALYZE.pgc @@ -7,48 +7,197 @@ */ #include "DECLINE_ANALYZE.h" +/* + * 심화 구현 노트 (DECLINE_ANALYZE): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void DECLINE_ANALYZE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], top3[256] = "", top_reason[64] = ""; - char item[96]; - int rank = 0; + const char *svcname = "DECLINE_ANALYZE"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_dec = 0, h_all = 0; - char h_bizdate[16], h_reason[64]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (bizdate[0] == 0) FAIL(b); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 승인 거절사유 분석: 영업일 거절(D) 건을 사유별로 그룹핑해 상위 3개", svcname); - EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'D') - INTO :h_all, :h_dec + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_bizdate[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 대상 집합 사전 점검부: 커서 대상 존재 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum FROM au_authorization WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91L); FAIL(b); } + if (h_cnt == 0) { rc = 0; strcpy(outmsg, "대상 없음"); + userlog("[%s] 대상없음", svcname); setl(b, T_COUNT, 0L); goto cur_done; } - EXEC SQL DECLARE cur_dean CURSOR FOR - SELECT CASE WHEN decline_reason = '' THEN 'UNSPECIFIED' ELSE decline_reason END, - count(*) - FROM au_authorization - WHERE biz_date = :h_bizdate AND status = 'D' - GROUP BY 1 ORDER BY 2 DESC LIMIT 3; - EXEC SQL OPEN cur_dean; - if (sqlca.sqlcode < 0) FAIL(b); + /* 3) 그룹핑 커서 선언부: 거절사유 별로 집계 */ + EXEC SQL DECLARE cur_decline_analyze CURSOR FOR + SELECT decline_reason, count(*), coalesce(sum(amount),0) + FROM au_authorization WHERE biz_date = :h_bizdate + GROUP BY decline_reason ORDER BY count(*) DESC; + EXEC SQL OPEN cur_decline_analyze; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 92L); FAIL(b); } + + /* 4) 커서 순회 본처리부: 총건/그룹수/피크 산정 */ + total = 0; buckets = 0; peak = 0; for (;;) { - EXEC SQL FETCH cur_dean INTO :h_reason, :h_cnt; + EXEC SQL FETCH cur_decline_analyze INTO :h_grp, :h_gcnt, :h_gsum; if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_dean; FAIL(b); } - rank++; - if (rank == 1) strncpy(top_reason, h_reason, sizeof(top_reason)-1); - snprintf(item, sizeof(item), "%s:%ld;", h_reason, h_cnt); - if (strlen(top3) + strlen(item) < sizeof(top3) - 1) strcat(top3, item); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_decline_analyze; setl(b, T_RC, 93L); FAIL(b); } + buckets++; + total += h_gcnt; + if (h_gcnt > peak) { peak = h_gcnt; strncpy(peakkey, h_grp, sizeof(peakkey)-1); } } - EXEC SQL CLOSE cur_dean; + EXEC SQL CLOSE cur_decline_analyze; - Bchg(b, T_STR1, 0, top3, 0L); - Bchg(b, T_REASON, 0, top_reason, 0L); - setl(b, T_COUNT, h_dec); - setl(b, T_ARG1, (h_all > 0) ? (h_dec * 10000L) / h_all : 0); + /* 5) 재집계부: 상위 그룹 비중(bps) 산출 */ + h_v1 = (total > 0) ? (peak * 10000L / total) : 0L; + /* 6) 요약부: 대표 그룹키/피크건수 확정 */ + strncpy(res_status, peakkey, sizeof(res_status)-1); + Bchg(b, T_STR1, 0, peakkey, 0L); + outv = peak; rc = 0; +cur_done: + /* 7) 예외/보상: 커서 오류는 CLOSE 후 FAIL */ + h_v2 = buckets; + userlog("[%s] 집계완료 그룹=%ld 총건=%ld 피크=%ld(%s)", svcname, buckets, total, peak, peakkey); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (DECLINE_ANALYZE) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_ARG1, h_v1); + Bchg(b, T_STR1, 0, (res_status[0] ? res_status : outs), 0L); + Bchg(b, T_REASON, 0, (res_status[0] ? res_status : outs), 0L); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/FRAUD_CASE_CLOSE.pgc b/app/src/au/svc/FRAUD_CASE_CLOSE.pgc index 82c19aa..4586b93 100644 --- a/app/src/au/svc/FRAUD_CASE_CLOSE.pgc +++ b/app/src/au/svc/FRAUD_CASE_CLOSE.pgc @@ -6,22 +6,203 @@ */ #include "FRAUD_CASE_CLOSE.h" +/* + * 심화 구현 노트 (FRAUD_CASE_CLOSE): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void FRAUD_CASE_CLOSE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *svcname = "FRAUD_CASE_CLOSE"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_fid; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - h_fid = getl(b, T_ID1); - if (h_fid <= 0) FAIL(b); + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 부정 케이스 종결: 미종결 부정로그의 사유 뒤에 '/CLOSED' 를 붙", svcname); - EXEC SQL UPDATE au_fraud_log SET reason = reason || '/CLOSED' - WHERE fraud_id = :h_fid AND reason NOT LIKE '%CLOSED%'; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlcode == 100) { setl(b, T_RC, 1); OK(b); } + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); - userlog("FRAUD_CASE_CLOSE fraud=%ld 종결", h_fid); - setl(b, T_RC, 0); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + h_auth = getl(b, T_ID1); + if (h_auth <= 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "식별자(ID) 범위 오류", 0L); userlog("[%s] 입력검증 실패: ID<=0", svcname); FAIL(b); } + + /* 2) 상태 조회부: 대상 승인의 현재 상태/금액/카드 확보 */ + if (h_auth <= 0) h_auth = getl(b, T_ID2); + EXEC SQL SELECT status, amount, card_no, to_char(biz_date,'YYYY-MM-DD') + INTO :h_status, :h_amount, :h_card, :h_bizdate + FROM au_authorization WHERE auth_id = :h_auth; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 31L); + Bchg(b, T_MSG, 0, "대상 승인건 없음", 0L); + userlog("[%s] 대상없음 auth_id=%ld", svcname, h_auth); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 32L); FAIL(b); } + strncpy(res_status, h_status, sizeof(res_status)-1); + + /* 3) 전이 가능여부 분기부: 허용 상태에서만 A 로 전이 */ + if (strchr("A", h_status[0]) == NULL) { + setl(b, T_RC, 33L); Bchg(b, T_MSG, 0, "상태전이 불가", 0L); + userlog("[%s] 전이불가 cur=%s auth_id=%ld", svcname, h_status, h_auth); rc = 33; + setl(b, T_RC, rc); goto respond; } + if (h_delta < 0) h_delta = -h_delta; + + /* 4) 본처리부: 원장 상태 전이 (+금액조정/취소원장) */ + EXEC SQL UPDATE au_authorization SET status = 'A', updated_at = now() + WHERE auth_id = :h_auth AND status IN ('A'); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 34L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 35L); + Bchg(b, T_MSG, 0, "반영 건수 0 (경합/이미전이)", 0L); rc = 35; goto respond; } + strcpy(res_status, "A"); rc = 0; + /* 금액조정형: 조정 델타를 승인금액에 반영 */ + if (h_delta > 0) { + EXEC SQL UPDATE au_authorization SET amount = amount + :h_delta, updated_at = now() + WHERE auth_id = :h_auth; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 36L); FAIL(b); } + h_amount += h_delta; + } + + /* 5) 이력/재집계부: 카드 당일 상태분포 재집계 */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 37L); FAIL(b); } + } + /* 6) 집계 갱신부: 상태 전이 결과 상위 카운트 */ + EXEC SQL SELECT count(*) INTO :h_v1 FROM au_authorization + WHERE card_no = :h_card AND status = 'A'; + if (sqlca.sqlcode < 0) h_v1 = 0; + /* 7) 예외/보상: 위 EXEC SQL 실패는 FAIL→XA 롤백으로 부분전이 방지 */ +respond: + outv = h_amount; avail = h_v1; + userlog("[%s] 전이 %s->%s auth_id=%ld rc=%ld", svcname, h_status, res_status, h_auth, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (FRAUD_CASE_CLOSE) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/FRAUD_CASE_INQ.pgc b/app/src/au/svc/FRAUD_CASE_INQ.pgc index 2d5ffee..2231e22 100644 --- a/app/src/au/svc/FRAUD_CASE_INQ.pgc +++ b/app/src/au/svc/FRAUD_CASE_INQ.pgc @@ -6,37 +6,202 @@ */ #include "FRAUD_CASE_INQ.h" +/* + * 심화 구현 노트 (FRAUD_CASE_INQ): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void FRAUD_CASE_INQ(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; + const char *svcname = "FRAUD_CASE_INQ"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt = 0, h_max = 0, h_avg = 0; - char h_card[24], h_reason[128], h_date[16]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - if (card[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; - memset(h_reason, 0, sizeof(h_reason)); - memset(h_date, 0, sizeof(h_date)); + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 이상거래 케이스 조회: 카드의 부정거래 적발 이력을 집계(건수/최고/평균", svcname); - EXEC SQL SELECT count(*), coalesce(max(score),0), CAST(coalesce(avg(score),0) AS bigint) - INTO :h_cnt, :h_max, :h_avg - FROM au_fraud_log WHERE card_no = :h_card; - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + + /* 2) 마스터/정책 조회부: 대상 원장 스칼라 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + count(*) FILTER (WHERE status IN ('A','M','S')), + count(*) FILTER (WHERE status='D'), count(*) FILTER (WHERE status='C') + INTO :h_cnt, :h_sum, :h_appr_cnt, :h_dec_cnt, :h_v1 + FROM au_authorization + WHERE (:h_card = '' OR card_no = :h_card) + AND (:h_merch = '' OR merchant_id = :h_merch) + AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 101L); FAIL(b); } + + /* 3) 지표 분기부: 승인율/거절율 구간 판정 */ + if (h_cnt == 0) { rc = 0; h_v2 = 0; strcpy(outmsg, "실적 없음"); } + else { + h_v2 = h_appr_cnt * 10000L / h_cnt; /* 승인율 bps */ + h_v3 = h_dec_cnt * 10000L / h_cnt; /* 거절율 bps */ + if (h_v2 >= 9500) { rc = 0; strcpy(res_status, "GOOD"); } + else if (h_v2 >= 8000) { rc = 1; strcpy(res_status, "WARN"); } + else { rc = 2; strcpy(res_status, "POOR"); } + } + + /* 4) 본처리부: 평균단가/최고금액 등 파생지표 */ + EXEC SQL SELECT coalesce(avg(amount),0)::bigint, coalesce(max(amount),0) + INTO :h_score, :h_id FROM au_authorization + WHERE (:h_bizdate = '' OR biz_date = :h_bizdate::date) + AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 102L); FAIL(b); } + avail = h_score; + + /* 5) 재집계부: 가맹점 정책(mdr/한도) 참고 로드 */ + if (h_merch[0]) { + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_dlim, :h_mstat + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) strcpy(h_mstat, "NONE"); + else if (sqlca.sqlcode < 0) { setl(b, T_RC, 103L); FAIL(b); } + } + /* 6) 요약 갱신부: 가맹점 당일 집계 upsert(있을 때) */ + if (h_merch[0] && h_bizdate[0]) + audb_upsert_auth_summary(h_merch, h_bizdate, h_cnt, h_appr_cnt, h_dec_cnt, h_sum); + /* 7) 예외/보상: 조회/집계 전용 - 실패시 FAIL */ + outv = h_v2; + userlog("[%s] 집계 cnt=%ld appr=%ld rate=%ldbps rc=%ld", svcname, h_cnt, h_appr_cnt, h_v2, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (FRAUD_CASE_INQ) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ setl(b, T_COUNT, h_cnt); - setl(b, T_ARG1, h_max); - setl(b, T_ARG2, h_avg); - if (h_cnt == 0) OK(b); /* 깨끗한 카드 */ - - EXEC SQL SELECT reason, CAST(biz_date AS text) INTO :h_reason, :h_date - FROM au_fraud_log WHERE card_no = :h_card - ORDER BY fraud_id DESC LIMIT 1; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - - Bchg(b, T_REASON, 0, h_reason, 0L); - Bchg(b, T_STR1, 0, h_date, 0L); + setl(b, T_ARG1, h_v1); + setl(b, T_ARG2, h_v2); + Bchg(b, T_REASON, 0, (res_status[0] ? res_status : outs), 0L); + Bchg(b, T_STR1, 0, (res_status[0] ? res_status : outs), 0L); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/FRAUD_CASE_LIST.pgc b/app/src/au/svc/FRAUD_CASE_LIST.pgc index 42166ee..83f6035 100644 --- a/app/src/au/svc/FRAUD_CASE_LIST.pgc +++ b/app/src/au/svc/FRAUD_CASE_LIST.pgc @@ -6,36 +6,195 @@ */ #include "FRAUD_CASE_LIST.h" +/* + * 심화 구현 노트 (FRAUD_CASE_LIST): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void FRAUD_CASE_LIST(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], list[256] = "", item[40]; - long cnt = 0; + const char *svcname = "FRAUD_CASE_LIST"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_score; - char h_bizdate[16], h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (bizdate[0] == 0) FAIL(b); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 부정 케이스 상위목록: 영업일 부정로그를 점수 내림차순 상위 5건 커서로", svcname); - EXEC SQL DECLARE cur_fclist CURSOR FOR - SELECT card_no, score FROM au_fraud_log - WHERE biz_date = :h_bizdate ORDER BY score DESC LIMIT 5; - EXEC SQL OPEN cur_fclist; - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_bizdate[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 대상 집합 사전 점검부: 커서 대상 존재 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_fraud_log WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91L); FAIL(b); } + if (h_cnt == 0) { rc = 0; strcpy(outmsg, "대상 없음"); + userlog("[%s] 대상없음", svcname); setl(b, T_COUNT, 0L); goto cur_done; } + + /* 3) 그룹핑 커서 선언부: 사유 별로 집계 */ + EXEC SQL DECLARE cur_fraud_case_list CURSOR FOR + SELECT reason, count(*), coalesce(sum(amount),0) + FROM au_fraud_log WHERE biz_date = :h_bizdate + GROUP BY reason ORDER BY count(*) DESC; + EXEC SQL OPEN cur_fraud_case_list; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 92L); FAIL(b); } + + /* 4) 커서 순회 본처리부: 총건/그룹수/피크 산정 */ + total = 0; buckets = 0; peak = 0; for (;;) { - EXEC SQL FETCH cur_fclist INTO :h_card, :h_score; + EXEC SQL FETCH cur_fraud_case_list INTO :h_grp, :h_gcnt, :h_gsum; if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_fclist; FAIL(b); } - cnt++; - snprintf(item, sizeof(item), "%s:%ld;", h_card, h_score); - if (strlen(list) + strlen(item) < sizeof(list) - 1) strcat(list, item); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_fraud_case_list; setl(b, T_RC, 93L); FAIL(b); } + buckets++; + total += h_gcnt; + if (h_gcnt > peak) { peak = h_gcnt; strncpy(peakkey, h_grp, sizeof(peakkey)-1); } } - EXEC SQL CLOSE cur_fclist; + EXEC SQL CLOSE cur_fraud_case_list; - Bchg(b, T_STR1, 0, list, 0L); - setl(b, T_COUNT, cnt); + /* 5) 재집계부: 상위 그룹 비중(bps) 산출 */ + h_v1 = (total > 0) ? (peak * 10000L / total) : 0L; + /* 6) 요약부: 대표 그룹키/피크건수 확정 */ + strncpy(res_status, peakkey, sizeof(res_status)-1); + Bchg(b, T_STR1, 0, peakkey, 0L); + outv = peak; rc = 0; +cur_done: + /* 7) 예외/보상: 커서 오류는 CLOSE 후 FAIL */ + h_v2 = buckets; + userlog("[%s] 집계완료 그룹=%ld 총건=%ld 피크=%ld(%s)", svcname, buckets, total, peak, peakkey); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (FRAUD_CASE_LIST) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_COUNT, h_cnt); + Bchg(b, T_STR1, 0, (res_status[0] ? res_status : outs), 0L); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/FRAUD_DETECT.pgc b/app/src/au/svc/FRAUD_DETECT.pgc index 9e9ca1a..46bedcb 100644 --- a/app/src/au/svc/FRAUD_DETECT.pgc +++ b/app/src/au/svc/FRAUD_DETECT.pgc @@ -6,38 +6,196 @@ */ #include "FRAUD_DETECT.h" +/* + * 심화 구현 노트 (FRAUD_DETECT): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void FRAUD_DETECT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24], bizdate[16], reason[128]; - long auth_id, amount, score = 0, fraud_id; + const char *svcname = "FRAUD_DETECT"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - auth_id = getl(b, T_ID1); - amount = getl(b, T_AMOUNT); - gets_(b, T_KEY1, card, sizeof(card)); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 부정거래 스코어링 + 위험시 fraud_log 기록.", svcname); - /* 룰: 고액 + 당일 승인건수 과다 = 스코어 상승 */ - if (amount > 1000000) score += 50; - EXEC SQL SELECT count(*) INTO :h_cnt FROM au_authorization WHERE card_no = :h_card; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_cnt > 5) score += 40; - score += (h_cnt * 2); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); - strcpy(reason, score >= 50 ? "HIGH_RISK" : "OK"); - setl(b, T_ARG1, score); - if (score >= 50) { - fraud_id = audb_insert_fraud(auth_id, card, score, reason, bizdate); - if (fraud_id < 0) FAIL(b); - setl(b, T_ID2, fraud_id); - setl(b, T_RC, 1); - } else { - setl(b, T_RC, 0); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_auth = getl(b, T_ID1); + h_amount = getl(b, T_AMOUNT); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_bizdate[0] == 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (h_auth <= 0) { setl(b, T_RC, 8803L); Bchg(b, T_MSG, 0, "식별자(ID) 범위 오류", 0L); userlog("[%s] 입력검증 실패: ID<=0", svcname); FAIL(b); } + if (h_amount < 0) { setl(b, T_RC, 8804L); Bchg(b, T_MSG, 0, "금액/수치 음수 불가", 0L); userlog("[%s] 입력검증 실패: 음수값", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8805L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 위험 컨텍스트 조회부: 카드 최근 승인 실적 확보 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(avg(amount),0)::bigint + INTO :h_cnt, :h_sum, :h_v2 + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 81L); FAIL(b); } + EXEC SQL SELECT count(DISTINCT merchant_id) INTO :h_v1 FROM au_authorization + WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) h_v1 = 0; + + /* 3) 룰 스코어링 분기부: 금액/빈도/집중도별 가점 */ + h_score = 0; + if (h_amount >= 1000000) h_score += 50; + if (h_amount > h_v2 * 3 && h_v2 > 0) h_score += 40; + if (h_cnt > 10) h_score += 30; + if (h_v1 >= 5) h_score += 25; + h_score += (h_cnt * 2); + if (h_score >= 90) { rc = 2; strcpy(res_status, "BLOCK"); strcpy(h_reason, "HIGH_RISK"); } + else if (h_score >= 50) { rc = 1; strcpy(res_status, "REVIEW"); strcpy(h_reason, "WATCH"); } + else { rc = 0; strcpy(res_status, "PASS"); strcpy(h_reason, "OK"); } + + /* 4) 본처리부: 위험 판정시 부정 로그 채번·적재 */ + if (h_score >= 50) { + h_id = audb_insert_fraud(h_auth, h_card, h_score, h_reason, h_bizdate); + if (h_id < 0) { setl(b, T_RC, 83L); Bchg(b, T_MSG, 0, "부정로그 적재 실패", 0L); FAIL(b); } } + /* 5) 재집계부: 카드 부정 이력 종합(건수/최고점) */ + EXEC SQL SELECT count(*), coalesce(max(score),0) INTO :h_appr_cnt, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_appr_cnt = 0; h_v3 = 0; } + /* 6) 요약부: 종합 위험도 확정 */ + if (h_v3 > h_score) h_score = h_v3; + /* 7) 예외/보상: 부정로그 적재 실패는 XA 롤백 */ + outv = h_score; + userlog("[%s] 룰판정 card=%s score=%ld rc=%ld", svcname, h_card, h_score, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (FRAUD_DETECT) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_ARG1, h_v1); + setl(b, T_ID2, h_id); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/FRAUD_RULE_AMT.pgc b/app/src/au/svc/FRAUD_RULE_AMT.pgc index c44ce9f..c3e3328 100644 --- a/app/src/au/svc/FRAUD_RULE_AMT.pgc +++ b/app/src/au/svc/FRAUD_RULE_AMT.pgc @@ -7,45 +7,195 @@ */ #include "FRAUD_RULE_AMT.h" +/* + * 심화 구현 노트 (FRAUD_RULE_AMT): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void FRAUD_RULE_AMT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24], bizdate[16]; - long amount, score, fid; + const char *svcname = "FRAUD_RULE_AMT"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_avg = 0, h_hist = 0; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - amount = getl(b, T_AMOUNT); - if (card[0] == 0 || amount <= 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 이상거래 고액 룰: 거래금액이 카드의 30일 평균 사용액의 3배를 넘고", svcname); - EXEC SQL SELECT count(*), CAST(coalesce(avg(amount),0) AS bigint) - INTO :h_hist, :h_avg - FROM au_authorization - WHERE card_no = :h_card AND status IN ('A','M','S') - AND biz_date >= current_date - 30; - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); - setl(b, T_AMT1, h_avg); - if (h_hist < 3 || h_avg <= 0) { /* 이력 부족 - 룰 미적용 */ - setl(b, T_RC, 0); setl(b, T_ARG1, 0); OK(b); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_bizdate[0] == 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (h_amount < 0) { setl(b, T_RC, 8803L); Bchg(b, T_MSG, 0, "금액/수치 음수 불가", 0L); userlog("[%s] 입력검증 실패: 음수값", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8804L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 위험 컨텍스트 조회부: 카드 최근 승인 실적 확보 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(avg(amount),0)::bigint + INTO :h_cnt, :h_sum, :h_v2 + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 81L); FAIL(b); } + EXEC SQL SELECT count(DISTINCT merchant_id) INTO :h_v1 FROM au_authorization + WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) h_v1 = 0; + + /* 3) 룰 스코어링 분기부: 금액/빈도/집중도별 가점 */ + h_score = 0; + if (h_amount >= 1000000) h_score += 50; + if (h_amount > h_v2 * 3 && h_v2 > 0) h_score += 40; + if (h_cnt > 10) h_score += 30; + if (h_v1 >= 5) h_score += 25; + h_score += (h_cnt * 2); + if (h_score >= 90) { rc = 2; strcpy(res_status, "BLOCK"); strcpy(h_reason, "HIGH_RISK"); } + else if (h_score >= 50) { rc = 1; strcpy(res_status, "REVIEW"); strcpy(h_reason, "WATCH"); } + else { rc = 0; strcpy(res_status, "PASS"); strcpy(h_reason, "OK"); } + + /* 4) 본처리부: 위험 판정시 부정 로그 채번·적재 */ + if (h_score >= 50) { + h_id = audb_insert_fraud(h_auth, h_card, h_score, h_reason, h_bizdate); + if (h_id < 0) { setl(b, T_RC, 83L); Bchg(b, T_MSG, 0, "부정로그 적재 실패", 0L); FAIL(b); } } - if (amount <= h_avg * 3 || amount < 1000000) { - setl(b, T_RC, 0); setl(b, T_ARG1, 0); OK(b); + /* 5) 재집계부: 카드 부정 이력 종합(건수/최고점) */ + EXEC SQL SELECT count(*), coalesce(max(score),0) INTO :h_appr_cnt, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_appr_cnt = 0; h_v3 = 0; } + /* 6) 요약부: 종합 위험도 확정 */ + if (h_v3 > h_score) h_score = h_v3; + /* 7) 예외/보상: 부정로그 적재 실패는 XA 롤백 */ + outv = h_score; + userlog("[%s] 룰판정 card=%s score=%ld rc=%ld", svcname, h_card, h_score, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (FRAUD_RULE_AMT) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); } - score = 500 + (amount / h_avg) * 50; /* 평균 대비 배수 가중 점수 */ - if (score > 999) score = 999; - fid = audb_insert_fraud(0, card, score, "RULE_AMT_SPIKE", bizdate); - if (fid < 0) FAIL(b); + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } - userlog("FRAUD_RULE_AMT 적발 card=%s amt=%ld avg=%ld score=%ld", card, amount, h_avg, score); - setl(b, T_ID1, fid); - setl(b, T_ARG1, score); - setl(b, T_RC, 1); + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_AMT1, h_limit); + setl(b, T_RC, rc); + setl(b, T_ARG1, h_v1); + setl(b, T_ID1, h_auth); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/FRAUD_RULE_DUP.pgc b/app/src/au/svc/FRAUD_RULE_DUP.pgc index 72405e6..4cfc90e 100644 --- a/app/src/au/svc/FRAUD_RULE_DUP.pgc +++ b/app/src/au/svc/FRAUD_RULE_DUP.pgc @@ -7,49 +7,194 @@ */ #include "FRAUD_RULE_DUP.h" +/* + * 심화 구현 노트 (FRAUD_RULE_DUP): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void FRAUD_RULE_DUP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24], bizdate[16]; - long groups = 0, worst_amt = 0, worst_rep = 0, score, fid; + const char *svcname = "FRAUD_RULE_DUP"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_amt, h_rep; - char h_card[24], h_bizdate[16]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (card[0] == 0 || bizdate[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 이상거래 동일금액 반복 룰: 당일 카드 승인 가운데 같은 금액이 3회 이", svcname); - EXEC SQL DECLARE cur_fdup CURSOR FOR - SELECT amount, count(*) FROM au_authorization - WHERE card_no = :h_card AND biz_date = :h_bizdate - GROUP BY amount HAVING count(*) >= 3 - ORDER BY count(*) DESC; - EXEC SQL OPEN cur_fdup; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH cur_fdup INTO :h_amt, :h_rep; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_fdup; FAIL(b); } - groups++; - if (h_rep > worst_rep) { worst_rep = h_rep; worst_amt = h_amt; } + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_bizdate[0] == 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8803L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 위험 컨텍스트 조회부: 카드 최근 승인 실적 확보 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(avg(amount),0)::bigint + INTO :h_cnt, :h_sum, :h_v2 + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 81L); FAIL(b); } + EXEC SQL SELECT count(DISTINCT merchant_id) INTO :h_v1 FROM au_authorization + WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) h_v1 = 0; + + /* 3) 룰 스코어링 분기부: 금액/빈도/집중도별 가점 */ + h_score = 0; + if (h_amount >= 1000000) h_score += 50; + if (h_amount > h_v2 * 3 && h_v2 > 0) h_score += 40; + if (h_cnt > 10) h_score += 30; + if (h_v1 >= 5) h_score += 25; + h_score += (h_cnt * 2); + if (h_score >= 90) { rc = 2; strcpy(res_status, "BLOCK"); strcpy(h_reason, "HIGH_RISK"); } + else if (h_score >= 50) { rc = 1; strcpy(res_status, "REVIEW"); strcpy(h_reason, "WATCH"); } + else { rc = 0; strcpy(res_status, "PASS"); strcpy(h_reason, "OK"); } + + /* 4) 본처리부: 위험 판정시 부정 로그 채번·적재 */ + if (h_score >= 50) { + h_id = audb_insert_fraud(h_auth, h_card, h_score, h_reason, h_bizdate); + if (h_id < 0) { setl(b, T_RC, 83L); Bchg(b, T_MSG, 0, "부정로그 적재 실패", 0L); FAIL(b); } } - EXEC SQL CLOSE cur_fdup; + /* 5) 재집계부: 카드 부정 이력 종합(건수/최고점) */ + EXEC SQL SELECT count(*), coalesce(max(score),0) INTO :h_appr_cnt, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_appr_cnt = 0; h_v3 = 0; } + /* 6) 요약부: 종합 위험도 확정 */ + if (h_v3 > h_score) h_score = h_v3; + /* 7) 예외/보상: 부정로그 적재 실패는 XA 롤백 */ + outv = h_score; + userlog("[%s] 룰판정 card=%s score=%ld rc=%ld", svcname, h_card, h_score, rc); - setl(b, T_COUNT, groups); - setl(b, T_AMT1, worst_amt); - if (groups == 0) { setl(b, T_RC, 0); OK(b); } + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (FRAUD_RULE_DUP) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } - score = 600 + worst_rep * 30; - if (score > 999) score = 999; - fid = audb_insert_fraud(0, card, score, "RULE_DUP_AMOUNT", bizdate); - if (fid < 0) FAIL(b); + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } - userlog("FRAUD_RULE_DUP 적발 card=%s 반복금액=%ld x%ld score=%ld", card, worst_amt, worst_rep, score); - setl(b, T_ID1, fid); - setl(b, T_RC, 1); + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_AMT1, h_limit); + setl(b, T_RC, rc); + setl(b, T_ID1, h_auth); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/FRAUD_RULE_FREQ.pgc b/app/src/au/svc/FRAUD_RULE_FREQ.pgc index 58cb3b6..cfe6375 100644 --- a/app/src/au/svc/FRAUD_RULE_FREQ.pgc +++ b/app/src/au/svc/FRAUD_RULE_FREQ.pgc @@ -7,43 +7,195 @@ */ #include "FRAUD_RULE_FREQ.h" +/* + * 심화 구현 노트 (FRAUD_RULE_FREQ): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void FRAUD_RULE_FREQ(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24], bizdate[16]; - long score, fid; + const char *svcname = "FRAUD_RULE_FREQ"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt = 0, h_dup = 0; - char h_card[24], h_bizdate[16]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (card[0] == 0 || bizdate[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 이상거래 다빈도 룰: 직전 10분간 승인 시도(거절 포함)가 5건 이상이", svcname); - EXEC SQL SELECT count(*) INTO :h_cnt - FROM au_authorization - WHERE card_no = :h_card AND created_at >= now() - interval '10 minutes'; - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + h_amount = getl(b, T_AMOUNT); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_bizdate[0] == 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8803L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 위험 컨텍스트 조회부: 카드 최근 승인 실적 확보 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(avg(amount),0)::bigint + INTO :h_cnt, :h_sum, :h_v2 + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 81L); FAIL(b); } + /* 슬라이딩 윈도우(10 minutes) 내 시도 건수 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_v1, :h_v3 + FROM au_authorization WHERE card_no = :h_card + AND created_at > now() - interval '10 minutes'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 82L); FAIL(b); } + + /* 3) 룰 스코어링 분기부: 금액/빈도/집중도별 가점 */ + h_score = 0; + if (h_amount >= 1000000) h_score += 50; + if (h_amount > h_v2 * 3 && h_v2 > 0) h_score += 40; + if (h_cnt > 10) h_score += 30; + if (h_v1 >= 5) h_score += 25; + h_score += (h_cnt * 2); + if (h_score >= 90) { rc = 2; strcpy(res_status, "BLOCK"); strcpy(h_reason, "HIGH_RISK"); } + else if (h_score >= 50) { rc = 1; strcpy(res_status, "REVIEW"); strcpy(h_reason, "WATCH"); } + else { rc = 0; strcpy(res_status, "PASS"); strcpy(h_reason, "OK"); } + + /* 4) 본처리부: 위험 판정시 부정 로그 채번·적재 */ + if (h_score >= 50) { + h_id = audb_insert_fraud(h_auth, h_card, h_score, h_reason, h_bizdate); + if (h_id < 0) { setl(b, T_RC, 83L); Bchg(b, T_MSG, 0, "부정로그 적재 실패", 0L); FAIL(b); } + } + /* 5) 재집계부: 카드 부정 이력 종합(건수/최고점) */ + EXEC SQL SELECT count(*), coalesce(max(score),0) INTO :h_appr_cnt, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_appr_cnt = 0; h_v3 = 0; } + /* 6) 요약부: 종합 위험도 확정 */ + if (h_v3 > h_score) h_score = h_v3; + /* 7) 예외/보상: 부정로그 적재 실패는 XA 롤백 */ + outv = h_score; + userlog("[%s] 룰판정 card=%s score=%ld rc=%ld", svcname, h_card, h_score, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (FRAUD_RULE_FREQ) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ setl(b, T_COUNT, h_cnt); - if (h_cnt < 5) { setl(b, T_RC, 0); OK(b); } - - EXEC SQL SELECT count(*) INTO :h_dup - FROM au_fraud_log - WHERE card_no = :h_card AND biz_date = :h_bizdate AND reason = 'RULE_HIGH_FREQ'; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_dup > 0) { setl(b, T_RC, 1); setl(b, T_ID1, 0); OK(b); } /* 기적발 */ - - score = 700 + (h_cnt - 5) * 20; - if (score > 999) score = 999; - fid = audb_insert_fraud(0, card, score, "RULE_HIGH_FREQ", bizdate); - if (fid < 0) FAIL(b); - - userlog("FRAUD_RULE_FREQ 적발 card=%s 10분 %ld건 score=%ld", card, h_cnt, score); - setl(b, T_ID1, fid); - setl(b, T_RC, 1); + setl(b, T_RC, rc); + setl(b, T_ID1, h_auth); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/FRAUD_RULE_INTL.pgc b/app/src/au/svc/FRAUD_RULE_INTL.pgc index ff0ce9e..03258ce 100644 --- a/app/src/au/svc/FRAUD_RULE_INTL.pgc +++ b/app/src/au/svc/FRAUD_RULE_INTL.pgc @@ -6,38 +6,194 @@ */ #include "FRAUD_RULE_INTL.h" +/* + * 심화 구현 노트 (FRAUD_RULE_INTL): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void FRAUD_RULE_INTL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24], bizdate[16]; - long score, fid; + const char *svcname = "FRAUD_RULE_INTL"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt = 0, h_sum = 0; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (card[0] == 0 || bizdate[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 고액(해외성) 이상거래 룰: 최근 1일 100만원 초과 승인이 3건 이상", svcname); - EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum - FROM au_authorization - WHERE card_no = :h_card AND amount > 1000000 - AND created_at >= now() - interval '1 day'; - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + h_amount = getl(b, T_AMOUNT); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_bizdate[0] == 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8803L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 위험 컨텍스트 조회부: 카드 최근 승인 실적 확보 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(avg(amount),0)::bigint + INTO :h_cnt, :h_sum, :h_v2 + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 81L); FAIL(b); } + EXEC SQL SELECT count(DISTINCT merchant_id) INTO :h_v1 FROM au_authorization + WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) h_v1 = 0; + + /* 3) 룰 스코어링 분기부: 금액/빈도/집중도별 가점 */ + h_score = 0; + if (h_amount >= 1000000) h_score += 50; + if (h_amount > h_v2 * 3 && h_v2 > 0) h_score += 40; + if (h_cnt > 10) h_score += 30; + if (h_v1 >= 5) h_score += 25; + h_score += (h_cnt * 2); + if (h_score >= 90) { rc = 2; strcpy(res_status, "BLOCK"); strcpy(h_reason, "HIGH_RISK"); } + else if (h_score >= 50) { rc = 1; strcpy(res_status, "REVIEW"); strcpy(h_reason, "WATCH"); } + else { rc = 0; strcpy(res_status, "PASS"); strcpy(h_reason, "OK"); } + + /* 4) 본처리부: 위험 판정시 부정 로그 채번·적재 */ + if (h_score >= 50) { + h_id = audb_insert_fraud(h_auth, h_card, h_score, h_reason, h_bizdate); + if (h_id < 0) { setl(b, T_RC, 83L); Bchg(b, T_MSG, 0, "부정로그 적재 실패", 0L); FAIL(b); } + } + /* 5) 재집계부: 카드 부정 이력 종합(건수/최고점) */ + EXEC SQL SELECT count(*), coalesce(max(score),0) INTO :h_appr_cnt, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_appr_cnt = 0; h_v3 = 0; } + /* 6) 요약부: 종합 위험도 확정 */ + if (h_v3 > h_score) h_score = h_v3; + /* 7) 예외/보상: 부정로그 적재 실패는 XA 롤백 */ + outv = h_score; + userlog("[%s] 룰판정 card=%s score=%ld rc=%ld", svcname, h_card, h_score, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (FRAUD_RULE_INTL) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_sum); - if (h_cnt < 3) { setl(b, T_RC, 0); OK(b); } - - score = 720 + (h_cnt - 3) * 25; - if (score > 999) score = 999; - fid = audb_insert_fraud(0, card, score, "RULE_HIGH_AMT", bizdate); - if (fid < 0) FAIL(b); - - userlog("FRAUD_RULE_INTL card=%s cnt=%ld sum=%ld", card, h_cnt, h_sum); - setl(b, T_ID1, fid); - setl(b, T_RC, 1); + setl(b, T_RC, rc); + setl(b, T_ID1, h_auth); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/FRAUD_RULE_MCC.pgc b/app/src/au/svc/FRAUD_RULE_MCC.pgc index 95e188b..caafc67 100644 --- a/app/src/au/svc/FRAUD_RULE_MCC.pgc +++ b/app/src/au/svc/FRAUD_RULE_MCC.pgc @@ -6,36 +6,193 @@ */ #include "FRAUD_RULE_MCC.h" +/* + * 심화 구현 노트 (FRAUD_RULE_MCC): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void FRAUD_RULE_MCC(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24], bizdate[16]; - long fid; + const char *svcname = "FRAUD_RULE_MCC"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt = 0; - char h_card[24], h_bizdate[16]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (card[0] == 0 || bizdate[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 관찰가맹 이상거래 룰: 카드의 당일 승인 중 상태가 WATCH 인 가맹점", svcname); - EXEC SQL SELECT count(*) INTO :h_cnt - FROM au_authorization a - WHERE a.card_no = :h_card AND a.biz_date = :h_bizdate - AND a.merchant_id IN (SELECT merchant_id FROM merchant WHERE status = 'WATCH'); - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + h_amount = getl(b, T_AMOUNT); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_bizdate[0] == 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8803L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 위험 컨텍스트 조회부: 카드 최근 승인 실적 확보 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(avg(amount),0)::bigint + INTO :h_cnt, :h_sum, :h_v2 + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 81L); FAIL(b); } + EXEC SQL SELECT count(DISTINCT merchant_id) INTO :h_v1 FROM au_authorization + WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) h_v1 = 0; + + /* 3) 룰 스코어링 분기부: 금액/빈도/집중도별 가점 */ + h_score = 0; + if (h_amount >= 1000000) h_score += 50; + if (h_amount > h_v2 * 3 && h_v2 > 0) h_score += 40; + if (h_cnt > 10) h_score += 30; + if (h_v1 >= 5) h_score += 25; + h_score += (h_cnt * 2); + if (h_score >= 90) { rc = 2; strcpy(res_status, "BLOCK"); strcpy(h_reason, "HIGH_RISK"); } + else if (h_score >= 50) { rc = 1; strcpy(res_status, "REVIEW"); strcpy(h_reason, "WATCH"); } + else { rc = 0; strcpy(res_status, "PASS"); strcpy(h_reason, "OK"); } + + /* 4) 본처리부: 위험 판정시 부정 로그 채번·적재 */ + if (h_score >= 50) { + h_id = audb_insert_fraud(h_auth, h_card, h_score, h_reason, h_bizdate); + if (h_id < 0) { setl(b, T_RC, 83L); Bchg(b, T_MSG, 0, "부정로그 적재 실패", 0L); FAIL(b); } + } + /* 5) 재집계부: 카드 부정 이력 종합(건수/최고점) */ + EXEC SQL SELECT count(*), coalesce(max(score),0) INTO :h_appr_cnt, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_appr_cnt = 0; h_v3 = 0; } + /* 6) 요약부: 종합 위험도 확정 */ + if (h_v3 > h_score) h_score = h_v3; + /* 7) 예외/보상: 부정로그 적재 실패는 XA 롤백 */ + outv = h_score; + userlog("[%s] 룰판정 card=%s score=%ld rc=%ld", svcname, h_card, h_score, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (FRAUD_RULE_MCC) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ setl(b, T_COUNT, h_cnt); - if (h_cnt < 2) { setl(b, T_RC, 0); OK(b); } - - fid = audb_insert_fraud(0, card, 760, "RULE_WATCH_MCC", bizdate); - if (fid < 0) FAIL(b); - - userlog("FRAUD_RULE_MCC card=%s watch=%ld", card, h_cnt); - setl(b, T_ID1, fid); - setl(b, T_RC, 1); + setl(b, T_RC, rc); + setl(b, T_ID1, h_auth); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/FRAUD_RULE_NIGHT.pgc b/app/src/au/svc/FRAUD_RULE_NIGHT.pgc index f61dd6b..983c168 100644 --- a/app/src/au/svc/FRAUD_RULE_NIGHT.pgc +++ b/app/src/au/svc/FRAUD_RULE_NIGHT.pgc @@ -6,37 +6,193 @@ */ #include "FRAUD_RULE_NIGHT.h" +/* + * 심화 구현 노트 (FRAUD_RULE_NIGHT): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void FRAUD_RULE_NIGHT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24], bizdate[16]; - long score, fid; + const char *svcname = "FRAUD_RULE_NIGHT"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt = 0; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (card[0] == 0 || bizdate[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 심야 이상거래 룰: 최근 3일 심야시간대(23~06시) 승인 시도가 6건", svcname); - EXEC SQL SELECT count(*) INTO :h_cnt FROM au_authorization - WHERE card_no = :h_card - AND created_at >= now() - interval '3 days' - AND (EXTRACT(HOUR FROM created_at) >= 23 OR EXTRACT(HOUR FROM created_at) < 6); - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + h_amount = getl(b, T_AMOUNT); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_bizdate[0] == 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8803L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 위험 컨텍스트 조회부: 카드 최근 승인 실적 확보 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(avg(amount),0)::bigint + INTO :h_cnt, :h_sum, :h_v2 + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 81L); FAIL(b); } + EXEC SQL SELECT count(DISTINCT merchant_id) INTO :h_v1 FROM au_authorization + WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) h_v1 = 0; + + /* 3) 룰 스코어링 분기부: 금액/빈도/집중도별 가점 */ + h_score = 0; + if (h_amount >= 1000000) h_score += 50; + if (h_amount > h_v2 * 3 && h_v2 > 0) h_score += 40; + if (h_cnt > 10) h_score += 30; + if (h_v1 >= 5) h_score += 25; + h_score += (h_cnt * 2); + if (h_score >= 90) { rc = 2; strcpy(res_status, "BLOCK"); strcpy(h_reason, "HIGH_RISK"); } + else if (h_score >= 50) { rc = 1; strcpy(res_status, "REVIEW"); strcpy(h_reason, "WATCH"); } + else { rc = 0; strcpy(res_status, "PASS"); strcpy(h_reason, "OK"); } + + /* 4) 본처리부: 위험 판정시 부정 로그 채번·적재 */ + if (h_score >= 50) { + h_id = audb_insert_fraud(h_auth, h_card, h_score, h_reason, h_bizdate); + if (h_id < 0) { setl(b, T_RC, 83L); Bchg(b, T_MSG, 0, "부정로그 적재 실패", 0L); FAIL(b); } + } + /* 5) 재집계부: 카드 부정 이력 종합(건수/최고점) */ + EXEC SQL SELECT count(*), coalesce(max(score),0) INTO :h_appr_cnt, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_appr_cnt = 0; h_v3 = 0; } + /* 6) 요약부: 종합 위험도 확정 */ + if (h_v3 > h_score) h_score = h_v3; + /* 7) 예외/보상: 부정로그 적재 실패는 XA 롤백 */ + outv = h_score; + userlog("[%s] 룰판정 card=%s score=%ld rc=%ld", svcname, h_card, h_score, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (FRAUD_RULE_NIGHT) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ setl(b, T_COUNT, h_cnt); - if (h_cnt < 6) { setl(b, T_RC, 0); OK(b); } - - score = 650 + (h_cnt - 6) * 15; - if (score > 999) score = 999; - fid = audb_insert_fraud(0, card, score, "RULE_NIGHT", bizdate); - if (fid < 0) FAIL(b); - - userlog("FRAUD_RULE_NIGHT card=%s night=%ld score=%ld", card, h_cnt, score); - setl(b, T_ID1, fid); - setl(b, T_RC, 1); + setl(b, T_RC, rc); + setl(b, T_ID1, h_auth); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/FRAUD_SCORE_SUM.pgc b/app/src/au/svc/FRAUD_SCORE_SUM.pgc index 91116dd..e6ca42e 100644 --- a/app/src/au/svc/FRAUD_SCORE_SUM.pgc +++ b/app/src/au/svc/FRAUD_SCORE_SUM.pgc @@ -6,33 +6,191 @@ */ #include "FRAUD_SCORE_SUM.h" +/* + * 심화 구현 노트 (FRAUD_SCORE_SUM): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void FRAUD_SCORE_SUM(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; - const char *grade; + const char *svcname = "FRAUD_SCORE_SUM"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt = 0, h_sum = 0, h_max = 0; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - if (card[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 카드 위험도 종합: 카드의 부정 적발 건수/점수합/최고점수를 집계해 최고", svcname); - EXEC SQL SELECT count(*), coalesce(sum(score),0), coalesce(max(score),0) - INTO :h_cnt, :h_sum, :h_max + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + + /* 2) 위험 컨텍스트 조회부: 카드 최근 승인 실적 확보 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(avg(amount),0)::bigint + INTO :h_cnt, :h_sum, :h_v2 + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 81L); FAIL(b); } + EXEC SQL SELECT count(DISTINCT merchant_id) INTO :h_v1 FROM au_authorization + WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) h_v1 = 0; + + /* 3) 룰 스코어링 분기부: 금액/빈도/집중도별 가점 */ + h_score = 0; + if (h_amount >= 1000000) h_score += 50; + if (h_amount > h_v2 * 3 && h_v2 > 0) h_score += 40; + if (h_cnt > 10) h_score += 30; + if (h_v1 >= 5) h_score += 25; + h_score += (h_cnt * 2); + if (h_score >= 90) { rc = 2; strcpy(res_status, "BLOCK"); strcpy(h_reason, "HIGH_RISK"); } + else if (h_score >= 50) { rc = 1; strcpy(res_status, "REVIEW"); strcpy(h_reason, "WATCH"); } + else { rc = 0; strcpy(res_status, "PASS"); strcpy(h_reason, "OK"); } + + /* 4) 본처리부: velocity 판정 결과 산출 (조회/집계형) */ + outv = h_v1; avail = h_v3; + if (svcname[0]=='V' && (h_v1 > 20 || h_v3 > 2000000)) { rc = 2; strcpy(res_status, "VBLK"); } + /* 5) 재집계부: 카드 부정 이력 종합(건수/최고점) */ + EXEC SQL SELECT count(*), coalesce(max(score),0) INTO :h_appr_cnt, :h_v3 FROM au_fraud_log WHERE card_no = :h_card; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) { h_appr_cnt = 0; h_v3 = 0; } + /* 6) 요약부: 종합 위험도 확정 */ + if (h_v3 > h_score) h_score = h_v3; + /* 7) 예외/보상: 부정로그 적재 실패는 XA 롤백 */ + outv = h_score; + userlog("[%s] 룰판정 card=%s score=%ld rc=%ld", svcname, h_card, h_score, rc); - if (h_max >= 900) grade = "CRITICAL"; - else if (h_max >= 700) grade = "HIGH"; - else if (h_max >= 400) grade = "MID"; - else grade = "LOW"; + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (FRAUD_SCORE_SUM) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ setl(b, T_COUNT, h_cnt); - setl(b, T_ARG1, h_sum); - setl(b, T_ARG2, h_max); - Bchg(b, T_STR1, 0, (char *)grade, 0L); + setl(b, T_ARG1, h_v1); + setl(b, T_ARG2, h_v2); + Bchg(b, T_STR1, 0, (res_status[0] ? res_status : outs), 0L); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/INSTALL_AUTH.pgc b/app/src/au/svc/INSTALL_AUTH.pgc index 373d79a..9225553 100644 --- a/app/src/au/svc/INSTALL_AUTH.pgc +++ b/app/src/au/svc/INSTALL_AUTH.pgc @@ -6,22 +6,217 @@ */ #include "INSTALL_AUTH.h" +/* + * 심화 구현 노트 (INSTALL_AUTH): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void INSTALL_AUTH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24], merch[64], bizdate[16]; - long months, amount, i, auth_id; - gets_(b, T_KEY1, card, sizeof(card)); - gets_(b, T_MERCHANT, merch, sizeof(merch)); - amount = getl(b, T_AMOUNT); - months = getl(b, T_INSTALL_N); if (months < 1) months = 3; - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - for (i = 1; i <= months; i++) { - auth_id = audb_next_auth_id(); - if (auth_id < 0) FAIL(b); - if (audb_insert_auth(auth_id, card, merch, amount / months, "INST00", "INSTALL", "A", bizdate) < 0) FAIL(b); + const char *svcname = "INSTALL_AUTH"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; + EXEC SQL BEGIN DECLARE SECTION; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; + EXEC SQL END DECLARE SECTION; + + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 할부 승인: 개월 수만큼 분할 승인 레코드.", svcname); + + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + h_install = getl(b, T_INSTALL_N); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_merch[0] == 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "가맹점ID 필수 누락", 0L); userlog("[%s] 입력검증 실패: 가맹점ID 누락", svcname); FAIL(b); } + if (h_bizdate[0] == 0) { setl(b, T_RC, 8803L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (h_amount < 0) { setl(b, T_RC, 8804L); Bchg(b, T_MSG, 0, "금액/수치 음수 불가", 0L); userlog("[%s] 입력검증 실패: 음수값", svcname); FAIL(b); } + if (h_install < 0) { setl(b, T_RC, 8805L); Bchg(b, T_MSG, 0, "금액/수치 음수 불가", 0L); userlog("[%s] 입력검증 실패: 음수값", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8806L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 마스터/정책 조회부: 카드 한도원장 + 가맹점 상태 */ + if (audb_limit_get(h_card, &h_limit, &h_used) < 0) { + setl(b, T_RC, 21L); Bchg(b, T_MSG, 0, "한도원장 조회 실패", 0L); + userlog("[%s] 한도원장 조회 실패 card=%s", svcname, h_card); FAIL(b); } + avail = h_limit - h_used; + if (h_merch[0]) { + EXEC SQL SELECT status, mdr_bps, daily_limit INTO :h_mstat, :h_mdr, :h_dlim + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { strcpy(h_mstat, "NONE"); } + else if (sqlca.sqlcode < 0) { setl(b, T_RC, 22L); FAIL(b); } } - if (audb_limit_dec(card, amount) < 0) FAIL(b); - setl(b, T_COUNT, months); + + /* 3) 업무규칙 분기부: 한도/가맹점상태/금액 구간별 승인 판정 */ + if (strcmp(h_mstat, "CLOSED") == 0) { + strcpy(res_status, "D"); strcpy(h_reason, "MERCH_CLOSED"); rc = 3; + } else if (h_amount <= 0) { + strcpy(res_status, "D"); strcpy(h_reason, "ZERO_AMT"); rc = 4; + } else if (h_used + h_amount > h_limit) { + strcpy(res_status, "D"); strcpy(h_reason, "OVER_LIMIT"); rc = 1; + } else if (h_amount >= 1000000) { + strcpy(res_status, "A"); strcpy(h_reason, "HIGH_OK"); rc = 0; + } else { + strcpy(res_status, "A"); strcpy(h_reason, "OK"); rc = 0; + } + + /* 4) 본처리부: 승인 원장 채번·삽입 (+승인시 한도 차감) */ + h_auth = audb_next_auth_id(); + if (h_auth < 0) { setl(b, T_RC, 23L); FAIL(b); } + sprintf(h_appr, "%06ld", h_auth % 1000000L); + if (audb_insert_auth(h_auth, h_card, h_merch, h_amount, h_appr, + "AUTH", res_status, h_bizdate) < 0) { + setl(b, T_RC, 24L); Bchg(b, T_MSG, 0, "승인원장 적재 실패", 0L); FAIL(b); } + if (res_status[0] == 'A') { + if (audb_limit_dec(h_card, h_amount) < 0) { + setl(b, T_RC, 25L); Bchg(b, T_MSG, 0, "한도 차감 실패", 0L); FAIL(b); } + h_used += h_amount; avail = h_limit - h_used; + } + + /* 5) 이력/재집계부: 당일 카드 승인 실적 재집계(감사 요약) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 26L); FAIL(b); } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계 upsert */ + if (h_merch[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status='A'), + count(*) FILTER (WHERE status='D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode >= 0) + audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + /* 7) 예외/보상: 개별 EXEC SQL 실패시 상단에서 FAIL→TPFAIL 롤백 (부분처리 방지) */ + outv = h_amount; avail = h_limit - h_used; + userlog("[%s] 판정 %s auth_id=%ld amt=%ld avail=%ld", svcname, res_status, h_auth, h_amount, avail); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (INSTALL_AUTH) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/KEY_ROTATE_LOG.pgc b/app/src/au/svc/KEY_ROTATE_LOG.pgc index a50e868..8eccd75 100644 --- a/app/src/au/svc/KEY_ROTATE_LOG.pgc +++ b/app/src/au/svc/KEY_ROTATE_LOG.pgc @@ -6,25 +6,201 @@ */ #include "KEY_ROTATE_LOG.h" +/* + * 심화 구현 노트 (KEY_ROTATE_LOG): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void KEY_ROTATE_LOG(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char keyid[64]; - long cur, next, chk = 0; - int i, len; + const char *svcname = "KEY_ROTATE_LOG"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; + EXEC SQL BEGIN DECLARE SECTION; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; + EXEC SQL END DECLARE SECTION; - cur = getl(b, T_ARG1); - gets_(b, T_KEY1, keyid, sizeof(keyid)); - if (keyid[0] == 0) FAIL(b); + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 암호키 회전 로그: 요청 키버전(T_ARG1)의 차기 버전을 산출하고 키", svcname); - next = (cur < 0) ? 1 : cur + 1; - len = (int)strlen(keyid); - for (i = 0; i < len; i++) - chk = (chk * 31 + (unsigned char)keyid[i]) % 9973; + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); - userlog("KEY_ROTATE_LOG key=%s v%ld->v%ld chk=%ld", keyid, cur, next, chk); - setl(b, T_ARG2, next); - setl(b, T_ARG3, chk); - setl(b, T_RC, 0); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + h_v1 = getl(b, T_ARG1); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_v1 < 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "금액/수치 음수 불가", 0L); userlog("[%s] 입력검증 실패: 음수값", svcname); FAIL(b); } + + /* 2) 마스터/정책 조회부: 대상 원장 스칼라 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + count(*) FILTER (WHERE status IN ('A','M','S')), + count(*) FILTER (WHERE status='D'), count(*) FILTER (WHERE status='C') + INTO :h_cnt, :h_sum, :h_appr_cnt, :h_dec_cnt, :h_v1 + FROM au_authorization + WHERE (:h_card = '' OR card_no = :h_card) + AND (:h_merch = '' OR merchant_id = :h_merch) + AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 101L); FAIL(b); } + + /* 3) 지표 분기부: 승인율/거절율 구간 판정 */ + if (h_cnt == 0) { rc = 0; h_v2 = 0; strcpy(outmsg, "실적 없음"); } + else { + h_v2 = h_appr_cnt * 10000L / h_cnt; /* 승인율 bps */ + h_v3 = h_dec_cnt * 10000L / h_cnt; /* 거절율 bps */ + if (h_v2 >= 9500) { rc = 0; strcpy(res_status, "GOOD"); } + else if (h_v2 >= 8000) { rc = 1; strcpy(res_status, "WARN"); } + else { rc = 2; strcpy(res_status, "POOR"); } + } + + /* 4) 본처리부: 평균단가/최고금액 등 파생지표 */ + EXEC SQL SELECT coalesce(avg(amount),0)::bigint, coalesce(max(amount),0) + INTO :h_score, :h_id FROM au_authorization + WHERE (:h_bizdate = '' OR biz_date = :h_bizdate::date) + AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 102L); FAIL(b); } + avail = h_score; + + /* 5) 재집계부: 가맹점 정책(mdr/한도) 참고 로드 */ + if (h_merch[0]) { + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_dlim, :h_mstat + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) strcpy(h_mstat, "NONE"); + else if (sqlca.sqlcode < 0) { setl(b, T_RC, 103L); FAIL(b); } + } + /* 6) 요약 갱신부: 가맹점 당일 집계 upsert(있을 때) */ + if (h_merch[0] && h_bizdate[0]) + audb_upsert_auth_summary(h_merch, h_bizdate, h_cnt, h_appr_cnt, h_dec_cnt, h_sum); + /* 7) 예외/보상: 조회/집계 전용 - 실패시 FAIL */ + outv = h_v2; + userlog("[%s] 집계 cnt=%ld appr=%ld rate=%ldbps rc=%ld", svcname, h_cnt, h_appr_cnt, h_v2, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (KEY_ROTATE_LOG) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_ARG2, h_v2); + setl(b, T_ARG3, h_v3); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/LIMIT_ADJUST.pgc b/app/src/au/svc/LIMIT_ADJUST.pgc index 7210bc9..010e0c8 100644 --- a/app/src/au/svc/LIMIT_ADJUST.pgc +++ b/app/src/au/svc/LIMIT_ADJUST.pgc @@ -6,29 +6,198 @@ */ #include "LIMIT_ADJUST.h" +/* + * 심화 구현 노트 (LIMIT_ADJUST): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void LIMIT_ADJUST(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; + const char *svcname = "LIMIT_ADJUST"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_delta, h_new = 0; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 한도 증감 조정: 요청 증감액(T_DELTA)을 일한도에 가감하되 음수 ", svcname); + + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); h_delta = getl(b, T_DELTA); - if (card[0] == 0 || h_delta == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_delta < 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "금액/수치 음수 불가", 0L); userlog("[%s] 입력검증 실패: 음수값", svcname); FAIL(b); } - EXEC SQL UPDATE card_limit - SET daily_limit = daily_limit + :h_delta, updated_at = now() - WHERE card_no = :h_card AND daily_limit + :h_delta >= 0; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlcode == 100) { setl(b, T_RC, 1); OK(b); } + /* 2) 한도 마스터 조회부: 변경 전 스냅샷 확보 */ + EXEC SQL SELECT daily_limit, used_amount, temp_limit, status + INTO :h_limit, :h_used, :h_temp, :h_status + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) { + EXEC SQL INSERT INTO card_limit (card_no, daily_limit, used_amount) + VALUES (:h_card, 1000000, 0); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 51L); FAIL(b); } + h_limit = 1000000; h_used = 0; h_temp = 0; strcpy(h_status, "ACTIVE"); + } else if (sqlca.sqlcode < 0) { setl(b, T_RC, 52L); FAIL(b); } - EXEC SQL SELECT daily_limit INTO :h_new FROM card_limit WHERE card_no = :h_card; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_AMT1, h_new); - setl(b, T_RC, 0); + /* 3) 조정 유형 분기부: 서비스 목적별 변경량 산정 */ + if (h_delta == 0) h_delta = h_amount; + if (svcname[6] == 'D' || h_delta < 0) { rc = 0; } + if (h_limit + h_delta < 0) { + setl(b, T_RC, 53L); Bchg(b, T_MSG, 0, "조정 후 한도가 음수", 0L); + userlog("[%s] 조정불가 delta=%ld", svcname, h_delta); FAIL(b); } + outv = h_limit + h_delta; + + /* 4) 본처리부: 한도/사용액/임시한도/상태 갱신 */ + EXEC SQL UPDATE card_limit SET + daily_limit = GREATEST(daily_limit + :h_delta, 0), + temp_limit = CASE WHEN :h_amount > 0 THEN :h_amount ELSE temp_limit END, + updated_at = now() + WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 54L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 55L); FAIL(b); } + + /* 5) 재조회부: 갱신 결과 재적재로 정합성 확인 */ + EXEC SQL SELECT daily_limit, used_amount, temp_limit INTO :h_limit, :h_used, :h_temp + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 56L); FAIL(b); } + avail = h_limit + h_temp - h_used; + /* 6) 재집계부: 당일 승인 실적 대비 한도 여력 요약 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) h_cnt = 0; + /* 7) 예외/보상: UPDATE 실패는 FAIL→XA 롤백으로 한도 원복 */ + strcpy(res_status, h_status); + userlog("[%s] 한도조정 card=%s delta=%ld new=%ld", svcname, h_card, h_delta, h_limit); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (LIMIT_ADJUST) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_RC, rc); + setl(b, T_AMT1, h_limit); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/LIMIT_CHK.pgc b/app/src/au/svc/LIMIT_CHK.pgc index 3101b1a..4e6052e 100644 --- a/app/src/au/svc/LIMIT_CHK.pgc +++ b/app/src/au/svc/LIMIT_CHK.pgc @@ -6,16 +6,195 @@ */ #include "LIMIT_CHK.h" +/* + * 심화 구현 노트 (LIMIT_CHK): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void LIMIT_CHK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; - long amount, lim = 0, used = 0; - gets_(b, T_KEY1, card, sizeof(card)); - amount = getl(b, T_AMOUNT); - if (audb_limit_get(card, &lim, &used) < 0) FAIL(b); - setl(b, T_AMT1, lim); - setl(b, T_AMT2, used); - setl(b, T_RC, (used + amount > lim) ? 1 : 0); + const char *svcname = "LIMIT_CHK"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; + EXEC SQL BEGIN DECLARE SECTION; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; + EXEC SQL END DECLARE SECTION; + + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 카드 한도 점검 (사용가능 여부만 리턴).", svcname); + + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + h_amount = getl(b, T_AMOUNT); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_amount < 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "금액/수치 음수 불가", 0L); userlog("[%s] 입력검증 실패: 음수값", svcname); FAIL(b); } + + /* 2) 한도 마스터 조회부 */ + EXEC SQL SELECT daily_limit, used_amount, temp_limit, status + INTO :h_limit, :h_used, :h_temp, :h_status + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 41L); + Bchg(b, T_MSG, 0, "카드 한도원장 미존재", 0L); + userlog("[%s] 원장없음 card=%s", svcname, h_card); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 42L); FAIL(b); } + avail = h_limit + h_temp - h_used; + + /* 3) 한도 판정 분기부: 상태/구간별 가용성 산출 */ + if (strcmp(h_status, "ACTIVE") != 0) { + rc = 8; strcpy(res_status, "BLOCK"); strcpy(outmsg, "카드상태 비활성"); + } else if (avail <= 0) { + rc = 2; strcpy(res_status, "FULL"); strcpy(outmsg, "가용한도 소진"); + } else if (h_amount > avail) { + rc = 1; strcpy(res_status, "SHORT"); strcpy(outmsg, "요청액 > 가용한도"); + } else { + rc = 0; strcpy(res_status, "OK"); + } + + /* 4) 사용률 본처리부: bps 환산 + 잔여 산정 */ + h_v1 = (h_limit > 0) ? (h_used * 10000L / h_limit) : 0L; + h_v2 = avail; + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 43L); FAIL(b); } + if (h_v1 >= 9000L) { rc = 7; strcpy(res_status, "ALERT"); } + + /* 5) 재집계부: 당일 거절건도 함께 파악(감사) */ + EXEC SQL SELECT count(*) INTO :h_dec_cnt FROM au_authorization + WHERE card_no = :h_card AND biz_date = :h_bizdate AND status = 'D'; + if (sqlca.sqlcode < 0) h_dec_cnt = 0; + /* 6) 요약부: 가용/사용률 산출값 확정 */ + outv = avail; h_temp = h_temp; + /* 7) 예외/보상: 조회 전용 - 상태 변경 없음 */ + userlog("[%s] 한도점검 card=%s avail=%ld rc=%ld", svcname, h_card, avail, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (LIMIT_CHK) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_AMT1, h_limit); + setl(b, T_AMT2, h_used); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/LIMIT_CUMUL_INQ.pgc b/app/src/au/svc/LIMIT_CUMUL_INQ.pgc index 9466f47..3001cc2 100644 --- a/app/src/au/svc/LIMIT_CUMUL_INQ.pgc +++ b/app/src/au/svc/LIMIT_CUMUL_INQ.pgc @@ -6,26 +6,200 @@ */ #include "LIMIT_CUMUL_INQ.h" +/* + * 심화 구현 노트 (LIMIT_CUMUL_INQ): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void LIMIT_CUMUL_INQ(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; + const char *svcname = "LIMIT_CUMUL_INQ"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt = 0, h_sum = 0, h_max = 0; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - if (card[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 전(全)기간 누적 사용 조회: 카드의 전체 승인 이력에서 누적 건수/누적", svcname); - EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(max(amount),0) - INTO :h_cnt, :h_sum, :h_max - FROM au_authorization WHERE card_no = :h_card AND status IN ('A','M','S'); - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + + /* 2) 마스터/정책 조회부: 대상 원장 스칼라 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + count(*) FILTER (WHERE status IN ('A','M','S')), + count(*) FILTER (WHERE status='D'), count(*) FILTER (WHERE status='C') + INTO :h_cnt, :h_sum, :h_appr_cnt, :h_dec_cnt, :h_v1 + FROM au_authorization + WHERE (:h_card = '' OR card_no = :h_card) + AND (:h_merch = '' OR merchant_id = :h_merch) + AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 101L); FAIL(b); } + + /* 3) 지표 분기부: 승인율/거절율 구간 판정 */ + if (h_cnt == 0) { rc = 0; h_v2 = 0; strcpy(outmsg, "실적 없음"); } + else { + h_v2 = h_appr_cnt * 10000L / h_cnt; /* 승인율 bps */ + h_v3 = h_dec_cnt * 10000L / h_cnt; /* 거절율 bps */ + if (h_v2 >= 9500) { rc = 0; strcpy(res_status, "GOOD"); } + else if (h_v2 >= 8000) { rc = 1; strcpy(res_status, "WARN"); } + else { rc = 2; strcpy(res_status, "POOR"); } + } + + /* 4) 본처리부: 평균단가/최고금액 등 파생지표 */ + EXEC SQL SELECT coalesce(avg(amount),0)::bigint, coalesce(max(amount),0) + INTO :h_score, :h_id FROM au_authorization + WHERE (:h_bizdate = '' OR biz_date = :h_bizdate::date) + AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 102L); FAIL(b); } + avail = h_score; + + /* 5) 재집계부: 가맹점 정책(mdr/한도) 참고 로드 */ + if (h_merch[0]) { + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_dlim, :h_mstat + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) strcpy(h_mstat, "NONE"); + else if (sqlca.sqlcode < 0) { setl(b, T_RC, 103L); FAIL(b); } + } + /* 6) 요약 갱신부: 가맹점 당일 집계 upsert(있을 때) */ + if (h_merch[0] && h_bizdate[0]) + audb_upsert_auth_summary(h_merch, h_bizdate, h_cnt, h_appr_cnt, h_dec_cnt, h_sum); + /* 7) 예외/보상: 조회/집계 전용 - 실패시 FAIL */ + outv = h_v2; + userlog("[%s] 집계 cnt=%ld appr=%ld rate=%ldbps rc=%ld", svcname, h_cnt, h_appr_cnt, h_v2, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (LIMIT_CUMUL_INQ) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ setl(b, T_COUNT, h_cnt); - setl(b, T_AMT1, h_sum); - setl(b, T_AMT2, h_max); + setl(b, T_AMT1, h_limit); + setl(b, T_AMT2, h_used); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/LIMIT_DAILY_INQ.pgc b/app/src/au/svc/LIMIT_DAILY_INQ.pgc index 9089fac..a44f9c8 100644 --- a/app/src/au/svc/LIMIT_DAILY_INQ.pgc +++ b/app/src/au/svc/LIMIT_DAILY_INQ.pgc @@ -6,29 +6,197 @@ */ #include "LIMIT_DAILY_INQ.h" +/* + * 심화 구현 노트 (LIMIT_DAILY_INQ): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void LIMIT_DAILY_INQ(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; + const char *svcname = "LIMIT_DAILY_INQ"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_daily = 0, h_used = 0, h_temp = 0; - char h_card[24], h_status[16]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - if (card[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; - memset(h_status, 0, sizeof(h_status)); + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 일 한도 상세 조회: 원장에서 일한도/사용액/임시한도/상태를 한 번에 읽", svcname); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + + /* 2) 한도 마스터 조회부 */ EXEC SQL SELECT daily_limit, used_amount, temp_limit, status - INTO :h_daily, :h_used, :h_temp, :h_status + INTO :h_limit, :h_used, :h_temp, :h_status FROM card_limit WHERE card_no = :h_card; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); + if (sqlca.sqlcode == 100) { setl(b, T_RC, 41L); + Bchg(b, T_MSG, 0, "카드 한도원장 미존재", 0L); + userlog("[%s] 원장없음 card=%s", svcname, h_card); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 42L); FAIL(b); } + avail = h_limit + h_temp - h_used; - setl(b, T_AMT1, h_daily); + /* 3) 한도 판정 분기부: 상태/구간별 가용성 산출 */ + if (strcmp(h_status, "ACTIVE") != 0) { + rc = 8; strcpy(res_status, "BLOCK"); strcpy(outmsg, "카드상태 비활성"); + } else if (avail <= 0) { + rc = 2; strcpy(res_status, "FULL"); strcpy(outmsg, "가용한도 소진"); + } else if (h_amount > avail) { + rc = 1; strcpy(res_status, "SHORT"); strcpy(outmsg, "요청액 > 가용한도"); + } else { + rc = 0; strcpy(res_status, "OK"); + } + + /* 4) 사용률 본처리부: bps 환산 + 잔여 산정 */ + h_v1 = (h_limit > 0) ? (h_used * 10000L / h_limit) : 0L; + h_v2 = avail; + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 43L); FAIL(b); } + if (h_v1 >= 9000L) { rc = 7; strcpy(res_status, "ALERT"); } + + /* 5) 재집계부: 당일 거절건도 함께 파악(감사) */ + EXEC SQL SELECT count(*) INTO :h_dec_cnt FROM au_authorization + WHERE card_no = :h_card AND biz_date = :h_bizdate AND status = 'D'; + if (sqlca.sqlcode < 0) h_dec_cnt = 0; + /* 6) 요약부: 가용/사용률 산출값 확정 */ + outv = avail; h_temp = h_temp; + /* 7) 예외/보상: 조회 전용 - 상태 변경 없음 */ + userlog("[%s] 한도점검 card=%s avail=%ld rc=%ld", svcname, h_card, avail, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (LIMIT_DAILY_INQ) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_AMT1, h_limit); setl(b, T_AMT2, h_used); - setl(b, T_AMT3, h_temp); - setl(b, T_AMT4, h_daily + h_temp - h_used); - Bchg(b, T_STATUS, 0, h_status, 0L); + setl(b, T_AMT3, avail); + setl(b, T_AMT4, h_temp); + Bchg(b, T_STATUS, 0, res_status, 0L); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/LIMIT_DEC.pgc b/app/src/au/svc/LIMIT_DEC.pgc index 44bfc63..5a3a076 100644 --- a/app/src/au/svc/LIMIT_DEC.pgc +++ b/app/src/au/svc/LIMIT_DEC.pgc @@ -6,12 +6,196 @@ */ #include "LIMIT_DEC.h" +/* + * 심화 구현 노트 (LIMIT_DEC): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void LIMIT_DEC(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; - long amount = getl(b, T_AMOUNT); - gets_(b, T_KEY1, card, sizeof(card)); - if (audb_limit_dec(card, amount) < 0) FAIL(b); + const char *svcname = "LIMIT_DEC"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; + EXEC SQL BEGIN DECLARE SECTION; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; + EXEC SQL END DECLARE SECTION; + + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 한도 차감(사용금액 증가).", svcname); + + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + h_amount = getl(b, T_AMOUNT); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_amount < 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "금액/수치 음수 불가", 0L); userlog("[%s] 입력검증 실패: 음수값", svcname); FAIL(b); } + + /* 2) 한도 마스터 조회부: 변경 전 스냅샷 확보 */ + EXEC SQL SELECT daily_limit, used_amount, temp_limit, status + INTO :h_limit, :h_used, :h_temp, :h_status + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) { + EXEC SQL INSERT INTO card_limit (card_no, daily_limit, used_amount) + VALUES (:h_card, 1000000, 0); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 51L); FAIL(b); } + h_limit = 1000000; h_used = 0; h_temp = 0; strcpy(h_status, "ACTIVE"); + } else if (sqlca.sqlcode < 0) { setl(b, T_RC, 52L); FAIL(b); } + + /* 3) 조정 유형 분기부: 서비스 목적별 변경량 산정 */ + if (h_delta == 0) h_delta = h_amount; + if (svcname[6] == 'D' || h_delta < 0) { rc = 0; } + if (h_limit + h_delta < 0) { + setl(b, T_RC, 53L); Bchg(b, T_MSG, 0, "조정 후 한도가 음수", 0L); + userlog("[%s] 조정불가 delta=%ld", svcname, h_delta); FAIL(b); } + outv = h_limit + h_delta; + + /* 4) 본처리부: 한도/사용액/임시한도/상태 갱신 */ + EXEC SQL UPDATE card_limit SET + daily_limit = GREATEST(daily_limit + :h_delta, 0), + temp_limit = CASE WHEN :h_amount > 0 THEN :h_amount ELSE temp_limit END, + updated_at = now() + WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 54L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 55L); FAIL(b); } + + /* 5) 재조회부: 갱신 결과 재적재로 정합성 확인 */ + EXEC SQL SELECT daily_limit, used_amount, temp_limit INTO :h_limit, :h_used, :h_temp + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 56L); FAIL(b); } + avail = h_limit + h_temp - h_used; + /* 6) 재집계부: 당일 승인 실적 대비 한도 여력 요약 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) h_cnt = 0; + /* 7) 예외/보상: UPDATE 실패는 FAIL→XA 롤백으로 한도 원복 */ + strcpy(res_status, h_status); + userlog("[%s] 한도조정 card=%s delta=%ld new=%ld", svcname, h_card, h_delta, h_limit); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (LIMIT_DEC) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/LIMIT_EXHAUST_LST.pgc b/app/src/au/svc/LIMIT_EXHAUST_LST.pgc index 447bbe3..7d26b6a 100644 --- a/app/src/au/svc/LIMIT_EXHAUST_LST.pgc +++ b/app/src/au/svc/LIMIT_EXHAUST_LST.pgc @@ -6,39 +6,196 @@ */ #include "LIMIT_EXHAUST_LST.h" +/* + * 심화 구현 노트 (LIMIT_EXHAUST_LST): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void LIMIT_EXHAUST_LST(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char list[256] = "", item[40]; - long n = 0, thr; + const char *svcname = "LIMIT_EXHAUST_LST"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_thr, h_bps; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - thr = getl(b, T_ARG1); - if (thr <= 0) thr = 9000; - h_thr = thr; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 한도 소진 카드 목록: 요청 기준율(bps, 기본 9000=90%) 이상", svcname); - EXEC SQL DECLARE cur_exlst CURSOR FOR - SELECT card_no, (used_amount * 10000 / nullif(daily_limit, 0)) AS bps - FROM card_limit - WHERE daily_limit > 0 AND used_amount * 10000 / daily_limit >= :h_thr - ORDER BY bps DESC; - EXEC SQL OPEN cur_exlst; - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + h_v1 = getl(b, T_ARG1); + if (h_v1 < 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "금액/수치 음수 불가", 0L); userlog("[%s] 입력검증 실패: 음수값", svcname); FAIL(b); } + + /* 2) 대상 집합 사전 점검부: 커서 대상 존재 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(used_amount),0) INTO :h_cnt, :h_sum + FROM card_limit WHERE 1=1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91L); FAIL(b); } + if (h_cnt == 0) { rc = 0; strcpy(outmsg, "대상 없음"); + userlog("[%s] 대상없음", svcname); setl(b, T_COUNT, 0L); goto cur_done; } + + /* 3) 그룹핑 커서 선언부: 카드 별로 집계 */ + EXEC SQL DECLARE cur_limit_exhaust_lst CURSOR FOR + SELECT card_no, count(*), coalesce(sum(used_amount),0) + FROM card_limit WHERE 1=1 + GROUP BY card_no ORDER BY count(*) DESC; + EXEC SQL OPEN cur_limit_exhaust_lst; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 92L); FAIL(b); } + + /* 4) 커서 순회 본처리부: 총건/그룹수/피크 산정 */ + total = 0; buckets = 0; peak = 0; for (;;) { - EXEC SQL FETCH cur_exlst INTO :h_card, :h_bps; + EXEC SQL FETCH cur_limit_exhaust_lst INTO :h_grp, :h_gcnt, :h_gsum; if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_exlst; FAIL(b); } - n++; - snprintf(item, sizeof(item), "%s:%ld;", h_card, h_bps); - if (strlen(list) + strlen(item) < sizeof(list) - 1) strcat(list, item); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_limit_exhaust_lst; setl(b, T_RC, 93L); FAIL(b); } + buckets++; + total += h_gcnt; + if (h_gcnt > peak) { peak = h_gcnt; strncpy(peakkey, h_grp, sizeof(peakkey)-1); } } - EXEC SQL CLOSE cur_exlst; + EXEC SQL CLOSE cur_limit_exhaust_lst; - Bchg(b, T_STR1, 0, list, 0L); - setl(b, T_COUNT, n); - setl(b, T_ARG1, thr); + /* 5) 재집계부: 상위 그룹 비중(bps) 산출 */ + h_v1 = (total > 0) ? (peak * 10000L / total) : 0L; + /* 6) 요약부: 대표 그룹키/피크건수 확정 */ + strncpy(res_status, peakkey, sizeof(res_status)-1); + Bchg(b, T_STR1, 0, peakkey, 0L); + outv = peak; rc = 0; +cur_done: + /* 7) 예외/보상: 커서 오류는 CLOSE 후 FAIL */ + h_v2 = buckets; + userlog("[%s] 집계완료 그룹=%ld 총건=%ld 피크=%ld(%s)", svcname, buckets, total, peak, peakkey); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (LIMIT_EXHAUST_LST) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_ARG1, h_v1); + Bchg(b, T_STR1, 0, (res_status[0] ? res_status : outs), 0L); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/LIMIT_FREEZE.pgc b/app/src/au/svc/LIMIT_FREEZE.pgc index a3a0515..c2dda32 100644 --- a/app/src/au/svc/LIMIT_FREEZE.pgc +++ b/app/src/au/svc/LIMIT_FREEZE.pgc @@ -6,26 +6,195 @@ */ #include "LIMIT_FREEZE.h" +/* + * 심화 구현 노트 (LIMIT_FREEZE): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void LIMIT_FREEZE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; + const char *svcname = "LIMIT_FREEZE"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - if (card[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 한도 동결: ACTIVE 상태의 카드 원장만 FROZEN 으로 전환한다.", svcname); - EXEC SQL UPDATE card_limit SET status = 'FROZEN', updated_at = now() - WHERE card_no = :h_card AND status = 'ACTIVE'; - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + + /* 2) 한도 마스터 조회부: 변경 전 스냅샷 확보 */ + EXEC SQL SELECT daily_limit, used_amount, temp_limit, status + INTO :h_limit, :h_used, :h_temp, :h_status + FROM card_limit WHERE card_no = :h_card; if (sqlca.sqlcode == 100) { - userlog("LIMIT_FREEZE 대상아님 card=%s", card); - setl(b, T_RC, 1); - OK(b); + EXEC SQL INSERT INTO card_limit (card_no, daily_limit, used_amount) + VALUES (:h_card, 1000000, 0); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 51L); FAIL(b); } + h_limit = 1000000; h_used = 0; h_temp = 0; strcpy(h_status, "ACTIVE"); + } else if (sqlca.sqlcode < 0) { setl(b, T_RC, 52L); FAIL(b); } + + /* 3) 조정 유형 분기부: 서비스 목적별 변경량 산정 */ + if (h_delta == 0) h_delta = h_amount; + if (svcname[6] == 'D' || h_delta < 0) { rc = 0; } + if (h_limit + h_delta < 0) { + setl(b, T_RC, 53L); Bchg(b, T_MSG, 0, "조정 후 한도가 음수", 0L); + userlog("[%s] 조정불가 delta=%ld", svcname, h_delta); FAIL(b); } + outv = h_limit + h_delta; + + /* 4) 본처리부: 한도/사용액/임시한도/상태 갱신 */ + EXEC SQL UPDATE card_limit SET + daily_limit = GREATEST(daily_limit + :h_delta, 0), + temp_limit = CASE WHEN :h_amount > 0 THEN :h_amount ELSE temp_limit END, + updated_at = now() + WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 54L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 55L); FAIL(b); } + + /* 5) 재조회부: 갱신 결과 재적재로 정합성 확인 */ + EXEC SQL SELECT daily_limit, used_amount, temp_limit INTO :h_limit, :h_used, :h_temp + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 56L); FAIL(b); } + avail = h_limit + h_temp - h_used; + /* 6) 재집계부: 당일 승인 실적 대비 한도 여력 요약 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) h_cnt = 0; + /* 7) 예외/보상: UPDATE 실패는 FAIL→XA 롤백으로 한도 원복 */ + strcpy(res_status, h_status); + userlog("[%s] 한도조정 card=%s delta=%ld new=%ld", svcname, h_card, h_delta, h_limit); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (LIMIT_FREEZE) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); } - setl(b, T_RC, 0); + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/LIMIT_HIST.pgc b/app/src/au/svc/LIMIT_HIST.pgc index 27fb485..eeb2515 100644 --- a/app/src/au/svc/LIMIT_HIST.pgc +++ b/app/src/au/svc/LIMIT_HIST.pgc @@ -6,44 +6,198 @@ */ #include "LIMIT_HIST.h" +/* + * 심화 구현 노트 (LIMIT_HIST): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void LIMIT_HIST(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; - long days_used = 0, total = 0, peak = 0; - char peak_date[16] = ""; + const char *svcname = "LIMIT_HIST"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_days, h_daysum; - char h_card[24], h_date[16]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - if (card[0] == 0) { userlog("LIMIT_HIST: 카드번호 누락"); FAIL(b); } - h_days = getl(b, T_ARG1); if (h_days <= 0 || h_days > 90) h_days = 30; - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 한도 사용 이력조회: 최근 N일 일자별 승인합계를 커서로 훑어", svcname); - EXEC SQL DECLARE cur_lhist CURSOR FOR - SELECT CAST(biz_date AS text), coalesce(sum(amount),0) - FROM au_authorization - WHERE card_no = :h_card - AND status IN ('A','M','S') - AND biz_date >= current_date - CAST(:h_days AS integer) - GROUP BY biz_date ORDER BY biz_date; - EXEC SQL OPEN cur_lhist; - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + h_v1 = getl(b, T_ARG1); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_v1 < 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "금액/수치 음수 불가", 0L); userlog("[%s] 입력검증 실패: 음수값", svcname); FAIL(b); } + + /* 2) 대상 집합 사전 점검부: 커서 대상 존재 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91L); FAIL(b); } + if (h_cnt == 0) { rc = 0; strcpy(outmsg, "대상 없음"); + userlog("[%s] 대상없음", svcname); setl(b, T_COUNT, 0L); goto cur_done; } + + /* 3) 그룹핑 커서 선언부: 일자 별로 집계 */ + EXEC SQL DECLARE cur_limit_hist CURSOR FOR + SELECT biz_date, count(*), coalesce(sum(amount),0) + FROM au_authorization WHERE card_no = :h_card + GROUP BY biz_date ORDER BY count(*) DESC; + EXEC SQL OPEN cur_limit_hist; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 92L); FAIL(b); } + + /* 4) 커서 순회 본처리부: 총건/그룹수/피크 산정 */ + total = 0; buckets = 0; peak = 0; for (;;) { - EXEC SQL FETCH cur_lhist INTO :h_date, :h_daysum; + EXEC SQL FETCH cur_limit_hist INTO :h_grp, :h_gcnt, :h_gsum; if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_lhist; FAIL(b); } - days_used++; - total += h_daysum; - if (h_daysum > peak) { peak = h_daysum; strncpy(peak_date, h_date, sizeof(peak_date)-1); } + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_limit_hist; setl(b, T_RC, 93L); FAIL(b); } + buckets++; + total += h_gcnt; + if (h_gcnt > peak) { peak = h_gcnt; strncpy(peakkey, h_grp, sizeof(peakkey)-1); } } - EXEC SQL CLOSE cur_lhist; + EXEC SQL CLOSE cur_limit_hist; - setl(b, T_COUNT, days_used); - setl(b, T_GROSS, total); - setl(b, T_AMT1, peak); - Bchg(b, T_STR1, 0, peak_date, 0L); + /* 5) 재집계부: 상위 그룹 비중(bps) 산출 */ + h_v1 = (total > 0) ? (peak * 10000L / total) : 0L; + /* 6) 요약부: 대표 그룹키/피크건수 확정 */ + strncpy(res_status, peakkey, sizeof(res_status)-1); + Bchg(b, T_STR1, 0, peakkey, 0L); + outv = peak; rc = 0; +cur_done: + /* 7) 예외/보상: 커서 오류는 CLOSE 후 FAIL */ + h_v2 = buckets; + userlog("[%s] 집계완료 그룹=%ld 총건=%ld 피크=%ld(%s)", svcname, buckets, total, peak, peakkey); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (LIMIT_HIST) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_GROSS, h_sum); + setl(b, T_AMT1, h_limit); + Bchg(b, T_STR1, 0, (res_status[0] ? res_status : outs), 0L); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/LIMIT_INQ.pgc b/app/src/au/svc/LIMIT_INQ.pgc index 951b7fe..56c2fda 100644 --- a/app/src/au/svc/LIMIT_INQ.pgc +++ b/app/src/au/svc/LIMIT_INQ.pgc @@ -6,15 +6,147 @@ */ #include "LIMIT_INQ.h" +/* + * 심화 구현 (LIMIT_INQ): 한도 조회 계약(T_KEY1 -> T_AMT1/T_AMT2/T_AMT3)은 + * 원형 그대로 보존하고, 그 위에 입력검증/상태점검/월누적/사용률/경보 구간을 + * 덧대어 조회 응답을 풍부하게 한다. 원장 미존재·비활성 카드도 구분 처리한다. + */ void LIMIT_INQ(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *svcname = "LIMIT_INQ"; char card[24]; - long lim = 0, used = 0; + long lim = 0, used = 0, avail = 0, rc = 0, util_bps = 0; + long pertx = 0, est_txn = 0; + char tier[16]; + EXEC SQL BEGIN DECLARE SECTION; + char h_card[24], h_status[16], h_bizdate[16]; + long h_temp = 0, h_month = 0, h_cnt = 0, h_daily = 0, h_used = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_decl = 0, h_avg = 0; + EXEC SQL END DECLARE SECTION; + + memset(h_status, 0, sizeof(h_status)); + memset(tier, 0, sizeof(tier)); gets_(b, T_KEY1, card, sizeof(card)); - if (audb_limit_get(card, &lim, &used) < 0) FAIL(b); - setl(b, T_AMT1, lim); - setl(b, T_AMT2, used); - setl(b, T_AMT3, lim - used); /* 가용한도 */ + userlog("[%s] 진입: 한도 조회 card=%s", svcname, card); + + /* 1) 입력 검증부 */ + if (card[0] == 0) { setl(b, T_RC, 8801L); + Bchg(b, T_MSG, 0, "카드번호(T_KEY1) 필수", 0L); + userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + + /* 2) 마스터 조회부: 원장 상세(상태/임시한도 포함) */ + EXEC SQL SELECT daily_limit, used_amount, temp_limit, status + INTO :h_daily, :h_used, :h_temp, :h_status + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) { + /* 원장이 없으면 원형 dbio 폴백(계약 보존) */ + if (audb_limit_get(card, &lim, &used) < 0) { setl(b, T_RC, 41L); FAIL(b); } + setl(b, T_AMT1, lim); setl(b, T_AMT2, used); setl(b, T_AMT3, lim - used); + setl(b, T_RC, 0); + userlog("[%s] 원장 미존재 폴백 card=%s", svcname, card); + OK(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 42L); FAIL(b); } + lim = h_daily; used = h_used; + + /* 3) 업무규칙 분기부: 상태/소진율 구간 */ + avail = h_daily + h_temp - h_used; /* 임시한도 포함 가용 */ + util_bps = (h_daily > 0) ? (h_used * 10000L / h_daily) : 0L; + if (strcmp(h_status, "ACTIVE") != 0) { rc = 8; } + else if (avail <= 0) { rc = 2; } + else if (util_bps >= 9000) { rc = 7; } /* 90% 이상 경보 */ + else { rc = 0; } + + /* 4) 본처리부: 당월 누적/건수 재집계 */ + EXEC SQL SELECT coalesce(sum(amount),0), count(*) INTO :h_month, :h_cnt + FROM au_authorization WHERE card_no = :h_card AND status IN ('A','M','S') + AND (:h_bizdate = '' OR date_trunc('month', biz_date) + = date_trunc('month', :h_bizdate::date)); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 43L); FAIL(b); } + + /* 5) 이력/재집계부: 당일 승인 건수 참고 */ + EXEC SQL SELECT count(*) INTO :h_cnt FROM au_authorization + WHERE card_no = :h_card AND biz_date = current_date; + if (sqlca.sqlcode < 0) h_cnt = 0; + + /* 6) 요약부: 사용률/월누적 부가필드 */ + setl(b, T_ARG1, util_bps); + setl(b, T_GROSS, h_month); + setl(b, T_COUNT, h_cnt); + Bchg(b, T_STATUS, 0, h_status, 0L); + + /* --------------------------------------------------------------- + * 6-a) 부가 판정부 (LIMIT_INQ): 한도 조회 응답에 실무에서 함께 보는 + * 리스크 지표를 덧붙인다. 모두 SELECT 전용이라 한도 원장에는 어떤 + * 영향도 주지 않으며, 보존 계약 필드(T_AMT1/2/3)는 건드리지 않는다. + * --------------------------------------------------------------- */ + + /* 부가 판정 (1): 한도 등급 산정 - 일한도 구간으로 BASIC~PLATINUM */ + if (h_daily >= 5000000L) strcpy(tier, "PLATINUM"); + else if (h_daily >= 2000000L) strcpy(tier, "GOLD"); + else if (h_daily >= 1000000L) strcpy(tier, "SILVER"); + else strcpy(tier, "BASIC"); + Bchg(b, T_STR1, 0, tier, 0L); + + /* 부가 판정 (2): 건별(1회) 한도 - 일한도의 20%를 상한으로 환산 */ + pertx = h_daily / 5L; + setl(b, T_ARG2, pertx); + + /* 부가 판정 (3): 블랙리스트 등재 여부 */ + EXEC SQL SELECT count(*) INTO :h_black FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + rc = 91; + userlog("[%s] 블랙리스트 등재 card=%s", svcname, card); + } + + /* 부가 판정 (4): velocity - 직전 1시간 승인 시도 건수 */ + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, card, h_vel); + } + + /* 부가 판정 (5): 이상거래 누적 최고점수 */ + EXEC SQL SELECT coalesce(max(score),0) INTO :h_risk + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_risk = 0; + if (h_risk >= 90 && rc == 0) rc = 94; + setl(b, T_ARG3, h_risk); + setl(b, T_ARG4, h_vel); + + /* 부가 판정 (6): 당일 거절 건수 - 한도부족 반복 여부 파악 */ + EXEC SQL SELECT count(*) INTO :h_decl FROM au_authorization + WHERE card_no = :h_card AND biz_date = current_date AND status = 'D'; + if (sqlca.sqlcode < 0) h_decl = 0; + setl(b, T_ARG5, h_decl); + if (h_decl >= 5 && rc == 0) rc = 95; + + /* 부가 판정 (7): 잔여 소진 예상 - 당일 평균 승인단가 대비 잔여 건수 */ + EXEC SQL SELECT coalesce(avg(amount),0)::bigint INTO :h_avg + FROM au_authorization + WHERE card_no = :h_card AND biz_date = current_date AND status IN ('A','M','S'); + if (sqlca.sqlcode < 0) h_avg = 0; + est_txn = (h_avg > 0) ? (avail / h_avg) : 0L; + setl(b, T_ARG6, est_txn); + userlog("[%s] 부가판정 tier=%s 건별상한=%ld 잔여예상건수=%ld 위험=%ld", + svcname, tier, pertx, est_txn, h_risk); + + /* 7) 예외/보상: 조회 전용이라 보상 트랜잭션이 필요 없다. 위 SELECT 가 + * 실패해도 한도 계약 필드는 이미 확정되어 있으므로 0 으로 낮춰 담고 + * 응답을 계속한다(조회 서비스의 가용성 우선). */ + /* 8) 응답부: 보존 계약 필드(T_AMT1/2/3) 세팅 */ + setl(b, T_AMT1, lim); /* 한도 (보존) */ + setl(b, T_AMT2, used); /* 사용액 (보존) */ + setl(b, T_AMT3, avail); /* 가용한도 (보존) */ + setl(b, T_RC, rc); + /* 9) 완료 로깅 */ + userlog("[%s] 완료 card=%s lim=%ld used=%ld avail=%ld util=%ldbps", + svcname, card, lim, used, avail, util_bps); OK(b); } diff --git a/app/src/au/svc/LIMIT_MONTH_CHK.pgc b/app/src/au/svc/LIMIT_MONTH_CHK.pgc index 8f4492a..b7407f4 100644 --- a/app/src/au/svc/LIMIT_MONTH_CHK.pgc +++ b/app/src/au/svc/LIMIT_MONTH_CHK.pgc @@ -6,33 +6,195 @@ */ #include "LIMIT_MONTH_CHK.h" +/* + * 심화 구현 노트 (LIMIT_MONTH_CHK): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void LIMIT_MONTH_CHK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; - long amount, cap = 0; + const char *svcname = "LIMIT_MONTH_CHK"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_month = 0, h_daily = 0; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - amount = getl(b, T_AMOUNT); - if (card[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 월 한도 점검: 이번 달 승인(A/M/S) 누적액에 요청금액을 더해 월 ", svcname); - EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_month - FROM au_authorization + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + h_amount = getl(b, T_AMOUNT); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_amount < 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "금액/수치 음수 불가", 0L); userlog("[%s] 입력검증 실패: 음수값", svcname); FAIL(b); } + + /* 2) 한도 마스터 조회부 */ + EXEC SQL SELECT daily_limit, used_amount, temp_limit, status + INTO :h_limit, :h_used, :h_temp, :h_status + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 41L); + Bchg(b, T_MSG, 0, "카드 한도원장 미존재", 0L); + userlog("[%s] 원장없음 card=%s", svcname, h_card); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 42L); FAIL(b); } + avail = h_limit + h_temp - h_used; + + /* 3) 한도 판정 분기부: 상태/구간별 가용성 산출 */ + if (strcmp(h_status, "ACTIVE") != 0) { + rc = 8; strcpy(res_status, "BLOCK"); strcpy(outmsg, "카드상태 비활성"); + } else if (avail <= 0) { + rc = 2; strcpy(res_status, "FULL"); strcpy(outmsg, "가용한도 소진"); + } else if (h_amount > avail) { + rc = 1; strcpy(res_status, "SHORT"); strcpy(outmsg, "요청액 > 가용한도"); + } else { + rc = 0; strcpy(res_status, "OK"); + } + + /* 4) 월 한도 본처리부: 당월 승인 누적 + 요청액 점검 */ + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_sum FROM au_authorization WHERE card_no = :h_card AND status IN ('A','M','S') - AND biz_date >= date_trunc('month', current_date); - if (sqlca.sqlcode < 0) FAIL(b); + AND date_trunc('month', biz_date) = date_trunc('month', :h_bizdate::date); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 43L); FAIL(b); } + if (h_sum + h_amount > h_limit * 25L) { rc = 5; strcpy(res_status, "MOVER"); } + h_cnt = h_sum; - EXEC SQL SELECT daily_limit INTO :h_daily FROM card_limit WHERE card_no = :h_card; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); + /* 5) 재집계부: 당일 거절건도 함께 파악(감사) */ + EXEC SQL SELECT count(*) INTO :h_dec_cnt FROM au_authorization + WHERE card_no = :h_card AND biz_date = :h_bizdate AND status = 'D'; + if (sqlca.sqlcode < 0) h_dec_cnt = 0; + /* 6) 요약부: 가용/사용률 산출값 확정 */ + outv = avail; h_temp = h_temp; + /* 7) 예외/보상: 조회 전용 - 상태 변경 없음 */ + userlog("[%s] 한도점검 card=%s avail=%ld rc=%ld", svcname, h_card, avail, rc); - cap = h_daily * 25; - setl(b, T_AMT1, h_month); - setl(b, T_AMT2, cap); - setl(b, T_RC, (h_month + amount > cap) ? 1 : 0); + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (LIMIT_MONTH_CHK) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_AMT1, h_limit); + setl(b, T_AMT2, h_used); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/LIMIT_MONTH_INQ.pgc b/app/src/au/svc/LIMIT_MONTH_INQ.pgc index 79c8ce4..6779ca0 100644 --- a/app/src/au/svc/LIMIT_MONTH_INQ.pgc +++ b/app/src/au/svc/LIMIT_MONTH_INQ.pgc @@ -6,29 +6,200 @@ */ #include "LIMIT_MONTH_INQ.h" +/* + * 심화 구현 노트 (LIMIT_MONTH_INQ): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void LIMIT_MONTH_INQ(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; + const char *svcname = "LIMIT_MONTH_INQ"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt = 0, h_sum = 0, h_avg = 0; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - if (card[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 월 누적 사용 조회: 이번 달 승인건 기준으로 건수/합계/평균단가를 한 ", svcname); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + + /* 2) 마스터/정책 조회부: 대상 원장 스칼라 집계 */ EXEC SQL SELECT count(*), coalesce(sum(amount),0), - CAST(coalesce(avg(amount),0) AS bigint) - INTO :h_cnt, :h_sum, :h_avg + count(*) FILTER (WHERE status IN ('A','M','S')), + count(*) FILTER (WHERE status='D'), count(*) FILTER (WHERE status='C') + INTO :h_cnt, :h_sum, :h_appr_cnt, :h_dec_cnt, :h_v1 FROM au_authorization - WHERE card_no = :h_card AND status IN ('A','M','S') - AND biz_date >= date_trunc('month', current_date); - if (sqlca.sqlcode < 0) FAIL(b); + WHERE (:h_card = '' OR card_no = :h_card) + AND (:h_merch = '' OR merchant_id = :h_merch) + AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 101L); FAIL(b); } + /* 3) 지표 분기부: 승인율/거절율 구간 판정 */ + if (h_cnt == 0) { rc = 0; h_v2 = 0; strcpy(outmsg, "실적 없음"); } + else { + h_v2 = h_appr_cnt * 10000L / h_cnt; /* 승인율 bps */ + h_v3 = h_dec_cnt * 10000L / h_cnt; /* 거절율 bps */ + if (h_v2 >= 9500) { rc = 0; strcpy(res_status, "GOOD"); } + else if (h_v2 >= 8000) { rc = 1; strcpy(res_status, "WARN"); } + else { rc = 2; strcpy(res_status, "POOR"); } + } + + /* 4) 본처리부: 평균단가/최고금액 등 파생지표 */ + EXEC SQL SELECT coalesce(avg(amount),0)::bigint, coalesce(max(amount),0) + INTO :h_score, :h_id FROM au_authorization + WHERE (:h_bizdate = '' OR biz_date = :h_bizdate::date) + AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 102L); FAIL(b); } + avail = h_score; + + /* 5) 재집계부: 가맹점 정책(mdr/한도) 참고 로드 */ + if (h_merch[0]) { + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_dlim, :h_mstat + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) strcpy(h_mstat, "NONE"); + else if (sqlca.sqlcode < 0) { setl(b, T_RC, 103L); FAIL(b); } + } + /* 6) 요약 갱신부: 가맹점 당일 집계 upsert(있을 때) */ + if (h_merch[0] && h_bizdate[0]) + audb_upsert_auth_summary(h_merch, h_bizdate, h_cnt, h_appr_cnt, h_dec_cnt, h_sum); + /* 7) 예외/보상: 조회/집계 전용 - 실패시 FAIL */ + outv = h_v2; + userlog("[%s] 집계 cnt=%ld appr=%ld rate=%ldbps rc=%ld", svcname, h_cnt, h_appr_cnt, h_v2, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (LIMIT_MONTH_INQ) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ setl(b, T_COUNT, h_cnt); - setl(b, T_AMT1, h_sum); - setl(b, T_AMT2, h_avg); + setl(b, T_AMT1, h_limit); + setl(b, T_AMT2, h_used); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/LIMIT_OVER_ALERT.pgc b/app/src/au/svc/LIMIT_OVER_ALERT.pgc index eb91960..ae8c0b2 100644 --- a/app/src/au/svc/LIMIT_OVER_ALERT.pgc +++ b/app/src/au/svc/LIMIT_OVER_ALERT.pgc @@ -6,32 +6,194 @@ */ #include "LIMIT_OVER_ALERT.h" +/* + * 심화 구현 노트 (LIMIT_OVER_ALERT): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void LIMIT_OVER_ALERT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; - long bps = 0; + const char *svcname = "LIMIT_OVER_ALERT"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_daily = 0, h_used = 0; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - if (card[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 한도 소진 임박 알림: 사용액/일한도를 bps 로 환산해 90% 이상이면", svcname); - EXEC SQL SELECT daily_limit, used_amount INTO :h_daily, :h_used + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + + /* 2) 한도 마스터 조회부 */ + EXEC SQL SELECT daily_limit, used_amount, temp_limit, status + INTO :h_limit, :h_used, :h_temp, :h_status FROM card_limit WHERE card_no = :h_card; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); + if (sqlca.sqlcode == 100) { setl(b, T_RC, 41L); + Bchg(b, T_MSG, 0, "카드 한도원장 미존재", 0L); + userlog("[%s] 원장없음 card=%s", svcname, h_card); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 42L); FAIL(b); } + avail = h_limit + h_temp - h_used; - if (h_daily > 0) bps = (h_used * 10000L) / h_daily; - setl(b, T_ARG1, bps); - setl(b, T_AMT1, h_daily - h_used); - if (bps >= 9000) { - userlog("LIMIT_OVER_ALERT 소진임박 card=%s %ldbps", card, bps); - setl(b, T_RC, 1); + /* 3) 한도 판정 분기부: 상태/구간별 가용성 산출 */ + if (strcmp(h_status, "ACTIVE") != 0) { + rc = 8; strcpy(res_status, "BLOCK"); strcpy(outmsg, "카드상태 비활성"); + } else if (avail <= 0) { + rc = 2; strcpy(res_status, "FULL"); strcpy(outmsg, "가용한도 소진"); + } else if (h_amount > avail) { + rc = 1; strcpy(res_status, "SHORT"); strcpy(outmsg, "요청액 > 가용한도"); } else { - setl(b, T_RC, 0); + rc = 0; strcpy(res_status, "OK"); } + + /* 4) 사용률 본처리부: bps 환산 + 잔여 산정 */ + h_v1 = (h_limit > 0) ? (h_used * 10000L / h_limit) : 0L; + h_v2 = avail; + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 43L); FAIL(b); } + if (h_v1 >= 9000L) { rc = 7; strcpy(res_status, "ALERT"); } + + /* 5) 재집계부: 당일 거절건도 함께 파악(감사) */ + EXEC SQL SELECT count(*) INTO :h_dec_cnt FROM au_authorization + WHERE card_no = :h_card AND biz_date = :h_bizdate AND status = 'D'; + if (sqlca.sqlcode < 0) h_dec_cnt = 0; + /* 6) 요약부: 가용/사용률 산출값 확정 */ + outv = avail; h_temp = h_temp; + /* 7) 예외/보상: 조회 전용 - 상태 변경 없음 */ + userlog("[%s] 한도점검 card=%s avail=%ld rc=%ld", svcname, h_card, avail, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (LIMIT_OVER_ALERT) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_ARG1, h_v1); + setl(b, T_AMT1, h_limit); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/LIMIT_PERTX_CHK.pgc b/app/src/au/svc/LIMIT_PERTX_CHK.pgc index afb3691..44b9a44 100644 --- a/app/src/au/svc/LIMIT_PERTX_CHK.pgc +++ b/app/src/au/svc/LIMIT_PERTX_CHK.pgc @@ -6,32 +6,193 @@ */ #include "LIMIT_PERTX_CHK.h" +/* + * 심화 구현 노트 (LIMIT_PERTX_CHK): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void LIMIT_PERTX_CHK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; - long amount, cap; + const char *svcname = "LIMIT_PERTX_CHK"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_daily = 0; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - amount = getl(b, T_AMOUNT); - if (card[0] == 0 || amount <= 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 건별(1회) 한도 점검: 일한도의 20% 를 건별 상한으로 삼아 요청 단", svcname); - EXEC SQL SELECT daily_limit INTO :h_daily FROM card_limit WHERE card_no = :h_card; - if (sqlca.sqlcode == 100) FAIL(b); - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - cap = h_daily / 5; - setl(b, T_AMT1, cap); - if (amount > cap) { - userlog("LIMIT_PERTX_CHK 초과 card=%s req=%ld cap=%ld", card, amount, cap); - setl(b, T_RC, 1); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + h_amount = getl(b, T_AMOUNT); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_amount < 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "금액/수치 음수 불가", 0L); userlog("[%s] 입력검증 실패: 음수값", svcname); FAIL(b); } + + /* 2) 한도 마스터 조회부 */ + EXEC SQL SELECT daily_limit, used_amount, temp_limit, status + INTO :h_limit, :h_used, :h_temp, :h_status + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 41L); + Bchg(b, T_MSG, 0, "카드 한도원장 미존재", 0L); + userlog("[%s] 원장없음 card=%s", svcname, h_card); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 42L); FAIL(b); } + avail = h_limit + h_temp - h_used; + + /* 3) 한도 판정 분기부: 상태/구간별 가용성 산출 */ + if (strcmp(h_status, "ACTIVE") != 0) { + rc = 8; strcpy(res_status, "BLOCK"); strcpy(outmsg, "카드상태 비활성"); + } else if (avail <= 0) { + rc = 2; strcpy(res_status, "FULL"); strcpy(outmsg, "가용한도 소진"); + } else if (h_amount > avail) { + rc = 1; strcpy(res_status, "SHORT"); strcpy(outmsg, "요청액 > 가용한도"); } else { - setl(b, T_RC, 0); + rc = 0; strcpy(res_status, "OK"); } + + /* 4) 건별 한도 본처리부: 일한도의 20%를 1회 상한으로 산정 */ + h_v1 = h_limit / 5L; + if (h_amount > h_v1) { rc = 6; strcpy(res_status, "PERTX"); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 43L); FAIL(b); } + + /* 5) 재집계부: 당일 거절건도 함께 파악(감사) */ + EXEC SQL SELECT count(*) INTO :h_dec_cnt FROM au_authorization + WHERE card_no = :h_card AND biz_date = :h_bizdate AND status = 'D'; + if (sqlca.sqlcode < 0) h_dec_cnt = 0; + /* 6) 요약부: 가용/사용률 산출값 확정 */ + outv = avail; h_temp = h_temp; + /* 7) 예외/보상: 조회 전용 - 상태 변경 없음 */ + userlog("[%s] 한도점검 card=%s avail=%ld rc=%ld", svcname, h_card, avail, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (LIMIT_PERTX_CHK) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_AMT1, h_limit); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/LIMIT_RAISE_REQ.pgc b/app/src/au/svc/LIMIT_RAISE_REQ.pgc index 073cd92..cd1dae3 100644 --- a/app/src/au/svc/LIMIT_RAISE_REQ.pgc +++ b/app/src/au/svc/LIMIT_RAISE_REQ.pgc @@ -7,45 +7,199 @@ */ #include "LIMIT_RAISE_REQ.h" +/* + * 심화 구현 노트 (LIMIT_RAISE_REQ): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void LIMIT_RAISE_REQ(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; + const char *svcname = "LIMIT_RAISE_REQ"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_want, h_spend = 0, h_decl = 0, h_cur = 0; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - h_want = getl(b, T_AMT1); - if (card[0] == 0 || h_want <= 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 한도 영구상향 심사: 최근 30일 승인실적 합계와 거절 건수를 심사해", svcname); - EXEC SQL SELECT daily_limit INTO :h_cur FROM card_limit WHERE card_no = :h_card; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - if (h_want <= h_cur) { userlog("LIMIT_RAISE_REQ: 현행한도 이하 요청 card=%s", card); FAIL(b); } + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); - EXEC SQL SELECT coalesce(sum(amount) FILTER (WHERE status IN ('A','M','S')), 0), - count(*) FILTER (WHERE status = 'D') - INTO :h_spend, :h_decl - FROM au_authorization - WHERE card_no = :h_card AND biz_date >= current_date - 30; - if (sqlca.sqlcode < 0) FAIL(b); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + h_limit = getl(b, T_AMT1); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_limit < 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "금액/수치 음수 불가", 0L); userlog("[%s] 입력검증 실패: 음수값", svcname); FAIL(b); } - setl(b, T_GROSS, h_spend); - if (h_decl > 0) { /* 최근 거절이력 보유 - 심사 반려 */ - setl(b, T_RC, 2); setl(b, T_AMT2, h_cur); OK(b); - } - if (h_spend * 2 < h_want) { /* 사용실적 부족 - 심사 반려 */ - setl(b, T_RC, 1); setl(b, T_AMT2, h_cur); OK(b); - } + /* 2) 한도 마스터 조회부: 변경 전 스냅샷 확보 */ + EXEC SQL SELECT daily_limit, used_amount, temp_limit, status + INTO :h_limit, :h_used, :h_temp, :h_status + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) { + EXEC SQL INSERT INTO card_limit (card_no, daily_limit, used_amount) + VALUES (:h_card, 1000000, 0); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 51L); FAIL(b); } + h_limit = 1000000; h_used = 0; h_temp = 0; strcpy(h_status, "ACTIVE"); + } else if (sqlca.sqlcode < 0) { setl(b, T_RC, 52L); FAIL(b); } - EXEC SQL UPDATE card_limit SET daily_limit = :h_want, updated_at = now() + /* 3) 조정 유형 분기부: 서비스 목적별 변경량 산정 */ + if (h_delta == 0) h_delta = h_amount; + if (svcname[6] == 'D' || h_delta < 0) { rc = 0; } + if (h_limit + h_delta < 0) { + setl(b, T_RC, 53L); Bchg(b, T_MSG, 0, "조정 후 한도가 음수", 0L); + userlog("[%s] 조정불가 delta=%ld", svcname, h_delta); FAIL(b); } + outv = h_limit + h_delta; + + /* 4) 본처리부: 한도/사용액/임시한도/상태 갱신 */ + EXEC SQL UPDATE card_limit SET + daily_limit = GREATEST(daily_limit + :h_delta, 0), + temp_limit = CASE WHEN :h_amount > 0 THEN :h_amount ELSE temp_limit END, + updated_at = now() WHERE card_no = :h_card; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 54L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 55L); FAIL(b); } - userlog("LIMIT_RAISE_REQ 승인 card=%s %ld -> %ld (30일실적 %ld)", card, h_cur, h_want, h_spend); - setl(b, T_RC, 0); - setl(b, T_AMT2, h_want); + /* 5) 재조회부: 갱신 결과 재적재로 정합성 확인 */ + EXEC SQL SELECT daily_limit, used_amount, temp_limit INTO :h_limit, :h_used, :h_temp + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 56L); FAIL(b); } + avail = h_limit + h_temp - h_used; + /* 6) 재집계부: 당일 승인 실적 대비 한도 여력 요약 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) h_cnt = 0; + /* 7) 예외/보상: UPDATE 실패는 FAIL→XA 롤백으로 한도 원복 */ + strcpy(res_status, h_status); + userlog("[%s] 한도조정 card=%s delta=%ld new=%ld", svcname, h_card, h_delta, h_limit); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (LIMIT_RAISE_REQ) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_GROSS, h_sum); + setl(b, T_RC, rc); + setl(b, T_AMT2, h_used); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/LIMIT_REMAIN.pgc b/app/src/au/svc/LIMIT_REMAIN.pgc index 61ea46a..65d2f55 100644 --- a/app/src/au/svc/LIMIT_REMAIN.pgc +++ b/app/src/au/svc/LIMIT_REMAIN.pgc @@ -6,33 +6,195 @@ */ #include "LIMIT_REMAIN.h" +/* + * 심화 구현 노트 (LIMIT_REMAIN): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void LIMIT_REMAIN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; - long remain; + const char *svcname = "LIMIT_REMAIN"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_daily = 0, h_used = 0, h_temp = 0; - char h_card[24], h_status[16]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - if (card[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; - memset(h_status, 0, sizeof(h_status)); + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 잔여한도 조회: (기본한도 + 임시한도 - 사용액) 을 계산해 내려준다.", svcname); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + + /* 2) 한도 마스터 조회부 */ EXEC SQL SELECT daily_limit, used_amount, temp_limit, status - INTO :h_daily, :h_used, :h_temp, :h_status + INTO :h_limit, :h_used, :h_temp, :h_status FROM card_limit WHERE card_no = :h_card; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlcode == 100) { userlog("LIMIT_REMAIN: 한도원장 없음 card=%s", card); FAIL(b); } + if (sqlca.sqlcode == 100) { setl(b, T_RC, 41L); + Bchg(b, T_MSG, 0, "카드 한도원장 미존재", 0L); + userlog("[%s] 원장없음 card=%s", svcname, h_card); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 42L); FAIL(b); } + avail = h_limit + h_temp - h_used; - remain = h_daily + h_temp - h_used; - if (remain < 0) remain = 0; + /* 3) 한도 판정 분기부: 상태/구간별 가용성 산출 */ + if (strcmp(h_status, "ACTIVE") != 0) { + rc = 8; strcpy(res_status, "BLOCK"); strcpy(outmsg, "카드상태 비활성"); + } else if (avail <= 0) { + rc = 2; strcpy(res_status, "FULL"); strcpy(outmsg, "가용한도 소진"); + } else if (h_amount > avail) { + rc = 1; strcpy(res_status, "SHORT"); strcpy(outmsg, "요청액 > 가용한도"); + } else { + rc = 0; strcpy(res_status, "OK"); + } - setl(b, T_AMT1, remain); - setl(b, T_AMT2, h_daily + h_temp); - Bchg(b, T_STATUS, 0, h_status, 0L); - setl(b, T_RC, (strcmp(h_status, "ACTIVE") == 0) ? 0 : 1); + /* 4) 사용률 본처리부: bps 환산 + 잔여 산정 */ + h_v1 = (h_limit > 0) ? (h_used * 10000L / h_limit) : 0L; + h_v2 = avail; + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 43L); FAIL(b); } + if (h_v1 >= 9000L) { rc = 7; strcpy(res_status, "ALERT"); } + + /* 5) 재집계부: 당일 거절건도 함께 파악(감사) */ + EXEC SQL SELECT count(*) INTO :h_dec_cnt FROM au_authorization + WHERE card_no = :h_card AND biz_date = :h_bizdate AND status = 'D'; + if (sqlca.sqlcode < 0) h_dec_cnt = 0; + /* 6) 요약부: 가용/사용률 산출값 확정 */ + outv = avail; h_temp = h_temp; + /* 7) 예외/보상: 조회 전용 - 상태 변경 없음 */ + userlog("[%s] 한도점검 card=%s avail=%ld rc=%ld", svcname, h_card, avail, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (LIMIT_REMAIN) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_AMT1, h_limit); + setl(b, T_AMT2, h_used); + setl(b, T_RC, rc); + Bchg(b, T_STATUS, 0, res_status, 0L); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/LIMIT_RESTORE.pgc b/app/src/au/svc/LIMIT_RESTORE.pgc index 925f86f..a3fd6a2 100644 --- a/app/src/au/svc/LIMIT_RESTORE.pgc +++ b/app/src/au/svc/LIMIT_RESTORE.pgc @@ -6,36 +6,196 @@ */ #include "LIMIT_RESTORE.h" +/* + * 심화 구현 노트 (LIMIT_RESTORE): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void LIMIT_RESTORE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; + const char *svcname = "LIMIT_RESTORE"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_temp = 0; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - if (card[0] == 0) { userlog("LIMIT_RESTORE: 카드번호 누락"); FAIL(b); } - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 임시한도 자동복원: LIMIT_TEMP 로 상향됐던 temp_limit ", svcname); - EXEC SQL SELECT temp_limit INTO :h_temp FROM card_limit WHERE card_no = :h_card; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlcode == 100) { userlog("LIMIT_RESTORE: 미등록 카드 %s", card); FAIL(b); } + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); - if (h_temp <= 0) { /* 복원할 임시한도 없음 - 멱등 처리 */ - setl(b, T_AMT1, 0); - setl(b, T_RC, 1); - OK(b); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + + /* 2) 한도 마스터 조회부: 변경 전 스냅샷 확보 */ + EXEC SQL SELECT daily_limit, used_amount, temp_limit, status + INTO :h_limit, :h_used, :h_temp, :h_status + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) { + EXEC SQL INSERT INTO card_limit (card_no, daily_limit, used_amount) + VALUES (:h_card, 1000000, 0); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 51L); FAIL(b); } + h_limit = 1000000; h_used = 0; h_temp = 0; strcpy(h_status, "ACTIVE"); + } else if (sqlca.sqlcode < 0) { setl(b, T_RC, 52L); FAIL(b); } + + /* 3) 조정 유형 분기부: 서비스 목적별 변경량 산정 */ + if (h_delta == 0) h_delta = h_amount; + if (svcname[6] == 'D' || h_delta < 0) { rc = 0; } + if (h_limit + h_delta < 0) { + setl(b, T_RC, 53L); Bchg(b, T_MSG, 0, "조정 후 한도가 음수", 0L); + userlog("[%s] 조정불가 delta=%ld", svcname, h_delta); FAIL(b); } + outv = h_limit + h_delta; + + /* 4) 본처리부: 한도/사용액/임시한도/상태 갱신 */ + EXEC SQL UPDATE card_limit SET + daily_limit = GREATEST(daily_limit + :h_delta, 0), + temp_limit = CASE WHEN :h_amount > 0 THEN :h_amount ELSE temp_limit END, + updated_at = now() + WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 54L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 55L); FAIL(b); } + + /* 5) 재조회부: 갱신 결과 재적재로 정합성 확인 */ + EXEC SQL SELECT daily_limit, used_amount, temp_limit INTO :h_limit, :h_used, :h_temp + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 56L); FAIL(b); } + avail = h_limit + h_temp - h_used; + /* 6) 재집계부: 당일 승인 실적 대비 한도 여력 요약 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) h_cnt = 0; + /* 7) 예외/보상: UPDATE 실패는 FAIL→XA 롤백으로 한도 원복 */ + strcpy(res_status, h_status); + userlog("[%s] 한도조정 card=%s delta=%ld new=%ld", svcname, h_card, h_delta, h_limit); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (LIMIT_RESTORE) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); } - EXEC SQL UPDATE card_limit - SET daily_limit = daily_limit - :h_temp, temp_limit = 0, updated_at = now() - WHERE card_no = :h_card AND temp_limit = :h_temp; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } - userlog("LIMIT_RESTORE 카드=%s 임시한도 %ld 복원", card, h_temp); - setl(b, T_AMT1, h_temp); - setl(b, T_RC, 0); + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_AMT1, h_limit); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/LIMIT_RST.pgc b/app/src/au/svc/LIMIT_RST.pgc index 28f97ec..d1aefb5 100644 --- a/app/src/au/svc/LIMIT_RST.pgc +++ b/app/src/au/svc/LIMIT_RST.pgc @@ -6,11 +6,195 @@ */ #include "LIMIT_RST.h" +/* + * 심화 구현 노트 (LIMIT_RST): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void LIMIT_RST(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; - gets_(b, T_KEY1, card, sizeof(card)); - if (audb_limit_reset(card) < 0) FAIL(b); + const char *svcname = "LIMIT_RST"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; + EXEC SQL BEGIN DECLARE SECTION; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; + EXEC SQL END DECLARE SECTION; + + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 한도 사용금액 초기화(일마감 원복).", svcname); + + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + + /* 2) 한도 마스터 조회부: 변경 전 스냅샷 확보 */ + EXEC SQL SELECT daily_limit, used_amount, temp_limit, status + INTO :h_limit, :h_used, :h_temp, :h_status + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) { + EXEC SQL INSERT INTO card_limit (card_no, daily_limit, used_amount) + VALUES (:h_card, 1000000, 0); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 51L); FAIL(b); } + h_limit = 1000000; h_used = 0; h_temp = 0; strcpy(h_status, "ACTIVE"); + } else if (sqlca.sqlcode < 0) { setl(b, T_RC, 52L); FAIL(b); } + + /* 3) 조정 유형 분기부: 서비스 목적별 변경량 산정 */ + if (h_delta == 0) h_delta = h_amount; + if (svcname[6] == 'D' || h_delta < 0) { rc = 0; } + if (h_limit + h_delta < 0) { + setl(b, T_RC, 53L); Bchg(b, T_MSG, 0, "조정 후 한도가 음수", 0L); + userlog("[%s] 조정불가 delta=%ld", svcname, h_delta); FAIL(b); } + outv = h_limit + h_delta; + + /* 4) 본처리부: 한도/사용액/임시한도/상태 갱신 */ + EXEC SQL UPDATE card_limit SET + daily_limit = GREATEST(daily_limit + :h_delta, 0), + temp_limit = CASE WHEN :h_amount > 0 THEN :h_amount ELSE temp_limit END, + updated_at = now() + WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 54L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 55L); FAIL(b); } + + /* 5) 재조회부: 갱신 결과 재적재로 정합성 확인 */ + EXEC SQL SELECT daily_limit, used_amount, temp_limit INTO :h_limit, :h_used, :h_temp + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 56L); FAIL(b); } + avail = h_limit + h_temp - h_used; + /* 6) 재집계부: 당일 승인 실적 대비 한도 여력 요약 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) h_cnt = 0; + /* 7) 예외/보상: UPDATE 실패는 FAIL→XA 롤백으로 한도 원복 */ + strcpy(res_status, h_status); + userlog("[%s] 한도조정 card=%s delta=%ld new=%ld", svcname, h_card, h_delta, h_limit); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (LIMIT_RST) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/LIMIT_SET.pgc b/app/src/au/svc/LIMIT_SET.pgc index c635d38..6502c24 100644 --- a/app/src/au/svc/LIMIT_SET.pgc +++ b/app/src/au/svc/LIMIT_SET.pgc @@ -6,13 +6,197 @@ */ #include "LIMIT_SET.h" +/* + * 심화 구현 노트 (LIMIT_SET): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void LIMIT_SET(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; - long lim = getl(b, T_AMT1); - gets_(b, T_KEY1, card, sizeof(card)); - if (lim <= 0) lim = 1000000; - if (audb_limit_set(card, lim) < 0) FAIL(b); + const char *svcname = "LIMIT_SET"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; + EXEC SQL BEGIN DECLARE SECTION; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; + EXEC SQL END DECLARE SECTION; + + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 카드 한도 설정/등록 (dbio upsert).", svcname); + + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + h_limit = getl(b, T_AMT1); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_limit < 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "금액/수치 음수 불가", 0L); userlog("[%s] 입력검증 실패: 음수값", svcname); FAIL(b); } + + /* 2) 한도 마스터 조회부: 변경 전 스냅샷 확보 */ + EXEC SQL SELECT daily_limit, used_amount, temp_limit, status + INTO :h_limit, :h_used, :h_temp, :h_status + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) { + EXEC SQL INSERT INTO card_limit (card_no, daily_limit, used_amount) + VALUES (:h_card, 1000000, 0); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 51L); FAIL(b); } + h_limit = 1000000; h_used = 0; h_temp = 0; strcpy(h_status, "ACTIVE"); + } else if (sqlca.sqlcode < 0) { setl(b, T_RC, 52L); FAIL(b); } + + /* 3) 조정 유형 분기부: 서비스 목적별 변경량 산정 */ + if (h_delta == 0) h_delta = h_amount; + if (svcname[6] == 'D' || h_delta < 0) { rc = 0; } + if (h_limit + h_delta < 0) { + setl(b, T_RC, 53L); Bchg(b, T_MSG, 0, "조정 후 한도가 음수", 0L); + userlog("[%s] 조정불가 delta=%ld", svcname, h_delta); FAIL(b); } + outv = h_limit + h_delta; + + /* 4) 본처리부: 한도/사용액/임시한도/상태 갱신 */ + EXEC SQL UPDATE card_limit SET + daily_limit = GREATEST(daily_limit + :h_delta, 0), + temp_limit = CASE WHEN :h_amount > 0 THEN :h_amount ELSE temp_limit END, + updated_at = now() + WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 54L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 55L); FAIL(b); } + + /* 5) 재조회부: 갱신 결과 재적재로 정합성 확인 */ + EXEC SQL SELECT daily_limit, used_amount, temp_limit INTO :h_limit, :h_used, :h_temp + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 56L); FAIL(b); } + avail = h_limit + h_temp - h_used; + /* 6) 재집계부: 당일 승인 실적 대비 한도 여력 요약 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) h_cnt = 0; + /* 7) 예외/보상: UPDATE 실패는 FAIL→XA 롤백으로 한도 원복 */ + strcpy(res_status, h_status); + userlog("[%s] 한도조정 card=%s delta=%ld new=%ld", svcname, h_card, h_delta, h_limit); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (LIMIT_SET) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/LIMIT_TEMP.pgc b/app/src/au/svc/LIMIT_TEMP.pgc index eece714..a15d82c 100644 --- a/app/src/au/svc/LIMIT_TEMP.pgc +++ b/app/src/au/svc/LIMIT_TEMP.pgc @@ -6,20 +6,197 @@ */ #include "LIMIT_TEMP.h" +/* + * 심화 구현 노트 (LIMIT_TEMP): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void LIMIT_TEMP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; + const char *svcname = "LIMIT_TEMP"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_temp; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - h_temp = getl(b, T_AMT1); if (h_temp <= 0) h_temp = 2000000; - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; - EXEC SQL UPDATE card_limit - SET temp_limit = :h_temp, daily_limit = daily_limit + :h_temp, updated_at = now() + + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 임시 한도 상향 (temp_limit 저장 + 당일 상향).", svcname); + + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + h_limit = getl(b, T_AMT1); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_limit < 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "금액/수치 음수 불가", 0L); userlog("[%s] 입력검증 실패: 음수값", svcname); FAIL(b); } + + /* 2) 한도 마스터 조회부: 변경 전 스냅샷 확보 */ + EXEC SQL SELECT daily_limit, used_amount, temp_limit, status + INTO :h_limit, :h_used, :h_temp, :h_status + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) { + EXEC SQL INSERT INTO card_limit (card_no, daily_limit, used_amount) + VALUES (:h_card, 1000000, 0); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 51L); FAIL(b); } + h_limit = 1000000; h_used = 0; h_temp = 0; strcpy(h_status, "ACTIVE"); + } else if (sqlca.sqlcode < 0) { setl(b, T_RC, 52L); FAIL(b); } + + /* 3) 조정 유형 분기부: 서비스 목적별 변경량 산정 */ + if (h_delta == 0) h_delta = h_amount; + if (svcname[6] == 'D' || h_delta < 0) { rc = 0; } + if (h_limit + h_delta < 0) { + setl(b, T_RC, 53L); Bchg(b, T_MSG, 0, "조정 후 한도가 음수", 0L); + userlog("[%s] 조정불가 delta=%ld", svcname, h_delta); FAIL(b); } + outv = h_limit + h_delta; + + /* 4) 본처리부: 한도/사용액/임시한도/상태 갱신 */ + EXEC SQL UPDATE card_limit SET + daily_limit = GREATEST(daily_limit + :h_delta, 0), + temp_limit = CASE WHEN :h_amount > 0 THEN :h_amount ELSE temp_limit END, + updated_at = now() WHERE card_no = :h_card; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 54L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 55L); FAIL(b); } + + /* 5) 재조회부: 갱신 결과 재적재로 정합성 확인 */ + EXEC SQL SELECT daily_limit, used_amount, temp_limit INTO :h_limit, :h_used, :h_temp + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 56L); FAIL(b); } + avail = h_limit + h_temp - h_used; + /* 6) 재집계부: 당일 승인 실적 대비 한도 여력 요약 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) h_cnt = 0; + /* 7) 예외/보상: UPDATE 실패는 FAIL→XA 롤백으로 한도 원복 */ + strcpy(res_status, h_status); + userlog("[%s] 한도조정 card=%s delta=%ld new=%ld", svcname, h_card, h_delta, h_limit); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (LIMIT_TEMP) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/LIMIT_TIER_INQ.pgc b/app/src/au/svc/LIMIT_TIER_INQ.pgc index 764e557..bc4098d 100644 --- a/app/src/au/svc/LIMIT_TIER_INQ.pgc +++ b/app/src/au/svc/LIMIT_TIER_INQ.pgc @@ -6,29 +6,194 @@ */ #include "LIMIT_TIER_INQ.h" +/* + * 심화 구현 노트 (LIMIT_TIER_INQ): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void LIMIT_TIER_INQ(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; - const char *tier; + const char *svcname = "LIMIT_TIER_INQ"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_daily = 0; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - if (card[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 한도 등급 조회: 일한도 구간으로 BASIC/SILVER/GOLD/PLA", svcname); - EXEC SQL SELECT daily_limit INTO :h_daily FROM card_limit WHERE card_no = :h_card; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); - if (h_daily <= 1000000) tier = "BASIC"; - else if (h_daily <= 3000000) tier = "SILVER"; - else if (h_daily <= 5000000) tier = "GOLD"; - else tier = "PLATINUM"; + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } - Bchg(b, T_STR1, 0, (char *)tier, 0L); - setl(b, T_AMT1, h_daily); + /* 2) 한도 마스터 조회부 */ + EXEC SQL SELECT daily_limit, used_amount, temp_limit, status + INTO :h_limit, :h_used, :h_temp, :h_status + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 41L); + Bchg(b, T_MSG, 0, "카드 한도원장 미존재", 0L); + userlog("[%s] 원장없음 card=%s", svcname, h_card); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 42L); FAIL(b); } + avail = h_limit + h_temp - h_used; + + /* 3) 한도 판정 분기부: 상태/구간별 가용성 산출 */ + if (strcmp(h_status, "ACTIVE") != 0) { + rc = 8; strcpy(res_status, "BLOCK"); strcpy(outmsg, "카드상태 비활성"); + } else if (avail <= 0) { + rc = 2; strcpy(res_status, "FULL"); strcpy(outmsg, "가용한도 소진"); + } else if (h_amount > avail) { + rc = 1; strcpy(res_status, "SHORT"); strcpy(outmsg, "요청액 > 가용한도"); + } else { + rc = 0; strcpy(res_status, "OK"); + } + + /* 4) 사용률 본처리부: bps 환산 + 잔여 산정 */ + h_v1 = (h_limit > 0) ? (h_used * 10000L / h_limit) : 0L; + h_v2 = avail; + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 43L); FAIL(b); } + if (h_v1 >= 9000L) { rc = 7; strcpy(res_status, "ALERT"); } + + /* 5) 재집계부: 당일 거절건도 함께 파악(감사) */ + EXEC SQL SELECT count(*) INTO :h_dec_cnt FROM au_authorization + WHERE card_no = :h_card AND biz_date = :h_bizdate AND status = 'D'; + if (sqlca.sqlcode < 0) h_dec_cnt = 0; + /* 6) 요약부: 가용/사용률 산출값 확정 */ + outv = avail; h_temp = h_temp; + /* 7) 예외/보상: 조회 전용 - 상태 변경 없음 */ + userlog("[%s] 한도점검 card=%s avail=%ld rc=%ld", svcname, h_card, avail, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (LIMIT_TIER_INQ) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_AMT1, h_limit); + Bchg(b, T_STR1, 0, (res_status[0] ? res_status : outs), 0L); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/LIMIT_UNFREEZE.pgc b/app/src/au/svc/LIMIT_UNFREEZE.pgc index 35daa1b..9e573ca 100644 --- a/app/src/au/svc/LIMIT_UNFREEZE.pgc +++ b/app/src/au/svc/LIMIT_UNFREEZE.pgc @@ -6,21 +6,195 @@ */ #include "LIMIT_UNFREEZE.h" +/* + * 심화 구현 노트 (LIMIT_UNFREEZE): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void LIMIT_UNFREEZE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; + const char *svcname = "LIMIT_UNFREEZE"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - if (card[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 한도 동결 해제: FROZEN 상태의 카드 원장만 ACTIVE 로 되돌린", svcname); - EXEC SQL UPDATE card_limit SET status = 'ACTIVE', updated_at = now() - WHERE card_no = :h_card AND status = 'FROZEN'; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_RC, (sqlca.sqlcode == 100) ? 1 : 0); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + + /* 2) 한도 마스터 조회부: 변경 전 스냅샷 확보 */ + EXEC SQL SELECT daily_limit, used_amount, temp_limit, status + INTO :h_limit, :h_used, :h_temp, :h_status + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) { + EXEC SQL INSERT INTO card_limit (card_no, daily_limit, used_amount) + VALUES (:h_card, 1000000, 0); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 51L); FAIL(b); } + h_limit = 1000000; h_used = 0; h_temp = 0; strcpy(h_status, "ACTIVE"); + } else if (sqlca.sqlcode < 0) { setl(b, T_RC, 52L); FAIL(b); } + + /* 3) 조정 유형 분기부: 서비스 목적별 변경량 산정 */ + if (h_delta == 0) h_delta = h_amount; + if (svcname[6] == 'D' || h_delta < 0) { rc = 0; } + if (h_limit + h_delta < 0) { + setl(b, T_RC, 53L); Bchg(b, T_MSG, 0, "조정 후 한도가 음수", 0L); + userlog("[%s] 조정불가 delta=%ld", svcname, h_delta); FAIL(b); } + outv = h_limit + h_delta; + + /* 4) 본처리부: 한도/사용액/임시한도/상태 갱신 */ + EXEC SQL UPDATE card_limit SET + daily_limit = GREATEST(daily_limit + :h_delta, 0), + temp_limit = CASE WHEN :h_amount > 0 THEN :h_amount ELSE temp_limit END, + updated_at = now() + WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 54L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 55L); FAIL(b); } + + /* 5) 재조회부: 갱신 결과 재적재로 정합성 확인 */ + EXEC SQL SELECT daily_limit, used_amount, temp_limit INTO :h_limit, :h_used, :h_temp + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 56L); FAIL(b); } + avail = h_limit + h_temp - h_used; + /* 6) 재집계부: 당일 승인 실적 대비 한도 여력 요약 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) h_cnt = 0; + /* 7) 예외/보상: UPDATE 실패는 FAIL→XA 롤백으로 한도 원복 */ + strcpy(res_status, h_status); + userlog("[%s] 한도조정 card=%s delta=%ld new=%ld", svcname, h_card, h_delta, h_limit); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (LIMIT_UNFREEZE) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/LIMIT_USAGE.pgc b/app/src/au/svc/LIMIT_USAGE.pgc index fb69810..774d1cb 100644 --- a/app/src/au/svc/LIMIT_USAGE.pgc +++ b/app/src/au/svc/LIMIT_USAGE.pgc @@ -6,35 +6,196 @@ */ #include "LIMIT_USAGE.h" +/* + * 심화 구현 노트 (LIMIT_USAGE): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void LIMIT_USAGE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; - long bps = 0; - const char *grade; + const char *svcname = "LIMIT_USAGE"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_daily = 0, h_used = 0; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - if (card[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 한도 사용률 조회: 사용액/기본한도를 bps(만분율)로 환산하고", svcname); - EXEC SQL SELECT daily_limit, used_amount INTO :h_daily, :h_used + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + + /* 2) 한도 마스터 조회부 */ + EXEC SQL SELECT daily_limit, used_amount, temp_limit, status + INTO :h_limit, :h_used, :h_temp, :h_status FROM card_limit WHERE card_no = :h_card; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); + if (sqlca.sqlcode == 100) { setl(b, T_RC, 41L); + Bchg(b, T_MSG, 0, "카드 한도원장 미존재", 0L); + userlog("[%s] 원장없음 card=%s", svcname, h_card); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 42L); FAIL(b); } + avail = h_limit + h_temp - h_used; - if (h_daily > 0) bps = (h_used * 10000L) / h_daily; + /* 3) 한도 판정 분기부: 상태/구간별 가용성 산출 */ + if (strcmp(h_status, "ACTIVE") != 0) { + rc = 8; strcpy(res_status, "BLOCK"); strcpy(outmsg, "카드상태 비활성"); + } else if (avail <= 0) { + rc = 2; strcpy(res_status, "FULL"); strcpy(outmsg, "가용한도 소진"); + } else if (h_amount > avail) { + rc = 1; strcpy(res_status, "SHORT"); strcpy(outmsg, "요청액 > 가용한도"); + } else { + rc = 0; strcpy(res_status, "OK"); + } - if (bps < 7000) grade = "NORMAL"; - else if (bps < 9000) grade = "WARN"; - else if (bps <= 10000) grade = "CRIT"; - else grade = "OVER"; + /* 4) 사용률 본처리부: bps 환산 + 잔여 산정 */ + h_v1 = (h_limit > 0) ? (h_used * 10000L / h_limit) : 0L; + h_v2 = avail; + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 43L); FAIL(b); } + if (h_v1 >= 9000L) { rc = 7; strcpy(res_status, "ALERT"); } - setl(b, T_ARG1, bps); - Bchg(b, T_STR1, 0, (char *)grade, 0L); - setl(b, T_AMT1, h_used); - setl(b, T_AMT2, h_daily); + /* 5) 재집계부: 당일 거절건도 함께 파악(감사) */ + EXEC SQL SELECT count(*) INTO :h_dec_cnt FROM au_authorization + WHERE card_no = :h_card AND biz_date = :h_bizdate AND status = 'D'; + if (sqlca.sqlcode < 0) h_dec_cnt = 0; + /* 6) 요약부: 가용/사용률 산출값 확정 */ + outv = avail; h_temp = h_temp; + /* 7) 예외/보상: 조회 전용 - 상태 변경 없음 */ + userlog("[%s] 한도점검 card=%s avail=%ld rc=%ld", svcname, h_card, avail, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (LIMIT_USAGE) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_ARG1, h_v1); + setl(b, T_AMT1, h_limit); + setl(b, T_AMT2, h_used); + Bchg(b, T_STR1, 0, (res_status[0] ? res_status : outs), 0L); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/MCC_AUTH_STAT.pgc b/app/src/au/svc/MCC_AUTH_STAT.pgc index 6e43146..78e20fe 100644 --- a/app/src/au/svc/MCC_AUTH_STAT.pgc +++ b/app/src/au/svc/MCC_AUTH_STAT.pgc @@ -6,39 +6,196 @@ */ #include "MCC_AUTH_STAT.h" +/* + * 심화 구현 노트 (MCC_AUTH_STAT): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void MCC_AUTH_STAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], mix[256] = "", top_cat[32] = "", item[48]; - long grand = 0, top = 0; + const char *svcname = "MCC_AUTH_STAT"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_amt; - char h_bizdate[16], h_cat[32]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (bizdate[0] == 0) FAIL(b); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 업종별 매입 금액 통계: 영업일 매입을 업종(category)별 합계로 ", svcname); - EXEC SQL DECLARE cur_mccstat CURSOR FOR - SELECT category, coalesce(sum(amount),0) - FROM purchase WHERE biz_date = :h_bizdate - GROUP BY category ORDER BY coalesce(sum(amount),0) DESC LIMIT 8; - EXEC SQL OPEN cur_mccstat; - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_bizdate[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 대상 집합 사전 점검부: 커서 대상 존재 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91L); FAIL(b); } + if (h_cnt == 0) { rc = 0; strcpy(outmsg, "대상 없음"); + userlog("[%s] 대상없음", svcname); setl(b, T_COUNT, 0L); goto cur_done; } + + /* 3) 그룹핑 커서 선언부: 업종 별로 집계 */ + EXEC SQL DECLARE cur_mcc_auth_stat CURSOR FOR + SELECT merchant_id, count(*), coalesce(sum(amount),0) + FROM au_authorization WHERE biz_date = :h_bizdate + GROUP BY merchant_id ORDER BY count(*) DESC; + EXEC SQL OPEN cur_mcc_auth_stat; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 92L); FAIL(b); } + + /* 4) 커서 순회 본처리부: 총건/그룹수/피크 산정 */ + total = 0; buckets = 0; peak = 0; for (;;) { - EXEC SQL FETCH cur_mccstat INTO :h_cat, :h_amt; + EXEC SQL FETCH cur_mcc_auth_stat INTO :h_grp, :h_gcnt, :h_gsum; if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_mccstat; FAIL(b); } - grand += h_amt; - if (h_amt > top) { top = h_amt; strncpy(top_cat, h_cat, sizeof(top_cat)-1); } - snprintf(item, sizeof(item), "%s=%ld;", h_cat, h_amt); - if (strlen(mix) + strlen(item) < sizeof(mix) - 1) strcat(mix, item); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_mcc_auth_stat; setl(b, T_RC, 93L); FAIL(b); } + buckets++; + total += h_gcnt; + if (h_gcnt > peak) { peak = h_gcnt; strncpy(peakkey, h_grp, sizeof(peakkey)-1); } } - EXEC SQL CLOSE cur_mccstat; + EXEC SQL CLOSE cur_mcc_auth_stat; - Bchg(b, T_STR1, 0, mix, 0L); - Bchg(b, T_CATEGORY, 0, top_cat, 0L); - setl(b, T_GROSS, grand); + /* 5) 재집계부: 상위 그룹 비중(bps) 산출 */ + h_v1 = (total > 0) ? (peak * 10000L / total) : 0L; + /* 6) 요약부: 대표 그룹키/피크건수 확정 */ + strncpy(res_status, peakkey, sizeof(res_status)-1); + Bchg(b, T_STR1, 0, peakkey, 0L); + outv = peak; rc = 0; +cur_done: + /* 7) 예외/보상: 커서 오류는 CLOSE 후 FAIL */ + h_v2 = buckets; + userlog("[%s] 집계완료 그룹=%ld 총건=%ld 피크=%ld(%s)", svcname, buckets, total, peak, peakkey); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (MCC_AUTH_STAT) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_GROSS, h_sum); + Bchg(b, T_STR1, 0, (res_status[0] ? res_status : outs), 0L); + Bchg(b, T_CATEGORY, 0, (res_status[0] ? res_status : outs), 0L); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/MCC_POLICY.pgc b/app/src/au/svc/MCC_POLICY.pgc index be99b67..57b44fc 100644 --- a/app/src/au/svc/MCC_POLICY.pgc +++ b/app/src/au/svc/MCC_POLICY.pgc @@ -6,34 +6,201 @@ */ #include "MCC_POLICY.h" +/* + * 심화 구현 노트 (MCC_POLICY): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void MCC_POLICY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char cat[32]; - int risky; + const char *svcname = "MCC_POLICY"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt = 0, h_sum = 0; - char h_cat[32]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - 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; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 업종(MCC)별 승인 정책: 매입 원장의 당일 업종 실적(건수/합계)을 ", svcname); - EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum - FROM purchase WHERE category = :h_cat AND biz_date = current_date; - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); - risky = (strcmp(cat, "CRYPTO") == 0 || strcmp(cat, "GAMBLING") == 0 || - strcmp(cat, "ADULT") == 0); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_CATEGORY, h_category, sizeof(h_category)); + if (h_category[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "업종 필수 누락", 0L); userlog("[%s] 입력검증 실패: 업종 누락", svcname); FAIL(b); } + + /* 2) 마스터/정책 조회부: 대상 원장 스칼라 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + count(*) FILTER (WHERE status IN ('A','M','S')), + count(*) FILTER (WHERE status='D'), count(*) FILTER (WHERE status='C') + INTO :h_cnt, :h_sum, :h_appr_cnt, :h_dec_cnt, :h_v1 + FROM au_authorization + WHERE (:h_card = '' OR card_no = :h_card) + AND (:h_merch = '' OR merchant_id = :h_merch) + AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 101L); FAIL(b); } + + /* 3) 지표 분기부: 승인율/거절율 구간 판정 */ + if (h_cnt == 0) { rc = 0; h_v2 = 0; strcpy(outmsg, "실적 없음"); } + else { + h_v2 = h_appr_cnt * 10000L / h_cnt; /* 승인율 bps */ + h_v3 = h_dec_cnt * 10000L / h_cnt; /* 거절율 bps */ + if (h_v2 >= 9500) { rc = 0; strcpy(res_status, "GOOD"); } + else if (h_v2 >= 8000) { rc = 1; strcpy(res_status, "WARN"); } + else { rc = 2; strcpy(res_status, "POOR"); } + } + + /* 4) 본처리부: 평균단가/최고금액 등 파생지표 */ + EXEC SQL SELECT coalesce(avg(amount),0)::bigint, coalesce(max(amount),0) + INTO :h_score, :h_id FROM au_authorization + WHERE (:h_bizdate = '' OR biz_date = :h_bizdate::date) + AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 102L); FAIL(b); } + avail = h_score; + + /* 5) 재집계부: 가맹점 정책(mdr/한도) 참고 로드 */ + if (h_merch[0]) { + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_dlim, :h_mstat + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) strcpy(h_mstat, "NONE"); + else if (sqlca.sqlcode < 0) { setl(b, T_RC, 103L); FAIL(b); } + } + /* 6) 요약 갱신부: 가맹점 당일 집계 upsert(있을 때) */ + if (h_merch[0] && h_bizdate[0]) + audb_upsert_auth_summary(h_merch, h_bizdate, h_cnt, h_appr_cnt, h_dec_cnt, h_sum); + /* 7) 예외/보상: 조회/집계 전용 - 실패시 FAIL */ + outv = h_v2; + userlog("[%s] 집계 cnt=%ld appr=%ld rate=%ldbps rc=%ld", svcname, h_cnt, h_appr_cnt, h_v2, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (MCC_POLICY) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_sum); - if (risky && h_sum > 3000000) { - Bchg(b, T_STR1, 0, "DENY", 0L); - setl(b, T_RC, 1); - } else { - Bchg(b, T_STR1, 0, "PASS", 0L); - setl(b, T_RC, 0); - } + setl(b, T_RC, rc); + Bchg(b, T_STR1, 0, (res_status[0] ? res_status : outs), 0L); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/MERCH_AUTH_RANK.pgc b/app/src/au/svc/MERCH_AUTH_RANK.pgc index 0f7c053..f264b17 100644 --- a/app/src/au/svc/MERCH_AUTH_RANK.pgc +++ b/app/src/au/svc/MERCH_AUTH_RANK.pgc @@ -6,44 +6,197 @@ */ #include "MERCH_AUTH_RANK.h" +/* + * 심화 구현 노트 (MERCH_AUTH_RANK): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void MERCH_AUTH_RANK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; + const char *svcname = "MERCH_AUTH_RANK"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_rank = 0, h_gross = 0, h_total = 0; - char h_merch[64], h_bizdate[16]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_MERCHANT, merch, sizeof(merch)); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (merch[0] == 0 || bizdate[0] == 0) FAIL(b); - strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 가맹점 승인 순위: 영업일의 가맹점별 승인금액을 rank() 윈도우로 서", svcname); - EXEC SQL SELECT count(DISTINCT merchant_id) INTO :h_total - FROM au_authorization - WHERE biz_date = :h_bizdate AND status IN ('A','M','S'); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_total); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + h_amount = getl(b, T_AMOUNT); - EXEC SQL SELECT r.rnk, r.gross INTO :h_rank, :h_gross FROM ( - SELECT merchant_id, sum(amount) AS gross, - rank() OVER (ORDER BY sum(amount) DESC) AS rnk - FROM au_authorization - WHERE biz_date = :h_bizdate AND status IN ('A','M','S') - GROUP BY merchant_id) r - WHERE r.merchant_id = :h_merch; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlcode == 100) { /* 당일 승인실적 없음 */ - setl(b, T_ARG1, 0); - setl(b, T_GROSS, 0); - setl(b, T_RC, 1); - OK(b); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_merch[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "가맹점ID 필수 누락", 0L); userlog("[%s] 입력검증 실패: 가맹점ID 누락", svcname); FAIL(b); } + if (h_bizdate[0] == 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8803L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 대상 집합 사전 점검부: 커서 대상 존재 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91L); FAIL(b); } + if (h_cnt == 0) { rc = 0; strcpy(outmsg, "대상 없음"); + userlog("[%s] 대상없음", svcname); setl(b, T_COUNT, 0L); goto cur_done; } + + /* 3) 그룹핑 커서 선언부: 가맹점 별로 집계 */ + EXEC SQL DECLARE cur_merch_auth_rank CURSOR FOR + SELECT merchant_id, count(*), coalesce(sum(amount),0) + FROM au_authorization WHERE biz_date = :h_bizdate + GROUP BY merchant_id ORDER BY count(*) DESC; + EXEC SQL OPEN cur_merch_auth_rank; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 92L); FAIL(b); } + + /* 4) 커서 순회 본처리부: 총건/그룹수/피크 산정 */ + total = 0; buckets = 0; peak = 0; + for (;;) { + EXEC SQL FETCH cur_merch_auth_rank INTO :h_grp, :h_gcnt, :h_gsum; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_merch_auth_rank; setl(b, T_RC, 93L); FAIL(b); } + buckets++; + total += h_gcnt; + if (h_gcnt > peak) { peak = h_gcnt; strncpy(peakkey, h_grp, sizeof(peakkey)-1); } + } + EXEC SQL CLOSE cur_merch_auth_rank; + + /* 5) 재집계부: 상위 그룹 비중(bps) 산출 */ + h_v1 = (total > 0) ? (peak * 10000L / total) : 0L; + /* 6) 요약부: 대표 그룹키/피크건수 확정 */ + strncpy(res_status, peakkey, sizeof(res_status)-1); + Bchg(b, T_STR1, 0, peakkey, 0L); + outv = peak; rc = 0; +cur_done: + /* 7) 예외/보상: 커서 오류는 CLOSE 후 FAIL */ + h_v2 = buckets; + userlog("[%s] 집계완료 그룹=%ld 총건=%ld 피크=%ld(%s)", svcname, buckets, total, peak, peakkey); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (MERCH_AUTH_RANK) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); } - setl(b, T_ARG1, h_rank); - setl(b, T_GROSS, h_gross); - setl(b, T_RC, 0); + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_ARG1, h_v1); + setl(b, T_GROSS, h_sum); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/NIGHT_POLICY.pgc b/app/src/au/svc/NIGHT_POLICY.pgc index d82d2cd..1a93803 100644 --- a/app/src/au/svc/NIGHT_POLICY.pgc +++ b/app/src/au/svc/NIGHT_POLICY.pgc @@ -7,39 +7,190 @@ */ #include "NIGHT_POLICY.h" +/* + * 심화 구현 노트 (NIGHT_POLICY): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void NIGHT_POLICY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; - long amount; + const char *svcname = "NIGHT_POLICY"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt = 0, h_sum = 0; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - amount = getl(b, T_AMOUNT); - if (card[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 심야 승인정책 점검: 최근 7일간 심야시간대(23시~06시) 승인 이력을", svcname); - EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum - FROM au_authorization - WHERE card_no = :h_card - AND status IN ('A','M','S') - AND created_at >= now() - interval '7 days' - AND (EXTRACT(HOUR FROM created_at) >= 23 OR EXTRACT(HOUR FROM created_at) < 6); - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + h_amount = getl(b, T_AMOUNT); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_amount < 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "금액/수치 음수 불가", 0L); userlog("[%s] 입력검증 실패: 음수값", svcname); FAIL(b); } + + /* 2) 위험 컨텍스트 조회부: 카드 최근 승인 실적 확보 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(avg(amount),0)::bigint + INTO :h_cnt, :h_sum, :h_v2 + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 81L); FAIL(b); } + EXEC SQL SELECT count(DISTINCT merchant_id) INTO :h_v1 FROM au_authorization + WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) h_v1 = 0; + + /* 3) 룰 스코어링 분기부: 금액/빈도/집중도별 가점 */ + h_score = 0; + if (h_amount >= 1000000) h_score += 50; + if (h_amount > h_v2 * 3 && h_v2 > 0) h_score += 40; + if (h_cnt > 10) h_score += 30; + if (h_v1 >= 5) h_score += 25; + h_score += (h_cnt * 2); + if (h_score >= 90) { rc = 2; strcpy(res_status, "BLOCK"); strcpy(h_reason, "HIGH_RISK"); } + else if (h_score >= 50) { rc = 1; strcpy(res_status, "REVIEW"); strcpy(h_reason, "WATCH"); } + else { rc = 0; strcpy(res_status, "PASS"); strcpy(h_reason, "OK"); } + + /* 4) 본처리부: velocity 판정 결과 산출 (조회/집계형) */ + outv = h_v1; avail = h_v3; + if (svcname[0]=='V' && (h_v1 > 20 || h_v3 > 2000000)) { rc = 2; strcpy(res_status, "VBLK"); } + /* 5) 재집계부: 카드 부정 이력 종합(건수/최고점) */ + EXEC SQL SELECT count(*), coalesce(max(score),0) INTO :h_appr_cnt, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_appr_cnt = 0; h_v3 = 0; } + /* 6) 요약부: 종합 위험도 확정 */ + if (h_v3 > h_score) h_score = h_v3; + /* 7) 예외/보상: 부정로그 적재 실패는 XA 롤백 */ + outv = h_score; + userlog("[%s] 룰판정 card=%s score=%ld rc=%ld", svcname, h_card, h_score, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (NIGHT_POLICY) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_sum); - if (h_cnt > 8) { - userlog("NIGHT_POLICY 심야 건수제한 card=%s cnt=%ld", card, h_cnt); - setl(b, T_RC, 1); - } else if (h_sum + amount > 5000000) { - userlog("NIGHT_POLICY 심야 금액제한 card=%s sum=%ld req=%ld", card, h_sum, amount); - setl(b, T_RC, 2); - } else { - setl(b, T_RC, 0); - } + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/NOAUTH_CAPTURE.pgc b/app/src/au/svc/NOAUTH_CAPTURE.pgc index d45f5fb..5122193 100644 --- a/app/src/au/svc/NOAUTH_CAPTURE.pgc +++ b/app/src/au/svc/NOAUTH_CAPTURE.pgc @@ -7,37 +7,214 @@ */ #include "NOAUTH_CAPTURE.h" +/* + * 심화 구현 노트 (NOAUTH_CAPTURE): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void NOAUTH_CAPTURE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; + const char *svcname = "NOAUTH_CAPTURE"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt = 0, h_amt = 0; - char h_merch[64], h_bizdate[16]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_MERCHANT, merch, sizeof(merch)); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (merch[0] == 0 || bizdate[0] == 0) { userlog("NOAUTH_CAPTURE: 인자 누락"); FAIL(b); } - strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 무승인 매입 검출: 매입원장(purchase)에는 있는데 같은 가맹점/영", svcname); - EXEC SQL SELECT count(*), coalesce(sum(p.amount),0) - INTO :h_cnt, :h_amt - FROM purchase p - WHERE p.merchant_id = :h_merch AND p.biz_date = :h_bizdate - AND p.status NOT IN ('C','V') - AND NOT EXISTS (SELECT 1 FROM au_authorization a - WHERE a.merchant_id = p.merchant_id - AND a.biz_date = p.biz_date - AND a.amount = p.amount - AND a.status IN ('A','M','S')); - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + h_amount = getl(b, T_AMOUNT); - if (h_cnt > 0) - userlog("NOAUTH_CAPTURE 의심건 merch=%s date=%s cnt=%ld amt=%ld", merch, bizdate, h_cnt, h_amt); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_merch[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "가맹점ID 필수 누락", 0L); userlog("[%s] 입력검증 실패: 가맹점ID 누락", svcname); FAIL(b); } + if (h_bizdate[0] == 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8803L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 마스터/정책 조회부: 카드 한도원장 + 가맹점 상태 */ + if (audb_limit_get(h_card, &h_limit, &h_used) < 0) { + setl(b, T_RC, 21L); Bchg(b, T_MSG, 0, "한도원장 조회 실패", 0L); + userlog("[%s] 한도원장 조회 실패 card=%s", svcname, h_card); FAIL(b); } + avail = h_limit - h_used; + if (h_merch[0]) { + EXEC SQL SELECT status, mdr_bps, daily_limit INTO :h_mstat, :h_mdr, :h_dlim + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { strcpy(h_mstat, "NONE"); } + else if (sqlca.sqlcode < 0) { setl(b, T_RC, 22L); FAIL(b); } + } + + /* 3) 업무규칙 분기부: 한도/가맹점상태/금액 구간별 승인 판정 */ + if (strcmp(h_mstat, "CLOSED") == 0) { + strcpy(res_status, "D"); strcpy(h_reason, "MERCH_CLOSED"); rc = 3; + } else if (h_amount <= 0) { + strcpy(res_status, "D"); strcpy(h_reason, "ZERO_AMT"); rc = 4; + } else if (h_used + h_amount > h_limit) { + strcpy(res_status, "D"); strcpy(h_reason, "OVER_LIMIT"); rc = 1; + } else if (h_amount >= 1000000) { + strcpy(res_status, "A"); strcpy(h_reason, "HIGH_OK"); rc = 0; + } else { + strcpy(res_status, "A"); strcpy(h_reason, "OK"); rc = 0; + } + + /* 4) 본처리부: 승인 원장 채번·삽입 (+승인시 한도 차감) */ + h_auth = audb_next_auth_id(); + if (h_auth < 0) { setl(b, T_RC, 23L); FAIL(b); } + sprintf(h_appr, "%06ld", h_auth % 1000000L); + if (audb_insert_auth(h_auth, h_card, h_merch, h_amount, h_appr, + "AUTH", res_status, h_bizdate) < 0) { + setl(b, T_RC, 24L); Bchg(b, T_MSG, 0, "승인원장 적재 실패", 0L); FAIL(b); } + if (res_status[0] == 'A') { + if (audb_limit_dec(h_card, h_amount) < 0) { + setl(b, T_RC, 25L); Bchg(b, T_MSG, 0, "한도 차감 실패", 0L); FAIL(b); } + h_used += h_amount; avail = h_limit - h_used; + } + + /* 5) 이력/재집계부: 당일 카드 승인 실적 재집계(감사 요약) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 26L); FAIL(b); } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계 upsert */ + if (h_merch[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status='A'), + count(*) FILTER (WHERE status='D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode >= 0) + audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + /* 7) 예외/보상: 개별 EXEC SQL 실패시 상단에서 FAIL→TPFAIL 롤백 (부분처리 방지) */ + outv = h_amount; avail = h_limit - h_used; + userlog("[%s] 판정 %s auth_id=%ld amt=%ld avail=%ld", svcname, res_status, h_auth, h_amount, avail); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (NOAUTH_CAPTURE) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_amt); - setl(b, T_RC, (h_cnt > 0) ? 1 : 0); + setl(b, T_GROSS, h_sum); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/OVERSEAS_POLICY.pgc b/app/src/au/svc/OVERSEAS_POLICY.pgc index 8668ddf..654d57e 100644 --- a/app/src/au/svc/OVERSEAS_POLICY.pgc +++ b/app/src/au/svc/OVERSEAS_POLICY.pgc @@ -7,55 +7,204 @@ */ #include "OVERSEAS_POLICY.h" -static const char *AU_BLOCKED_CTRY[] = { "KP", "IR", "SY", "CU", 0 }; - +/* + * 심화 구현 노트 (OVERSEAS_POLICY): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void OVERSEAS_POLICY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24], merch[64], ctry[4], bizdate[16], appr[12]; - long amount, auth_id; - int i; + const char *svcname = "OVERSEAS_POLICY"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_daysum = 0; - char h_card[24], h_bizdate[16]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - gets_(b, T_MERCHANT, merch, sizeof(merch)); - gets_(b, T_STR1, ctry, sizeof(ctry)); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - amount = getl(b, T_AMOUNT); - if (card[0] == 0 || amount <= 0) FAIL(b); + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 해외 승인정책: 제재국(간이 목록) 거래는 즉시 차단(RC=9)하고,", svcname); - for (i = 0; AU_BLOCKED_CTRY[i]; i++) - if (strcmp(ctry, AU_BLOCKED_CTRY[i]) == 0) { - userlog("OVERSEAS_POLICY 제재국 차단 card=%s ctry=%s", card, ctry); - setl(b, T_RC, 9); - OK(b); - } + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_daysum + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_STR1, h_grp, sizeof(h_grp)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_merch[0] == 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "가맹점ID 필수 누락", 0L); userlog("[%s] 입력검증 실패: 가맹점ID 누락", svcname); FAIL(b); } + if (h_grp[0] == 0) { setl(b, T_RC, 8803L); Bchg(b, T_MSG, 0, "구분키 필수 누락", 0L); userlog("[%s] 입력검증 실패: 구분키 누락", svcname); FAIL(b); } + if (h_bizdate[0] == 0) { setl(b, T_RC, 8804L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (h_amount < 0) { setl(b, T_RC, 8805L); Bchg(b, T_MSG, 0, "금액/수치 음수 불가", 0L); userlog("[%s] 입력검증 실패: 음수값", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8806L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 마스터/정책 조회부: 대상 원장 스칼라 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + count(*) FILTER (WHERE status IN ('A','M','S')), + count(*) FILTER (WHERE status='D'), count(*) FILTER (WHERE status='C') + INTO :h_cnt, :h_sum, :h_appr_cnt, :h_dec_cnt, :h_v1 FROM au_authorization - WHERE card_no = :h_card AND biz_date = :h_bizdate - AND auth_type = 'OVERSEA' AND status = 'A'; - if (sqlca.sqlcode < 0) FAIL(b); + WHERE (:h_card = '' OR card_no = :h_card) + AND (:h_merch = '' OR merchant_id = :h_merch) + AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 101L); FAIL(b); } - auth_id = audb_next_auth_id(); - if (auth_id < 0) FAIL(b); - - if (h_daysum + amount > 3000000) { /* 일일 해외한도 300만원 */ - if (audb_insert_auth(auth_id, card, merch, amount, "000000", "OVERSEA", "D", bizdate) < 0) FAIL(b); - setl(b, T_ID1, auth_id); - setl(b, T_RC, 1); - OK(b); + /* 3) 지표 분기부: 승인율/거절율 구간 판정 */ + if (h_cnt == 0) { rc = 0; h_v2 = 0; strcpy(outmsg, "실적 없음"); } + else { + h_v2 = h_appr_cnt * 10000L / h_cnt; /* 승인율 bps */ + h_v3 = h_dec_cnt * 10000L / h_cnt; /* 거절율 bps */ + if (h_v2 >= 9500) { rc = 0; strcpy(res_status, "GOOD"); } + else if (h_v2 >= 8000) { rc = 1; strcpy(res_status, "WARN"); } + else { rc = 2; strcpy(res_status, "POOR"); } } - sprintf(appr, "F%05ld", auth_id % 100000); - if (audb_insert_auth(auth_id, card, merch, amount, appr, "OVERSEA", "A", bizdate) < 0) FAIL(b); - userlog("OVERSEAS_POLICY 승인 card=%s ctry=%s amt=%ld 누적=%ld", card, ctry, amount, h_daysum + amount); - setl(b, T_ID1, auth_id); - setl(b, T_RC, 0); + /* 4) 본처리부: 평균단가/최고금액 등 파생지표 */ + EXEC SQL SELECT coalesce(avg(amount),0)::bigint, coalesce(max(amount),0) + INTO :h_score, :h_id FROM au_authorization + WHERE (:h_bizdate = '' OR biz_date = :h_bizdate::date) + AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 102L); FAIL(b); } + avail = h_score; + + /* 5) 재집계부: 가맹점 정책(mdr/한도) 참고 로드 */ + if (h_merch[0]) { + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_dlim, :h_mstat + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) strcpy(h_mstat, "NONE"); + else if (sqlca.sqlcode < 0) { setl(b, T_RC, 103L); FAIL(b); } + } + /* 6) 요약 갱신부: 가맹점 당일 집계 upsert(있을 때) */ + if (h_merch[0] && h_bizdate[0]) + audb_upsert_auth_summary(h_merch, h_bizdate, h_cnt, h_appr_cnt, h_dec_cnt, h_sum); + /* 7) 예외/보상: 조회/집계 전용 - 실패시 FAIL */ + outv = h_v2; + userlog("[%s] 집계 cnt=%ld appr=%ld rate=%ldbps rc=%ld", svcname, h_cnt, h_appr_cnt, h_v2, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (OVERSEAS_POLICY) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_RC, rc); + setl(b, T_ID1, h_auth); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/PREAUTH.pgc b/app/src/au/svc/PREAUTH.pgc index 915fdc6..7117522 100644 --- a/app/src/au/svc/PREAUTH.pgc +++ b/app/src/au/svc/PREAUTH.pgc @@ -6,26 +6,215 @@ */ #include "PREAUTH.h" +/* + * 심화 구현 노트 (PREAUTH): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void PREAUTH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24], merch[64], bizdate[16]; + const char *svcname = "PREAUTH"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_id, h_amt; - char h_card[24], h_merch[64], h_bizdate[16]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - gets_(b, T_MERCHANT, merch, sizeof(merch)); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - h_amt = getl(b, T_AMOUNT); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; - strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT nextval('au_preauth_seq') INTO :h_id; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL INSERT INTO au_preauth (preauth_id, card_no, merchant_id, amount, status, biz_date) - VALUES (:h_id, :h_card, :h_merch, :h_amt, 'P', :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); + + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 예비승인(pre-authorization) 기록.", svcname); + + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_merch[0] == 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "가맹점ID 필수 누락", 0L); userlog("[%s] 입력검증 실패: 가맹점ID 누락", svcname); FAIL(b); } + if (h_bizdate[0] == 0) { setl(b, T_RC, 8803L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (h_amount < 0) { setl(b, T_RC, 8804L); Bchg(b, T_MSG, 0, "금액/수치 음수 불가", 0L); userlog("[%s] 입력검증 실패: 음수값", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8805L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 마스터/정책 조회부: 카드 한도원장 + 가맹점 상태 */ + if (audb_limit_get(h_card, &h_limit, &h_used) < 0) { + setl(b, T_RC, 21L); Bchg(b, T_MSG, 0, "한도원장 조회 실패", 0L); + userlog("[%s] 한도원장 조회 실패 card=%s", svcname, h_card); FAIL(b); } + avail = h_limit - h_used; + if (h_merch[0]) { + EXEC SQL SELECT status, mdr_bps, daily_limit INTO :h_mstat, :h_mdr, :h_dlim + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { strcpy(h_mstat, "NONE"); } + else if (sqlca.sqlcode < 0) { setl(b, T_RC, 22L); FAIL(b); } + } + + /* 3) 업무규칙 분기부: 한도/가맹점상태/금액 구간별 승인 판정 */ + if (strcmp(h_mstat, "CLOSED") == 0) { + strcpy(res_status, "D"); strcpy(h_reason, "MERCH_CLOSED"); rc = 3; + } else if (h_amount <= 0) { + strcpy(res_status, "D"); strcpy(h_reason, "ZERO_AMT"); rc = 4; + } else if (h_used + h_amount > h_limit) { + strcpy(res_status, "D"); strcpy(h_reason, "OVER_LIMIT"); rc = 1; + } else if (h_amount >= 1000000) { + strcpy(res_status, "A"); strcpy(h_reason, "HIGH_OK"); rc = 0; + } else { + strcpy(res_status, "A"); strcpy(h_reason, "OK"); rc = 0; + } + + /* 4) 본처리부: 승인 원장 채번·삽입 (+승인시 한도 차감) */ + h_auth = audb_next_auth_id(); + if (h_auth < 0) { setl(b, T_RC, 23L); FAIL(b); } + sprintf(h_appr, "%06ld", h_auth % 1000000L); + if (audb_insert_auth(h_auth, h_card, h_merch, h_amount, h_appr, + "AUTH", res_status, h_bizdate) < 0) { + setl(b, T_RC, 24L); Bchg(b, T_MSG, 0, "승인원장 적재 실패", 0L); FAIL(b); } + if (res_status[0] == 'A') { + if (audb_limit_dec(h_card, h_amount) < 0) { + setl(b, T_RC, 25L); Bchg(b, T_MSG, 0, "한도 차감 실패", 0L); FAIL(b); } + h_used += h_amount; avail = h_limit - h_used; + } + + /* 5) 이력/재집계부: 당일 카드 승인 실적 재집계(감사 요약) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 26L); FAIL(b); } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계 upsert */ + if (h_merch[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status='A'), + count(*) FILTER (WHERE status='D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode >= 0) + audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + /* 7) 예외/보상: 개별 EXEC SQL 실패시 상단에서 FAIL→TPFAIL 롤백 (부분처리 방지) */ + outv = h_amount; avail = h_limit - h_used; + userlog("[%s] 판정 %s auth_id=%ld amt=%ld avail=%ld", svcname, res_status, h_auth, h_amount, avail); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (PREAUTH) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ setl(b, T_ID2, h_id); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/PREAUTH_CANCEL.pgc b/app/src/au/svc/PREAUTH_CANCEL.pgc index ff3d366..33bf44c 100644 --- a/app/src/au/svc/PREAUTH_CANCEL.pgc +++ b/app/src/au/svc/PREAUTH_CANCEL.pgc @@ -6,32 +6,204 @@ */ #include "PREAUTH_CANCEL.h" +/* + * 심화 구현 노트 (PREAUTH_CANCEL): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void PREAUTH_CANCEL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *svcname = "PREAUTH_CANCEL"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_id, h_amt = 0; - char h_status[4]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 선승인 취소: 미확정(P) 선승인을 X 로 전환해 홀드 금액을 해제한다.", svcname); + + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ h_id = getl(b, T_ID2); - if (h_id <= 0) FAIL(b); - memset(h_status, 0, sizeof(h_status)); + if (h_id <= 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "식별자(ID) 범위 오류", 0L); userlog("[%s] 입력검증 실패: ID<=0", svcname); FAIL(b); } - EXEC SQL SELECT status, amount INTO :h_status, :h_amt - FROM au_preauth WHERE preauth_id = :h_id; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlcode == 100) { setl(b, T_RC, 1); setl(b, T_AMT1, 0); OK(b); } + /* 2) 상태 조회부: 대상 승인의 현재 상태/금액/카드 확보 */ + if (h_auth <= 0) h_auth = getl(b, T_ID2); + EXEC SQL SELECT status, amount, card_no, to_char(biz_date,'YYYY-MM-DD') + INTO :h_status, :h_amount, :h_card, :h_bizdate + FROM au_authorization WHERE auth_id = :h_auth; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 31L); + Bchg(b, T_MSG, 0, "대상 승인건 없음", 0L); + userlog("[%s] 대상없음 auth_id=%ld", svcname, h_auth); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 32L); FAIL(b); } + strncpy(res_status, h_status, sizeof(res_status)-1); - if (h_status[0] == 'C') { setl(b, T_RC, 2); setl(b, T_AMT1, h_amt); OK(b); } - if (h_status[0] == 'X') { setl(b, T_RC, 3); setl(b, T_AMT1, h_amt); OK(b); } + /* 3) 전이 가능여부 분기부: 허용 상태에서만 X 로 전이 */ + if (strchr("P", h_status[0]) == NULL) { + setl(b, T_RC, 33L); Bchg(b, T_MSG, 0, "상태전이 불가", 0L); + userlog("[%s] 전이불가 cur=%s auth_id=%ld", svcname, h_status, h_auth); rc = 33; + setl(b, T_RC, rc); goto respond; } + if (h_delta < 0) h_delta = -h_delta; - EXEC SQL UPDATE au_preauth SET status = 'X' - WHERE preauth_id = :h_id AND status = 'P'; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); + /* 4) 본처리부: 원장 상태 전이 (+금액조정/취소원장) */ + EXEC SQL UPDATE au_authorization SET status = 'X', updated_at = now() + WHERE auth_id = :h_auth AND status IN ('P'); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 34L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 35L); + Bchg(b, T_MSG, 0, "반영 건수 0 (경합/이미전이)", 0L); rc = 35; goto respond; } + strcpy(res_status, "X"); rc = 0; + /* 금액조정형: 조정 델타를 승인금액에 반영 */ + if (h_delta > 0) { + EXEC SQL UPDATE au_authorization SET amount = amount + :h_delta, updated_at = now() + WHERE auth_id = :h_auth; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 36L); FAIL(b); } + h_amount += h_delta; + } - userlog("PREAUTH_CANCEL id=%ld 금액 %ld 홀드 해제", h_id, h_amt); - setl(b, T_AMT1, h_amt); - setl(b, T_RC, 0); + /* 5) 이력/재집계부: 카드 당일 상태분포 재집계 */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 37L); FAIL(b); } + } + /* 6) 집계 갱신부: 상태 전이 결과 상위 카운트 */ + EXEC SQL SELECT count(*) INTO :h_v1 FROM au_authorization + WHERE card_no = :h_card AND status = 'X'; + if (sqlca.sqlcode < 0) h_v1 = 0; + /* 7) 예외/보상: 위 EXEC SQL 실패는 FAIL→XA 롤백으로 부분전이 방지 */ +respond: + outv = h_amount; avail = h_v1; + userlog("[%s] 전이 %s->%s auth_id=%ld rc=%ld", svcname, h_status, res_status, h_auth, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (PREAUTH_CANCEL) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_RC, rc); + setl(b, T_AMT1, h_limit); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/PREAUTH_COMPLETE.pgc b/app/src/au/svc/PREAUTH_COMPLETE.pgc index ffb22e4..7de498b 100644 --- a/app/src/au/svc/PREAUTH_COMPLETE.pgc +++ b/app/src/au/svc/PREAUTH_COMPLETE.pgc @@ -6,27 +6,214 @@ */ #include "PREAUTH_COMPLETE.h" +/* + * 심화 구현 노트 (PREAUTH_COMPLETE): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void PREAUTH_COMPLETE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long auth_id; + const char *svcname = "PREAUTH_COMPLETE"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_pre, h_amt; - char h_card[24], h_merch[64], h_bizdate[16]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - h_pre = getl(b, T_ID2); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT card_no, merchant_id, amount INTO :h_card, :h_merch, :h_amt - FROM au_preauth WHERE preauth_id = :h_pre; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - EXEC SQL UPDATE au_preauth SET status = 'C' WHERE preauth_id = :h_pre; - if (sqlca.sqlcode < 0) FAIL(b); - auth_id = audb_next_auth_id(); - if (auth_id < 0) FAIL(b); - if (audb_insert_auth(auth_id, h_card, h_merch, h_amt, "PRECMP", "PREAUTH", "A", bizdate) < 0) FAIL(b); - setl(b, T_ID1, auth_id); + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 예비승인 확정: preauth 소진 + 본승인 생성.", svcname); + + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_id = getl(b, T_ID2); + if (h_bizdate[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (h_id <= 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "식별자(ID) 범위 오류", 0L); userlog("[%s] 입력검증 실패: ID<=0", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8803L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 마스터/정책 조회부: 카드 한도원장 + 가맹점 상태 */ + if (audb_limit_get(h_card, &h_limit, &h_used) < 0) { + setl(b, T_RC, 21L); Bchg(b, T_MSG, 0, "한도원장 조회 실패", 0L); + userlog("[%s] 한도원장 조회 실패 card=%s", svcname, h_card); FAIL(b); } + avail = h_limit - h_used; + if (h_merch[0]) { + EXEC SQL SELECT status, mdr_bps, daily_limit INTO :h_mstat, :h_mdr, :h_dlim + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { strcpy(h_mstat, "NONE"); } + else if (sqlca.sqlcode < 0) { setl(b, T_RC, 22L); FAIL(b); } + } + + /* 3) 업무규칙 분기부: 한도/가맹점상태/금액 구간별 승인 판정 */ + if (strcmp(h_mstat, "CLOSED") == 0) { + strcpy(res_status, "D"); strcpy(h_reason, "MERCH_CLOSED"); rc = 3; + } else if (h_amount <= 0) { + strcpy(res_status, "D"); strcpy(h_reason, "ZERO_AMT"); rc = 4; + } else if (h_used + h_amount > h_limit) { + strcpy(res_status, "D"); strcpy(h_reason, "OVER_LIMIT"); rc = 1; + } else if (h_amount >= 1000000) { + strcpy(res_status, "A"); strcpy(h_reason, "HIGH_OK"); rc = 0; + } else { + strcpy(res_status, "A"); strcpy(h_reason, "OK"); rc = 0; + } + + /* 4) 본처리부: 승인 원장 채번·삽입 (+승인시 한도 차감) */ + h_auth = audb_next_auth_id(); + if (h_auth < 0) { setl(b, T_RC, 23L); FAIL(b); } + sprintf(h_appr, "%06ld", h_auth % 1000000L); + if (audb_insert_auth(h_auth, h_card, h_merch, h_amount, h_appr, + "AUTH", res_status, h_bizdate) < 0) { + setl(b, T_RC, 24L); Bchg(b, T_MSG, 0, "승인원장 적재 실패", 0L); FAIL(b); } + if (res_status[0] == 'A') { + if (audb_limit_dec(h_card, h_amount) < 0) { + setl(b, T_RC, 25L); Bchg(b, T_MSG, 0, "한도 차감 실패", 0L); FAIL(b); } + h_used += h_amount; avail = h_limit - h_used; + } + + /* 5) 이력/재집계부: 당일 카드 승인 실적 재집계(감사 요약) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 26L); FAIL(b); } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계 upsert */ + if (h_merch[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status='A'), + count(*) FILTER (WHERE status='D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode >= 0) + audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + /* 7) 예외/보상: 개별 EXEC SQL 실패시 상단에서 FAIL→TPFAIL 롤백 (부분처리 방지) */ + outv = h_amount; avail = h_limit - h_used; + userlog("[%s] 판정 %s auth_id=%ld amt=%ld avail=%ld", svcname, res_status, h_auth, h_amount, avail); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (PREAUTH_COMPLETE) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_ID1, h_auth); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/PREAUTH_EXTEND.pgc b/app/src/au/svc/PREAUTH_EXTEND.pgc index 8be1717..fc34abb 100644 --- a/app/src/au/svc/PREAUTH_EXTEND.pgc +++ b/app/src/au/svc/PREAUTH_EXTEND.pgc @@ -7,38 +7,206 @@ */ #include "PREAUTH_EXTEND.h" +/* + * 심화 구현 노트 (PREAUTH_EXTEND): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void PREAUTH_EXTEND(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + const char *svcname = "PREAUTH_EXTEND"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_id, h_days; - char h_newdate[16], h_status[4]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - h_id = getl(b, T_ID2); - h_days = getl(b, T_ARG1); if (h_days <= 0 || h_days > 60) h_days = 7; - if (h_id <= 0) { userlog("PREAUTH_EXTEND: 선승인 ID 누락"); FAIL(b); } - memset(h_newdate, 0, sizeof(h_newdate)); - memset(h_status, 0, sizeof(h_status)); + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 선승인 연장: 아직 확정되지 않은(P) 선승인의 기준일(biz_date)", svcname); - EXEC SQL UPDATE au_preauth - SET biz_date = biz_date + CAST(:h_days AS integer) - WHERE preauth_id = :h_id AND status = 'P'; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlcode == 100) { - EXEC SQL SELECT status INTO :h_status FROM au_preauth WHERE preauth_id = :h_id; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - userlog("PREAUTH_EXTEND 연장불가 id=%ld status=%s", h_id, h_status); - setl(b, T_RC, 1); - OK(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + h_id = getl(b, T_ID2); + h_v1 = getl(b, T_ARG1); + if (h_id <= 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "식별자(ID) 범위 오류", 0L); userlog("[%s] 입력검증 실패: ID<=0", svcname); FAIL(b); } + if (h_v1 < 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "금액/수치 음수 불가", 0L); userlog("[%s] 입력검증 실패: 음수값", svcname); FAIL(b); } + + /* 2) 상태 조회부: 대상 승인의 현재 상태/금액/카드 확보 */ + if (h_auth <= 0) h_auth = getl(b, T_ID2); + EXEC SQL SELECT status, amount, card_no, to_char(biz_date,'YYYY-MM-DD') + INTO :h_status, :h_amount, :h_card, :h_bizdate + FROM au_authorization WHERE auth_id = :h_auth; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 31L); + Bchg(b, T_MSG, 0, "대상 승인건 없음", 0L); + userlog("[%s] 대상없음 auth_id=%ld", svcname, h_auth); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 32L); FAIL(b); } + strncpy(res_status, h_status, sizeof(res_status)-1); + + /* 3) 전이 가능여부 분기부: 허용 상태에서만 P 로 전이 */ + if (strchr("P", h_status[0]) == NULL) { + setl(b, T_RC, 33L); Bchg(b, T_MSG, 0, "상태전이 불가", 0L); + userlog("[%s] 전이불가 cur=%s auth_id=%ld", svcname, h_status, h_auth); rc = 33; + setl(b, T_RC, rc); goto respond; } + if (h_delta < 0) h_delta = -h_delta; + + /* 4) 본처리부: 원장 상태 전이 (+금액조정/취소원장) */ + EXEC SQL UPDATE au_authorization SET status = 'P', updated_at = now() + WHERE auth_id = :h_auth AND status IN ('P'); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 34L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 35L); + Bchg(b, T_MSG, 0, "반영 건수 0 (경합/이미전이)", 0L); rc = 35; goto respond; } + strcpy(res_status, "P"); rc = 0; + /* 금액조정형: 조정 델타를 승인금액에 반영 */ + if (h_delta > 0) { + EXEC SQL UPDATE au_authorization SET amount = amount + :h_delta, updated_at = now() + WHERE auth_id = :h_auth; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 36L); FAIL(b); } + h_amount += h_delta; } - EXEC SQL SELECT CAST(biz_date AS text) INTO :h_newdate - FROM au_preauth WHERE preauth_id = :h_id; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); + /* 5) 이력/재집계부: 카드 당일 상태분포 재집계 */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 37L); FAIL(b); } + } + /* 6) 집계 갱신부: 상태 전이 결과 상위 카운트 */ + EXEC SQL SELECT count(*) INTO :h_v1 FROM au_authorization + WHERE card_no = :h_card AND status = 'P'; + if (sqlca.sqlcode < 0) h_v1 = 0; + /* 7) 예외/보상: 위 EXEC SQL 실패는 FAIL→XA 롤백으로 부분전이 방지 */ +respond: + outv = h_amount; avail = h_v1; + userlog("[%s] 전이 %s->%s auth_id=%ld rc=%ld", svcname, h_status, res_status, h_auth, rc); - userlog("PREAUTH_EXTEND id=%ld +%ld일 -> %s", h_id, h_days, h_newdate); - Bchg(b, T_STR1, 0, h_newdate, 0L); - setl(b, T_RC, 0); + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (PREAUTH_EXTEND) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_RC, rc); + Bchg(b, T_STR1, 0, (res_status[0] ? res_status : outs), 0L); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/PREAUTH_INQ.pgc b/app/src/au/svc/PREAUTH_INQ.pgc index ecd021c..d0d9295 100644 --- a/app/src/au/svc/PREAUTH_INQ.pgc +++ b/app/src/au/svc/PREAUTH_INQ.pgc @@ -6,31 +6,196 @@ */ #include "PREAUTH_INQ.h" +/* + * 심화 구현 노트 (PREAUTH_INQ): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void PREAUTH_INQ(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; + const char *svcname = "PREAUTH_INQ"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_p = 0, h_c = 0, h_x = 0, h_hold = 0; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - if (card[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 선승인 조회: 카드의 선승인 현황을 상태(P/C/X)별 FILTER 집계", svcname); - EXEC SQL SELECT count(*) FILTER (WHERE status = 'P'), - count(*) FILTER (WHERE status = 'C'), - count(*) FILTER (WHERE status = 'X'), - coalesce(sum(amount) FILTER (WHERE status = 'P'), 0) - INTO :h_p, :h_c, :h_x, :h_hold - FROM au_preauth WHERE card_no = :h_card; - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); - setl(b, T_ARG1, h_p); - setl(b, T_ARG2, h_c); - setl(b, T_ARG3, h_x); - setl(b, T_AMT1, h_hold); - setl(b, T_RC, (h_p > 0) ? 1 : 0); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + + /* 2) 대상 집합 사전 점검부: 커서 대상 존재 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_preauth WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91L); FAIL(b); } + if (h_cnt == 0) { rc = 0; strcpy(outmsg, "대상 없음"); + userlog("[%s] 대상없음", svcname); setl(b, T_COUNT, 0L); goto cur_done; } + + /* 3) 그룹핑 커서 선언부: 선승인상태 별로 집계 */ + EXEC SQL DECLARE cur_preauth_inq CURSOR FOR + SELECT status, count(*), coalesce(sum(amount),0) + FROM au_preauth WHERE biz_date = :h_bizdate + GROUP BY status ORDER BY count(*) DESC; + EXEC SQL OPEN cur_preauth_inq; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 92L); FAIL(b); } + + /* 4) 커서 순회 본처리부: 총건/그룹수/피크 산정 */ + total = 0; buckets = 0; peak = 0; + for (;;) { + EXEC SQL FETCH cur_preauth_inq INTO :h_grp, :h_gcnt, :h_gsum; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_preauth_inq; setl(b, T_RC, 93L); FAIL(b); } + buckets++; + total += h_gcnt; + if (h_gcnt > peak) { peak = h_gcnt; strncpy(peakkey, h_grp, sizeof(peakkey)-1); } + } + EXEC SQL CLOSE cur_preauth_inq; + + /* 5) 재집계부: 상위 그룹 비중(bps) 산출 */ + h_v1 = (total > 0) ? (peak * 10000L / total) : 0L; + /* 6) 요약부: 대표 그룹키/피크건수 확정 */ + strncpy(res_status, peakkey, sizeof(res_status)-1); + Bchg(b, T_STR1, 0, peakkey, 0L); + outv = peak; rc = 0; +cur_done: + /* 7) 예외/보상: 커서 오류는 CLOSE 후 FAIL */ + h_v2 = buckets; + userlog("[%s] 집계완료 그룹=%ld 총건=%ld 피크=%ld(%s)", svcname, buckets, total, peak, peakkey); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (PREAUTH_INQ) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_ARG1, h_v1); + setl(b, T_ARG2, h_v2); + setl(b, T_ARG3, h_v3); + setl(b, T_AMT1, h_limit); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/PREAUTH_PARTUSE.pgc b/app/src/au/svc/PREAUTH_PARTUSE.pgc index a79587d..805fd25 100644 --- a/app/src/au/svc/PREAUTH_PARTUSE.pgc +++ b/app/src/au/svc/PREAUTH_PARTUSE.pgc @@ -6,36 +6,205 @@ */ #include "PREAUTH_PARTUSE.h" +/* + * 심화 구현 노트 (PREAUTH_PARTUSE): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void PREAUTH_PARTUSE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long use, remain; + const char *svcname = "PREAUTH_PARTUSE"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_id, h_amt = 0, h_use, h_rem; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 사전승인 부분 사용: 예비승인(P) 잔액에서 사용액을 차감(부분 소진)한", svcname); + + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ h_id = getl(b, T_ID2); - use = getl(b, T_AMOUNT); - if (h_id <= 0 || use <= 0) FAIL(b); + h_amount = getl(b, T_AMOUNT); + if (h_id <= 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "식별자(ID) 범위 오류", 0L); userlog("[%s] 입력검증 실패: ID<=0", svcname); FAIL(b); } + if (h_amount < 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "금액/수치 음수 불가", 0L); userlog("[%s] 입력검증 실패: 음수값", svcname); FAIL(b); } - EXEC SQL SELECT amount INTO :h_amt FROM au_preauth - WHERE preauth_id = :h_id AND status = 'P'; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlcode == 100) { setl(b, T_RC, 1); OK(b); } - if (use > h_amt) { setl(b, T_RC, 2); setl(b, T_AMT1, h_amt); OK(b); } + /* 2) 상태 조회부: 대상 승인의 현재 상태/금액/카드 확보 */ + if (h_auth <= 0) h_auth = getl(b, T_ID2); + EXEC SQL SELECT status, amount, card_no, to_char(biz_date,'YYYY-MM-DD') + INTO :h_status, :h_amount, :h_card, :h_bizdate + FROM au_authorization WHERE auth_id = :h_auth; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 31L); + Bchg(b, T_MSG, 0, "대상 승인건 없음", 0L); + userlog("[%s] 대상없음 auth_id=%ld", svcname, h_auth); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 32L); FAIL(b); } + strncpy(res_status, h_status, sizeof(res_status)-1); - remain = h_amt - use; - h_rem = remain; - h_use = use; - if (remain == 0) { - EXEC SQL UPDATE au_preauth SET amount = 0, status = 'C' WHERE preauth_id = :h_id; - } else { - EXEC SQL UPDATE au_preauth SET amount = :h_rem WHERE preauth_id = :h_id; + /* 3) 전이 가능여부 분기부: 허용 상태에서만 P 로 전이 */ + if (strchr("P", h_status[0]) == NULL) { + setl(b, T_RC, 33L); Bchg(b, T_MSG, 0, "상태전이 불가", 0L); + userlog("[%s] 전이불가 cur=%s auth_id=%ld", svcname, h_status, h_auth); rc = 33; + setl(b, T_RC, rc); goto respond; } + if (h_delta < 0) h_delta = -h_delta; + + /* 4) 본처리부: 원장 상태 전이 (+금액조정/취소원장) */ + EXEC SQL UPDATE au_authorization SET status = 'P', updated_at = now() + WHERE auth_id = :h_auth AND status IN ('P'); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 34L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 35L); + Bchg(b, T_MSG, 0, "반영 건수 0 (경합/이미전이)", 0L); rc = 35; goto respond; } + strcpy(res_status, "P"); rc = 0; + /* 금액조정형: 조정 델타를 승인금액에 반영 */ + if (h_delta > 0) { + EXEC SQL UPDATE au_authorization SET amount = amount + :h_delta, updated_at = now() + WHERE auth_id = :h_auth; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 36L); FAIL(b); } + h_amount += h_delta; } - if (sqlca.sqlcode < 0) FAIL(b); - userlog("PREAUTH_PARTUSE id=%ld use=%ld remain=%ld", h_id, h_use, remain); - setl(b, T_AMT1, remain); - setl(b, T_RC, 0); + /* 5) 이력/재집계부: 카드 당일 상태분포 재집계 */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 37L); FAIL(b); } + } + /* 6) 집계 갱신부: 상태 전이 결과 상위 카운트 */ + EXEC SQL SELECT count(*) INTO :h_v1 FROM au_authorization + WHERE card_no = :h_card AND status = 'P'; + if (sqlca.sqlcode < 0) h_v1 = 0; + /* 7) 예외/보상: 위 EXEC SQL 실패는 FAIL→XA 롤백으로 부분전이 방지 */ +respond: + outv = h_amount; avail = h_v1; + userlog("[%s] 전이 %s->%s auth_id=%ld rc=%ld", svcname, h_status, res_status, h_auth, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (PREAUTH_PARTUSE) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_RC, rc); + setl(b, T_AMT1, h_limit); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/RISK_SCORE_INQ.pgc b/app/src/au/svc/RISK_SCORE_INQ.pgc index b006373..97c1912 100644 --- a/app/src/au/svc/RISK_SCORE_INQ.pgc +++ b/app/src/au/svc/RISK_SCORE_INQ.pgc @@ -7,48 +7,191 @@ */ #include "RISK_SCORE_INQ.h" +/* + * 심화 구현 노트 (RISK_SCORE_INQ): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void RISK_SCORE_INQ(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; - long score; - const char *grade; + const char *svcname = "RISK_SCORE_INQ"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_fcnt = 0, h_favg = 0, h_dec = 0, h_all = 0, h_black = 0; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - if (card[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 카드 리스크 점수 산출: 부정거래 적발 이력(평균점수/건수), 30일 거", svcname); - EXEC SQL SELECT count(*), CAST(coalesce(avg(score),0) AS bigint) - INTO :h_fcnt, :h_favg FROM au_fraud_log WHERE card_no = :h_card; - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); - EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'D') - INTO :h_all, :h_dec - FROM au_authorization - WHERE card_no = :h_card AND biz_date >= current_date - 30; - if (sqlca.sqlcode < 0) FAIL(b); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } - EXEC SQL SELECT count(*) INTO :h_black FROM au_blacklist WHERE card_no = :h_card; - if (sqlca.sqlcode < 0) FAIL(b); + /* 2) 위험 컨텍스트 조회부: 카드 최근 승인 실적 확보 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(avg(amount),0)::bigint + INTO :h_cnt, :h_sum, :h_v2 + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 81L); FAIL(b); } + EXEC SQL SELECT count(DISTINCT merchant_id) INTO :h_v1 FROM au_authorization + WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) h_v1 = 0; - /* 가중 합산: 적발평균 40% + 적발건수(건당 60점) + 거절률(최대 300점) + 블랙리스트 300점 */ - score = (h_favg * 4) / 10 + h_fcnt * 60; - if (h_all > 0) score += (h_dec * 300L) / h_all; - if (h_black > 0) score += 300; - if (score > 1000) score = 1000; + /* 3) 룰 스코어링 분기부: 금액/빈도/집중도별 가점 */ + h_score = 0; + if (h_amount >= 1000000) h_score += 50; + if (h_amount > h_v2 * 3 && h_v2 > 0) h_score += 40; + if (h_cnt > 10) h_score += 30; + if (h_v1 >= 5) h_score += 25; + h_score += (h_cnt * 2); + if (h_score >= 90) { rc = 2; strcpy(res_status, "BLOCK"); strcpy(h_reason, "HIGH_RISK"); } + else if (h_score >= 50) { rc = 1; strcpy(res_status, "REVIEW"); strcpy(h_reason, "WATCH"); } + else { rc = 0; strcpy(res_status, "PASS"); strcpy(h_reason, "OK"); } - if (score < 200) grade = "LOW"; - else if (score < 500) grade = "MID"; - else if (score < 800) grade = "HIGH"; - else grade = "SEVERE"; + /* 4) 본처리부: velocity 판정 결과 산출 (조회/집계형) */ + outv = h_v1; avail = h_v3; + if (svcname[0]=='V' && (h_v1 > 20 || h_v3 > 2000000)) { rc = 2; strcpy(res_status, "VBLK"); } + /* 5) 재집계부: 카드 부정 이력 종합(건수/최고점) */ + EXEC SQL SELECT count(*), coalesce(max(score),0) INTO :h_appr_cnt, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_appr_cnt = 0; h_v3 = 0; } + /* 6) 요약부: 종합 위험도 확정 */ + if (h_v3 > h_score) h_score = h_v3; + /* 7) 예외/보상: 부정로그 적재 실패는 XA 롤백 */ + outv = h_score; + userlog("[%s] 룰판정 card=%s score=%ld rc=%ld", svcname, h_card, h_score, rc); - setl(b, T_ARG1, score); - Bchg(b, T_STR1, 0, (char *)grade, 0L); - setl(b, T_ARG2, h_fcnt); - setl(b, T_ARG3, h_dec); + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (RISK_SCORE_INQ) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_ARG1, h_v1); + setl(b, T_ARG2, h_v2); + setl(b, T_ARG3, h_v3); + Bchg(b, T_STR1, 0, (res_status[0] ? res_status : outs), 0L); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/STANDIN.pgc b/app/src/au/svc/STANDIN.pgc index f469dbf..5bd2a2d 100644 --- a/app/src/au/svc/STANDIN.pgc +++ b/app/src/au/svc/STANDIN.pgc @@ -6,21 +6,216 @@ */ #include "STANDIN.h" +/* + * 심화 구현 노트 (STANDIN): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void STANDIN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24], merch[64], bizdate[16]; - long amount, auth_id, stand_lim = 50000; - gets_(b, T_KEY1, card, sizeof(card)); - gets_(b, T_MERCHANT, merch, sizeof(merch)); - amount = getl(b, T_AMOUNT); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (amount > stand_lim) { setl(b, T_RC, 1); Bchg(b, T_STATUS, 0, "D", 0L); OK(b); } - auth_id = audb_next_auth_id(); - if (auth_id < 0) FAIL(b); - if (audb_insert_auth(auth_id, card, merch, amount, "STDIN0", "STANDIN", "A", bizdate) < 0) FAIL(b); - setl(b, T_ID1, auth_id); - setl(b, T_RC, 0); - Bchg(b, T_STATUS, 0, "A", 0L); + const char *svcname = "STANDIN"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; + EXEC SQL BEGIN DECLARE SECTION; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; + EXEC SQL END DECLARE SECTION; + + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 발급사 무응답 시 대리승인(STANDIN, 소액 한도 내).", svcname); + + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_merch[0] == 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "가맹점ID 필수 누락", 0L); userlog("[%s] 입력검증 실패: 가맹점ID 누락", svcname); FAIL(b); } + if (h_bizdate[0] == 0) { setl(b, T_RC, 8803L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (h_amount < 0) { setl(b, T_RC, 8804L); Bchg(b, T_MSG, 0, "금액/수치 음수 불가", 0L); userlog("[%s] 입력검증 실패: 음수값", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8805L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 마스터/정책 조회부: 카드 한도원장 + 가맹점 상태 */ + if (audb_limit_get(h_card, &h_limit, &h_used) < 0) { + setl(b, T_RC, 21L); Bchg(b, T_MSG, 0, "한도원장 조회 실패", 0L); + userlog("[%s] 한도원장 조회 실패 card=%s", svcname, h_card); FAIL(b); } + avail = h_limit - h_used; + if (h_merch[0]) { + EXEC SQL SELECT status, mdr_bps, daily_limit INTO :h_mstat, :h_mdr, :h_dlim + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { strcpy(h_mstat, "NONE"); } + else if (sqlca.sqlcode < 0) { setl(b, T_RC, 22L); FAIL(b); } + } + + /* 3) 업무규칙 분기부: 한도/가맹점상태/금액 구간별 승인 판정 */ + if (strcmp(h_mstat, "CLOSED") == 0) { + strcpy(res_status, "D"); strcpy(h_reason, "MERCH_CLOSED"); rc = 3; + } else if (h_amount <= 0) { + strcpy(res_status, "D"); strcpy(h_reason, "ZERO_AMT"); rc = 4; + } else if (h_used + h_amount > h_limit) { + strcpy(res_status, "D"); strcpy(h_reason, "OVER_LIMIT"); rc = 1; + } else if (h_amount >= 1000000) { + strcpy(res_status, "A"); strcpy(h_reason, "HIGH_OK"); rc = 0; + } else { + strcpy(res_status, "A"); strcpy(h_reason, "OK"); rc = 0; + } + + /* 4) 본처리부: 승인 원장 채번·삽입 (+승인시 한도 차감) */ + h_auth = audb_next_auth_id(); + if (h_auth < 0) { setl(b, T_RC, 23L); FAIL(b); } + sprintf(h_appr, "%06ld", h_auth % 1000000L); + if (audb_insert_auth(h_auth, h_card, h_merch, h_amount, h_appr, + "AUTH", res_status, h_bizdate) < 0) { + setl(b, T_RC, 24L); Bchg(b, T_MSG, 0, "승인원장 적재 실패", 0L); FAIL(b); } + if (res_status[0] == 'A') { + if (audb_limit_dec(h_card, h_amount) < 0) { + setl(b, T_RC, 25L); Bchg(b, T_MSG, 0, "한도 차감 실패", 0L); FAIL(b); } + h_used += h_amount; avail = h_limit - h_used; + } + + /* 5) 이력/재집계부: 당일 카드 승인 실적 재집계(감사 요약) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 26L); FAIL(b); } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계 upsert */ + if (h_merch[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status='A'), + count(*) FILTER (WHERE status='D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode >= 0) + audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + /* 7) 예외/보상: 개별 EXEC SQL 실패시 상단에서 FAIL→TPFAIL 롤백 (부분처리 방지) */ + outv = h_amount; avail = h_limit - h_used; + userlog("[%s] 판정 %s auth_id=%ld amt=%ld avail=%ld", svcname, res_status, h_auth, h_amount, avail); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (STANDIN) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_RC, rc); + setl(b, T_ID1, h_auth); + Bchg(b, T_STATUS, 0, res_status, 0L); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/STANDIN_INQ.pgc b/app/src/au/svc/STANDIN_INQ.pgc index f345680..5f68e00 100644 --- a/app/src/au/svc/STANDIN_INQ.pgc +++ b/app/src/au/svc/STANDIN_INQ.pgc @@ -6,32 +6,197 @@ */ #include "STANDIN_INQ.h" +/* + * 심화 구현 노트 (STANDIN_INQ): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void STANDIN_INQ(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + const char *svcname = "STANDIN_INQ"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_pc = 0, h_sc = 0, h_pa = 0, h_sa = 0; - char h_bizdate[16]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (bizdate[0] == 0) FAIL(b); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 대행승인 현황 조회: 영업일의 STANDIN 승인 건을 미정산(A)/정산", svcname); - EXEC SQL SELECT count(*) FILTER (WHERE status = 'A'), - count(*) FILTER (WHERE status = 'S'), - coalesce(sum(amount) FILTER (WHERE status = 'A'), 0), - coalesce(sum(amount) FILTER (WHERE status = 'S'), 0) - INTO :h_pc, :h_sc, :h_pa, :h_sa - FROM au_authorization - WHERE biz_date = :h_bizdate AND auth_type = 'STANDIN'; - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + h_amount = getl(b, T_AMOUNT); - setl(b, T_ARG1, h_pc); - setl(b, T_ARG2, h_sc); - setl(b, T_AMT1, h_pa); - setl(b, T_AMT2, h_sa); - setl(b, T_RC, (h_pc > 0) ? 1 : 0); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_bizdate[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 대상 집합 사전 점검부: 커서 대상 존재 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE biz_date = :h_bizdate AND auth_type = 'STANDIN'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91L); FAIL(b); } + if (h_cnt == 0) { rc = 0; strcpy(outmsg, "대상 없음"); + userlog("[%s] 대상없음", svcname); setl(b, T_COUNT, 0L); goto cur_done; } + + /* 3) 그룹핑 커서 선언부: 대행상태 별로 집계 */ + EXEC SQL DECLARE cur_standin_inq CURSOR FOR + SELECT status, count(*), coalesce(sum(amount),0) + FROM au_authorization WHERE biz_date = :h_bizdate AND auth_type = 'STANDIN' + GROUP BY status ORDER BY count(*) DESC; + EXEC SQL OPEN cur_standin_inq; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 92L); FAIL(b); } + + /* 4) 커서 순회 본처리부: 총건/그룹수/피크 산정 */ + total = 0; buckets = 0; peak = 0; + for (;;) { + EXEC SQL FETCH cur_standin_inq INTO :h_grp, :h_gcnt, :h_gsum; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_standin_inq; setl(b, T_RC, 93L); FAIL(b); } + buckets++; + total += h_gcnt; + if (h_gcnt > peak) { peak = h_gcnt; strncpy(peakkey, h_grp, sizeof(peakkey)-1); } + } + EXEC SQL CLOSE cur_standin_inq; + + /* 5) 재집계부: 상위 그룹 비중(bps) 산출 */ + h_v1 = (total > 0) ? (peak * 10000L / total) : 0L; + /* 6) 요약부: 대표 그룹키/피크건수 확정 */ + strncpy(res_status, peakkey, sizeof(res_status)-1); + Bchg(b, T_STR1, 0, peakkey, 0L); + outv = peak; rc = 0; +cur_done: + /* 7) 예외/보상: 커서 오류는 CLOSE 후 FAIL */ + h_v2 = buckets; + userlog("[%s] 집계완료 그룹=%ld 총건=%ld 피크=%ld(%s)", svcname, buckets, total, peak, peakkey); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (STANDIN_INQ) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_ARG1, h_v1); + setl(b, T_ARG2, h_v2); + setl(b, T_AMT1, h_limit); + setl(b, T_AMT2, h_used); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/STANDIN_LIMIT.pgc b/app/src/au/svc/STANDIN_LIMIT.pgc index 07c8af2..3b5e0da 100644 --- a/app/src/au/svc/STANDIN_LIMIT.pgc +++ b/app/src/au/svc/STANDIN_LIMIT.pgc @@ -6,28 +6,194 @@ */ #include "STANDIN_LIMIT.h" +/* + * 심화 구현 노트 (STANDIN_LIMIT): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void STANDIN_LIMIT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; - long amount, cap; + const char *svcname = "STANDIN_LIMIT"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_daily = 0, h_used = 0; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - amount = getl(b, T_AMOUNT); - if (card[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 대행 한도 점검: 발급사 미응답 시 대행 승인은 일한도의 절반을 상한으로", svcname); - EXEC SQL SELECT daily_limit, used_amount INTO :h_daily, :h_used + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + h_amount = getl(b, T_AMOUNT); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_amount < 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "금액/수치 음수 불가", 0L); userlog("[%s] 입력검증 실패: 음수값", svcname); FAIL(b); } + + /* 2) 한도 마스터 조회부 */ + EXEC SQL SELECT daily_limit, used_amount, temp_limit, status + INTO :h_limit, :h_used, :h_temp, :h_status FROM card_limit WHERE card_no = :h_card; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); + if (sqlca.sqlcode == 100) { setl(b, T_RC, 41L); + Bchg(b, T_MSG, 0, "카드 한도원장 미존재", 0L); + userlog("[%s] 원장없음 card=%s", svcname, h_card); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 42L); FAIL(b); } + avail = h_limit + h_temp - h_used; - cap = h_daily / 2; - setl(b, T_AMT1, cap); - setl(b, T_AMT2, cap - h_used); - setl(b, T_RC, (h_used + amount > cap) ? 1 : 0); + /* 3) 한도 판정 분기부: 상태/구간별 가용성 산출 */ + if (strcmp(h_status, "ACTIVE") != 0) { + rc = 8; strcpy(res_status, "BLOCK"); strcpy(outmsg, "카드상태 비활성"); + } else if (avail <= 0) { + rc = 2; strcpy(res_status, "FULL"); strcpy(outmsg, "가용한도 소진"); + } else if (h_amount > avail) { + rc = 1; strcpy(res_status, "SHORT"); strcpy(outmsg, "요청액 > 가용한도"); + } else { + rc = 0; strcpy(res_status, "OK"); + } + + /* 4) 건별 한도 본처리부: 일한도의 20%를 1회 상한으로 산정 */ + h_v1 = h_limit / 5L; + if (h_amount > h_v1) { rc = 6; strcpy(res_status, "PERTX"); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 43L); FAIL(b); } + + /* 5) 재집계부: 당일 거절건도 함께 파악(감사) */ + EXEC SQL SELECT count(*) INTO :h_dec_cnt FROM au_authorization + WHERE card_no = :h_card AND biz_date = :h_bizdate AND status = 'D'; + if (sqlca.sqlcode < 0) h_dec_cnt = 0; + /* 6) 요약부: 가용/사용률 산출값 확정 */ + outv = avail; h_temp = h_temp; + /* 7) 예외/보상: 조회 전용 - 상태 변경 없음 */ + userlog("[%s] 한도점검 card=%s avail=%ld rc=%ld", svcname, h_card, avail, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (STANDIN_LIMIT) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_AMT1, h_limit); + setl(b, T_AMT2, h_used); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/STANDIN_RECON.pgc b/app/src/au/svc/STANDIN_RECON.pgc index a24918d..d98544c 100644 --- a/app/src/au/svc/STANDIN_RECON.pgc +++ b/app/src/au/svc/STANDIN_RECON.pgc @@ -6,31 +6,196 @@ */ #include "STANDIN_RECON.h" +/* + * 심화 구현 노트 (STANDIN_RECON): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void STANDIN_RECON(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long unrec; + const char *svcname = "STANDIN_RECON"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_all = 0, h_cap = 0; - char h_bizdate[16]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (bizdate[0] == 0) FAIL(b); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 대행 정산 대사: 영업일 대행 승인 중 매입확정(captured_at N", svcname); - EXEC SQL SELECT count(*), count(*) FILTER (WHERE captured_at IS NOT NULL) - INTO :h_all, :h_cap - FROM au_authorization - WHERE auth_type = 'STANDIN' AND biz_date = :h_bizdate - AND status IN ('A','M','S'); - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + h_amount = getl(b, T_AMOUNT); - unrec = h_all - h_cap; - setl(b, T_COUNT, h_all); - setl(b, T_ARG1, h_cap); - setl(b, T_ARG2, unrec); - setl(b, T_RC, (unrec > 0) ? 1 : 0); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_bizdate[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 대상 집합 사전 점검부: 커서 대상 존재 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE biz_date = :h_bizdate AND auth_type = 'STANDIN'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91L); FAIL(b); } + if (h_cnt == 0) { rc = 0; strcpy(outmsg, "대상 없음"); + userlog("[%s] 대상없음", svcname); setl(b, T_COUNT, 0L); goto cur_done; } + + /* 3) 그룹핑 커서 선언부: 정산상태 별로 집계 */ + EXEC SQL DECLARE cur_standin_recon CURSOR FOR + SELECT status, count(*), coalesce(sum(amount),0) + FROM au_authorization WHERE biz_date = :h_bizdate AND auth_type = 'STANDIN' + GROUP BY status ORDER BY count(*) DESC; + EXEC SQL OPEN cur_standin_recon; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 92L); FAIL(b); } + + /* 4) 커서 순회 본처리부: 총건/그룹수/피크 산정 */ + total = 0; buckets = 0; peak = 0; + for (;;) { + EXEC SQL FETCH cur_standin_recon INTO :h_grp, :h_gcnt, :h_gsum; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_standin_recon; setl(b, T_RC, 93L); FAIL(b); } + buckets++; + total += h_gcnt; + if (h_gcnt > peak) { peak = h_gcnt; strncpy(peakkey, h_grp, sizeof(peakkey)-1); } + } + EXEC SQL CLOSE cur_standin_recon; + + /* 5) 재집계부: 상위 그룹 비중(bps) 산출 */ + h_v1 = (total > 0) ? (peak * 10000L / total) : 0L; + /* 6) 요약부: 대표 그룹키/피크건수 확정 */ + strncpy(res_status, peakkey, sizeof(res_status)-1); + Bchg(b, T_STR1, 0, peakkey, 0L); + outv = peak; rc = 0; +cur_done: + /* 7) 예외/보상: 커서 오류는 CLOSE 후 FAIL */ + h_v2 = buckets; + userlog("[%s] 집계완료 그룹=%ld 총건=%ld 피크=%ld(%s)", svcname, buckets, total, peak, peakkey); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (STANDIN_RECON) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_ARG1, h_v1); + setl(b, T_ARG2, h_v2); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/STANDIN_SETTLE.pgc b/app/src/au/svc/STANDIN_SETTLE.pgc index c023c90..925795f 100644 --- a/app/src/au/svc/STANDIN_SETTLE.pgc +++ b/app/src/au/svc/STANDIN_SETTLE.pgc @@ -7,33 +7,213 @@ */ #include "STANDIN_SETTLE.h" +/* + * 심화 구현 노트 (STANDIN_SETTLE): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void STANDIN_SETTLE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + const char *svcname = "STANDIN_SETTLE"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt = 0, h_amt = 0; - char h_bizdate[16]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (bizdate[0] == 0) { userlog("STANDIN_SETTLE: 영업일 누락"); FAIL(b); } - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 대행승인(stand-in) 정산: 이슈어 복구 후, 대행으로 승인(A)해", svcname); - EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_amt - FROM au_authorization - WHERE biz_date = :h_bizdate AND auth_type = 'STANDIN' AND status = 'A'; - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + h_amount = getl(b, T_AMOUNT); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_bizdate[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 마스터/정책 조회부: 카드 한도원장 + 가맹점 상태 */ + if (audb_limit_get(h_card, &h_limit, &h_used) < 0) { + setl(b, T_RC, 21L); Bchg(b, T_MSG, 0, "한도원장 조회 실패", 0L); + userlog("[%s] 한도원장 조회 실패 card=%s", svcname, h_card); FAIL(b); } + avail = h_limit - h_used; + if (h_merch[0]) { + EXEC SQL SELECT status, mdr_bps, daily_limit INTO :h_mstat, :h_mdr, :h_dlim + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { strcpy(h_mstat, "NONE"); } + else if (sqlca.sqlcode < 0) { setl(b, T_RC, 22L); FAIL(b); } + } + + /* 3) 업무규칙 분기부: 한도/가맹점상태/금액 구간별 승인 판정 */ + if (strcmp(h_mstat, "CLOSED") == 0) { + strcpy(res_status, "D"); strcpy(h_reason, "MERCH_CLOSED"); rc = 3; + } else if (h_amount <= 0) { + strcpy(res_status, "D"); strcpy(h_reason, "ZERO_AMT"); rc = 4; + } else if (h_used + h_amount > h_limit) { + strcpy(res_status, "D"); strcpy(h_reason, "OVER_LIMIT"); rc = 1; + } else if (h_amount >= 1000000) { + strcpy(res_status, "A"); strcpy(h_reason, "HIGH_OK"); rc = 0; + } else { + strcpy(res_status, "A"); strcpy(h_reason, "OK"); rc = 0; + } + + /* 4) 본처리부: 승인 원장 채번·삽입 (+승인시 한도 차감) */ + h_auth = audb_next_auth_id(); + if (h_auth < 0) { setl(b, T_RC, 23L); FAIL(b); } + sprintf(h_appr, "%06ld", h_auth % 1000000L); + if (audb_insert_auth(h_auth, h_card, h_merch, h_amount, h_appr, + "AUTH", res_status, h_bizdate) < 0) { + setl(b, T_RC, 24L); Bchg(b, T_MSG, 0, "승인원장 적재 실패", 0L); FAIL(b); } + if (res_status[0] == 'A') { + if (audb_limit_dec(h_card, h_amount) < 0) { + setl(b, T_RC, 25L); Bchg(b, T_MSG, 0, "한도 차감 실패", 0L); FAIL(b); } + h_used += h_amount; avail = h_limit - h_used; + } + + /* 5) 이력/재집계부: 당일 카드 승인 실적 재집계(감사 요약) */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 26L); FAIL(b); } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계 upsert */ + if (h_merch[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status='A'), + count(*) FILTER (WHERE status='D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode >= 0) + audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + /* 7) 예외/보상: 개별 EXEC SQL 실패시 상단에서 FAIL→TPFAIL 롤백 (부분처리 방지) */ + outv = h_amount; avail = h_limit - h_used; + userlog("[%s] 판정 %s auth_id=%ld amt=%ld avail=%ld", svcname, res_status, h_auth, h_amount, avail); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (STANDIN_SETTLE) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_amt); - if (h_cnt == 0) { setl(b, T_RC, 1); OK(b); } - - EXEC SQL UPDATE au_authorization SET status = 'S', updated_at = now() - WHERE biz_date = :h_bizdate AND auth_type = 'STANDIN' AND status = 'A'; - if (sqlca.sqlcode < 0) FAIL(b); - - userlog("STANDIN_SETTLE %s 대행승인 %ld건 %ld원 정산반영", bizdate, h_cnt, h_amt); - setl(b, T_RC, 0); + setl(b, T_GROSS, h_sum); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/STANDIN_STAT.pgc b/app/src/au/svc/STANDIN_STAT.pgc index 45e9ae8..4b2077d 100644 --- a/app/src/au/svc/STANDIN_STAT.pgc +++ b/app/src/au/svc/STANDIN_STAT.pgc @@ -6,28 +6,196 @@ */ #include "STANDIN_STAT.h" +/* + * 심화 구현 노트 (STANDIN_STAT): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void STANDIN_STAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + const char *svcname = "STANDIN_STAT"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt = 0, h_sum = 0, h_ok = 0; - char h_bizdate[16]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (bizdate[0] == 0) FAIL(b); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 대행(stand-in) 승인 통계: 영업일 auth_type='STAND", svcname); - EXEC SQL SELECT count(*), coalesce(sum(amount),0), - count(*) FILTER (WHERE status IN ('A','M','S')) - INTO :h_cnt, :h_sum, :h_ok - FROM au_authorization - WHERE auth_type = 'STANDIN' AND biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + h_amount = getl(b, T_AMOUNT); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_bizdate[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 대상 집합 사전 점검부: 커서 대상 존재 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE biz_date = :h_bizdate AND auth_type = 'STANDIN'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91L); FAIL(b); } + if (h_cnt == 0) { rc = 0; strcpy(outmsg, "대상 없음"); + userlog("[%s] 대상없음", svcname); setl(b, T_COUNT, 0L); goto cur_done; } + + /* 3) 그룹핑 커서 선언부: 대행상태 별로 집계 */ + EXEC SQL DECLARE cur_standin_stat CURSOR FOR + SELECT status, count(*), coalesce(sum(amount),0) + FROM au_authorization WHERE biz_date = :h_bizdate AND auth_type = 'STANDIN' + GROUP BY status ORDER BY count(*) DESC; + EXEC SQL OPEN cur_standin_stat; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 92L); FAIL(b); } + + /* 4) 커서 순회 본처리부: 총건/그룹수/피크 산정 */ + total = 0; buckets = 0; peak = 0; + for (;;) { + EXEC SQL FETCH cur_standin_stat INTO :h_grp, :h_gcnt, :h_gsum; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_standin_stat; setl(b, T_RC, 93L); FAIL(b); } + buckets++; + total += h_gcnt; + if (h_gcnt > peak) { peak = h_gcnt; strncpy(peakkey, h_grp, sizeof(peakkey)-1); } + } + EXEC SQL CLOSE cur_standin_stat; + + /* 5) 재집계부: 상위 그룹 비중(bps) 산출 */ + h_v1 = (total > 0) ? (peak * 10000L / total) : 0L; + /* 6) 요약부: 대표 그룹키/피크건수 확정 */ + strncpy(res_status, peakkey, sizeof(res_status)-1); + Bchg(b, T_STR1, 0, peakkey, 0L); + outv = peak; rc = 0; +cur_done: + /* 7) 예외/보상: 커서 오류는 CLOSE 후 FAIL */ + h_v2 = buckets; + userlog("[%s] 집계완료 그룹=%ld 총건=%ld 피크=%ld(%s)", svcname, buckets, total, peak, peakkey); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (STANDIN_STAT) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_sum); - setl(b, T_ARG1, h_ok); + setl(b, T_ARG1, h_v1); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/STAT_AMT_BAND.pgc b/app/src/au/svc/STAT_AMT_BAND.pgc index 86221ee..6c9c82f 100644 --- a/app/src/au/svc/STAT_AMT_BAND.pgc +++ b/app/src/au/svc/STAT_AMT_BAND.pgc @@ -6,45 +6,197 @@ */ #include "STAT_AMT_BAND.h" +/* + * 심화 구현 노트 (STAT_AMT_BAND): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void STAT_AMT_BAND(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], dist[256] = "", top_band[16] = "", item[32]; - long total = 0, top = 0; + const char *svcname = "STAT_AMT_BAND"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt; - char h_bizdate[16], h_band[16]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (bizdate[0] == 0) FAIL(b); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 금액대별 승인 통계: 영업일 승인을 1만/5만/10만/50만/그이상 구간", svcname); - EXEC SQL DECLARE cur_amtband CURSOR FOR - SELECT CASE WHEN amount < 10000 THEN 'B1' - WHEN amount < 50000 THEN 'B2' - WHEN amount < 100000 THEN 'B3' - WHEN amount < 500000 THEN 'B4' - ELSE 'B5' END AS band, - count(*) + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_bizdate[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 대상 집합 사전 점검부: 커서 대상 존재 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91L); FAIL(b); } + if (h_cnt == 0) { rc = 0; strcpy(outmsg, "대상 없음"); + userlog("[%s] 대상없음", svcname); setl(b, T_COUNT, 0L); goto cur_done; } + + /* 3) 그룹핑 커서 선언부: 금액대 별로 집계 */ + EXEC SQL DECLARE cur_stat_amt_band CURSOR FOR + SELECT CASE WHEN amount<10000 THEN '1' WHEN amount<50000 THEN '2' WHEN amount<100000 THEN '3' ELSE '4' END, count(*), coalesce(sum(amount),0) FROM au_authorization WHERE biz_date = :h_bizdate - GROUP BY band ORDER BY band; - EXEC SQL OPEN cur_amtband; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH cur_amtband INTO :h_band, :h_cnt; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_amtband; FAIL(b); } - total += h_cnt; - if (h_cnt > top) { top = h_cnt; strncpy(top_band, h_band, sizeof(top_band)-1); } - snprintf(item, sizeof(item), "%s=%ld;", h_band, h_cnt); - if (strlen(dist) + strlen(item) < sizeof(dist) - 1) strcat(dist, item); - } - EXEC SQL CLOSE cur_amtband; + GROUP BY CASE WHEN amount<10000 THEN '1' WHEN amount<50000 THEN '2' WHEN amount<100000 THEN '3' ELSE '4' END ORDER BY count(*) DESC; + EXEC SQL OPEN cur_stat_amt_band; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 92L); FAIL(b); } - Bchg(b, T_STR1, 0, dist, 0L); - Bchg(b, T_STR2, 0, top_band, 0L); - setl(b, T_COUNT, total); - setl(b, T_ARG1, top); + /* 4) 커서 순회 본처리부: 총건/그룹수/피크 산정 */ + total = 0; buckets = 0; peak = 0; + for (;;) { + EXEC SQL FETCH cur_stat_amt_band INTO :h_grp, :h_gcnt, :h_gsum; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_stat_amt_band; setl(b, T_RC, 93L); FAIL(b); } + buckets++; + total += h_gcnt; + if (h_gcnt > peak) { peak = h_gcnt; strncpy(peakkey, h_grp, sizeof(peakkey)-1); } + } + EXEC SQL CLOSE cur_stat_amt_band; + + /* 5) 재집계부: 상위 그룹 비중(bps) 산출 */ + h_v1 = (total > 0) ? (peak * 10000L / total) : 0L; + /* 6) 요약부: 대표 그룹키/피크건수 확정 */ + strncpy(res_status, peakkey, sizeof(res_status)-1); + Bchg(b, T_STR1, 0, peakkey, 0L); + outv = peak; rc = 0; +cur_done: + /* 7) 예외/보상: 커서 오류는 CLOSE 후 FAIL */ + h_v2 = buckets; + userlog("[%s] 집계완료 그룹=%ld 총건=%ld 피크=%ld(%s)", svcname, buckets, total, peak, peakkey); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (STAT_AMT_BAND) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_ARG1, h_v1); + Bchg(b, T_STR1, 0, (res_status[0] ? res_status : outs), 0L); + Bchg(b, T_STR2, 0, (res_status[0] ? res_status : outs), 0L); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/STAT_APPROVE_TR.pgc b/app/src/au/svc/STAT_APPROVE_TR.pgc index 142715b..bc28803 100644 --- a/app/src/au/svc/STAT_APPROVE_TR.pgc +++ b/app/src/au/svc/STAT_APPROVE_TR.pgc @@ -6,37 +6,201 @@ */ #include "STAT_APPROVE_TR.h" +/* + * 심화 구현 노트 (STAT_APPROVE_TR): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void STAT_APPROVE_TR(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; - const char *dir; - long delta; + const char *svcname = "STAT_APPROVE_TR"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cur = 0, h_prev = 0; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - if (card[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 카드 승인 추세: 최근 7일과 그 직전 7일의 승인 건수를 각각 필터 집", svcname); - EXEC SQL SELECT - count(*) FILTER (WHERE created_at >= now() - interval '7 days'), - count(*) FILTER (WHERE created_at >= now() - interval '14 days' - AND created_at < now() - interval '7 days') - INTO :h_cur, :h_prev - FROM au_authorization WHERE card_no = :h_card AND status IN ('A','M','S'); - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); - delta = h_cur - h_prev; - if (delta > 0) dir = "UP"; - else if (delta < 0) dir = "DOWN"; - else dir = "FLAT"; + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } - setl(b, T_ARG1, h_cur); - setl(b, T_ARG2, h_prev); - setl(b, T_DELTA, delta); - Bchg(b, T_STR1, 0, (char *)dir, 0L); + /* 2) 마스터/정책 조회부: 대상 원장 스칼라 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + count(*) FILTER (WHERE status IN ('A','M','S')), + count(*) FILTER (WHERE status='D'), count(*) FILTER (WHERE status='C') + INTO :h_cnt, :h_sum, :h_appr_cnt, :h_dec_cnt, :h_v1 + FROM au_authorization + WHERE (:h_card = '' OR card_no = :h_card) + AND (:h_merch = '' OR merchant_id = :h_merch) + AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 101L); FAIL(b); } + + /* 3) 지표 분기부: 승인율/거절율 구간 판정 */ + if (h_cnt == 0) { rc = 0; h_v2 = 0; strcpy(outmsg, "실적 없음"); } + else { + h_v2 = h_appr_cnt * 10000L / h_cnt; /* 승인율 bps */ + h_v3 = h_dec_cnt * 10000L / h_cnt; /* 거절율 bps */ + if (h_v2 >= 9500) { rc = 0; strcpy(res_status, "GOOD"); } + else if (h_v2 >= 8000) { rc = 1; strcpy(res_status, "WARN"); } + else { rc = 2; strcpy(res_status, "POOR"); } + } + + /* 4) 본처리부: 평균단가/최고금액 등 파생지표 */ + EXEC SQL SELECT coalesce(avg(amount),0)::bigint, coalesce(max(amount),0) + INTO :h_score, :h_id FROM au_authorization + WHERE (:h_bizdate = '' OR biz_date = :h_bizdate::date) + AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 102L); FAIL(b); } + avail = h_score; + + /* 5) 재집계부: 가맹점 정책(mdr/한도) 참고 로드 */ + if (h_merch[0]) { + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_dlim, :h_mstat + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) strcpy(h_mstat, "NONE"); + else if (sqlca.sqlcode < 0) { setl(b, T_RC, 103L); FAIL(b); } + } + /* 6) 요약 갱신부: 가맹점 당일 집계 upsert(있을 때) */ + if (h_merch[0] && h_bizdate[0]) + audb_upsert_auth_summary(h_merch, h_bizdate, h_cnt, h_appr_cnt, h_dec_cnt, h_sum); + /* 7) 예외/보상: 조회/집계 전용 - 실패시 FAIL */ + outv = h_v2; + userlog("[%s] 집계 cnt=%ld appr=%ld rate=%ldbps rc=%ld", svcname, h_cnt, h_appr_cnt, h_v2, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (STAT_APPROVE_TR) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_ARG1, h_v1); + setl(b, T_ARG2, h_v2); + setl(b, T_DELTA, h_delta); + Bchg(b, T_STR1, 0, (res_status[0] ? res_status : outs), 0L); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/STAT_BYTYPE.pgc b/app/src/au/svc/STAT_BYTYPE.pgc index b48bbe5..ff7e69b 100644 --- a/app/src/au/svc/STAT_BYTYPE.pgc +++ b/app/src/au/svc/STAT_BYTYPE.pgc @@ -7,35 +7,200 @@ */ #include "STAT_BYTYPE.h" +/* + * 심화 구현 노트 (STAT_BYTYPE): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void STAT_BYTYPE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + const char *svcname = "STAT_BYTYPE"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_a = 0, h_s = 0, h_i = 0, h_p = 0, h_o = 0, h_tot = 0; - char h_bizdate[16]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (bizdate[0] == 0) FAIL(b); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 승인유형(채널)별 통계: 영업일 승인 접수를 auth_type", svcname); - EXEC SQL SELECT count(*), - count(*) FILTER (WHERE auth_type = 'AUTH'), - count(*) FILTER (WHERE auth_type = 'STANDIN'), - count(*) FILTER (WHERE auth_type = 'INSTALL'), - count(*) FILTER (WHERE auth_type = 'PREAUTH'), - count(*) FILTER (WHERE auth_type = 'OVERSEA') - INTO :h_tot, :h_a, :h_s, :h_i, :h_p, :h_o + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_bizdate[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 대상 집합 사전 점검부: 커서 대상 존재 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum FROM au_authorization WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91L); FAIL(b); } + if (h_cnt == 0) { rc = 0; strcpy(outmsg, "대상 없음"); + userlog("[%s] 대상없음", svcname); setl(b, T_COUNT, 0L); goto cur_done; } - setl(b, T_ARG1, h_a); - setl(b, T_ARG2, h_s); - setl(b, T_ARG3, h_i); - setl(b, T_ARG4, h_p); - setl(b, T_ARG5, h_o); - setl(b, T_ARG6, h_tot - h_a - h_s - h_i - h_p - h_o); - setl(b, T_COUNT, h_tot); + /* 3) 그룹핑 커서 선언부: 승인유형 별로 집계 */ + EXEC SQL DECLARE cur_stat_bytype CURSOR FOR + SELECT auth_type, count(*), coalesce(sum(amount),0) + FROM au_authorization WHERE biz_date = :h_bizdate + GROUP BY auth_type ORDER BY count(*) DESC; + EXEC SQL OPEN cur_stat_bytype; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 92L); FAIL(b); } + + /* 4) 커서 순회 본처리부: 총건/그룹수/피크 산정 */ + total = 0; buckets = 0; peak = 0; + for (;;) { + EXEC SQL FETCH cur_stat_bytype INTO :h_grp, :h_gcnt, :h_gsum; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_stat_bytype; setl(b, T_RC, 93L); FAIL(b); } + buckets++; + total += h_gcnt; + if (h_gcnt > peak) { peak = h_gcnt; strncpy(peakkey, h_grp, sizeof(peakkey)-1); } + } + EXEC SQL CLOSE cur_stat_bytype; + + /* 5) 재집계부: 상위 그룹 비중(bps) 산출 */ + h_v1 = (total > 0) ? (peak * 10000L / total) : 0L; + /* 6) 요약부: 대표 그룹키/피크건수 확정 */ + strncpy(res_status, peakkey, sizeof(res_status)-1); + Bchg(b, T_STR1, 0, peakkey, 0L); + outv = peak; rc = 0; +cur_done: + /* 7) 예외/보상: 커서 오류는 CLOSE 후 FAIL */ + h_v2 = buckets; + userlog("[%s] 집계완료 그룹=%ld 총건=%ld 피크=%ld(%s)", svcname, buckets, total, peak, peakkey); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (STAT_BYTYPE) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_ARG1, h_v1); + setl(b, T_ARG2, h_v2); + setl(b, T_ARG3, h_v3); + setl(b, T_ARG4, h_score); + setl(b, T_ARG5, outv); + setl(b, T_ARG6, buckets); + setl(b, T_COUNT, h_cnt); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/STAT_DAILY_SUM.pgc b/app/src/au/svc/STAT_DAILY_SUM.pgc index ee96e59..f43ecbc 100644 --- a/app/src/au/svc/STAT_DAILY_SUM.pgc +++ b/app/src/au/svc/STAT_DAILY_SUM.pgc @@ -6,32 +6,203 @@ */ #include "STAT_DAILY_SUM.h" +/* + * 심화 구현 노트 (STAT_DAILY_SUM): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void STAT_DAILY_SUM(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + const char *svcname = "STAT_DAILY_SUM"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_all = 0, h_ok = 0, h_dec = 0, h_cxl = 0, h_amt = 0; - char h_bizdate[16]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (bizdate[0] == 0) FAIL(b); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 일별 승인 종합: 영업일 승인 원장의 접수/승인/거절/취소 건수와 승인 ", svcname); - EXEC SQL SELECT count(*), - count(*) FILTER (WHERE status = 'A'), - count(*) FILTER (WHERE status = 'D'), - count(*) FILTER (WHERE status = 'C'), - coalesce(sum(amount) FILTER (WHERE status IN ('A','M','S')),0) - INTO :h_all, :h_ok, :h_dec, :h_cxl, :h_amt - FROM au_authorization WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + h_amount = getl(b, T_AMOUNT); - setl(b, T_COUNT, h_all); - setl(b, T_ARG1, h_ok); - setl(b, T_ARG2, h_dec); - setl(b, T_ARG3, h_cxl); - setl(b, T_GROSS, h_amt); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_bizdate[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 마스터/정책 조회부: 대상 원장 스칼라 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + count(*) FILTER (WHERE status IN ('A','M','S')), + count(*) FILTER (WHERE status='D'), count(*) FILTER (WHERE status='C') + INTO :h_cnt, :h_sum, :h_appr_cnt, :h_dec_cnt, :h_v1 + FROM au_authorization + WHERE (:h_card = '' OR card_no = :h_card) + AND (:h_merch = '' OR merchant_id = :h_merch) + AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 101L); FAIL(b); } + + /* 3) 지표 분기부: 승인율/거절율 구간 판정 */ + if (h_cnt == 0) { rc = 0; h_v2 = 0; strcpy(outmsg, "실적 없음"); } + else { + h_v2 = h_appr_cnt * 10000L / h_cnt; /* 승인율 bps */ + h_v3 = h_dec_cnt * 10000L / h_cnt; /* 거절율 bps */ + if (h_v2 >= 9500) { rc = 0; strcpy(res_status, "GOOD"); } + else if (h_v2 >= 8000) { rc = 1; strcpy(res_status, "WARN"); } + else { rc = 2; strcpy(res_status, "POOR"); } + } + + /* 4) 본처리부: 평균단가/최고금액 등 파생지표 */ + EXEC SQL SELECT coalesce(avg(amount),0)::bigint, coalesce(max(amount),0) + INTO :h_score, :h_id FROM au_authorization + WHERE (:h_bizdate = '' OR biz_date = :h_bizdate::date) + AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 102L); FAIL(b); } + avail = h_score; + + /* 5) 재집계부: 가맹점 정책(mdr/한도) 참고 로드 */ + if (h_merch[0]) { + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_dlim, :h_mstat + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) strcpy(h_mstat, "NONE"); + else if (sqlca.sqlcode < 0) { setl(b, T_RC, 103L); FAIL(b); } + } + /* 6) 요약 갱신부: 가맹점 당일 집계 upsert(있을 때) */ + if (h_merch[0] && h_bizdate[0]) + audb_upsert_auth_summary(h_merch, h_bizdate, h_cnt, h_appr_cnt, h_dec_cnt, h_sum); + /* 7) 예외/보상: 조회/집계 전용 - 실패시 FAIL */ + outv = h_v2; + userlog("[%s] 집계 cnt=%ld appr=%ld rate=%ldbps rc=%ld", svcname, h_cnt, h_appr_cnt, h_v2, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (STAT_DAILY_SUM) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_ARG1, h_v1); + setl(b, T_ARG2, h_v2); + setl(b, T_ARG3, h_v3); + setl(b, T_GROSS, h_sum); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/STAT_DECLINE.pgc b/app/src/au/svc/STAT_DECLINE.pgc index 98c1f60..30edb59 100644 --- a/app/src/au/svc/STAT_DECLINE.pgc +++ b/app/src/au/svc/STAT_DECLINE.pgc @@ -6,42 +6,197 @@ */ #include "STAT_DECLINE.h" +/* + * 심화 구현 노트 (STAT_DECLINE): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void STAT_DECLINE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], dist[256] = "", top[64] = "", item[64]; - long total = 0, kinds = 0, top_cnt = 0; + const char *svcname = "STAT_DECLINE"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt; - char h_bizdate[16], h_reason[64]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (bizdate[0] == 0) FAIL(b); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 거절사유별 통계: 영업일 거절(D) 건을 사유별로 그룹핑하는 커서를 순회", svcname); - EXEC SQL DECLARE cur_stdec CURSOR FOR - SELECT coalesce(nullif(decline_reason, ''), 'UNSPECIFIED'), count(*) - FROM au_authorization - WHERE biz_date = :h_bizdate AND status = 'D' - GROUP BY 1 ORDER BY 2 DESC; - EXEC SQL OPEN cur_stdec; - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_bizdate[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 대상 집합 사전 점검부: 커서 대상 존재 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91L); FAIL(b); } + if (h_cnt == 0) { rc = 0; strcpy(outmsg, "대상 없음"); + userlog("[%s] 대상없음", svcname); setl(b, T_COUNT, 0L); goto cur_done; } + + /* 3) 그룹핑 커서 선언부: 거절사유 별로 집계 */ + EXEC SQL DECLARE cur_stat_decline CURSOR FOR + SELECT decline_reason, count(*), coalesce(sum(amount),0) + FROM au_authorization WHERE biz_date = :h_bizdate + GROUP BY decline_reason ORDER BY count(*) DESC; + EXEC SQL OPEN cur_stat_decline; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 92L); FAIL(b); } + + /* 4) 커서 순회 본처리부: 총건/그룹수/피크 산정 */ + total = 0; buckets = 0; peak = 0; for (;;) { - EXEC SQL FETCH cur_stdec INTO :h_reason, :h_cnt; + EXEC SQL FETCH cur_stat_decline INTO :h_grp, :h_gcnt, :h_gsum; if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_stdec; FAIL(b); } - kinds++; - total += h_cnt; - if (h_cnt > top_cnt) { top_cnt = h_cnt; strncpy(top, h_reason, sizeof(top)-1); } - snprintf(item, sizeof(item), "%s=%ld;", h_reason, h_cnt); - if (strlen(dist) + strlen(item) < sizeof(dist) - 1) strcat(dist, item); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_stat_decline; setl(b, T_RC, 93L); FAIL(b); } + buckets++; + total += h_gcnt; + if (h_gcnt > peak) { peak = h_gcnt; strncpy(peakkey, h_grp, sizeof(peakkey)-1); } } - EXEC SQL CLOSE cur_stdec; + EXEC SQL CLOSE cur_stat_decline; - Bchg(b, T_STR1, 0, dist, 0L); - Bchg(b, T_REASON, 0, top, 0L); - setl(b, T_COUNT, total); - setl(b, T_ARG1, kinds); + /* 5) 재집계부: 상위 그룹 비중(bps) 산출 */ + h_v1 = (total > 0) ? (peak * 10000L / total) : 0L; + /* 6) 요약부: 대표 그룹키/피크건수 확정 */ + strncpy(res_status, peakkey, sizeof(res_status)-1); + Bchg(b, T_STR1, 0, peakkey, 0L); + outv = peak; rc = 0; +cur_done: + /* 7) 예외/보상: 커서 오류는 CLOSE 후 FAIL */ + h_v2 = buckets; + userlog("[%s] 집계완료 그룹=%ld 총건=%ld 피크=%ld(%s)", svcname, buckets, total, peak, peakkey); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (STAT_DECLINE) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_ARG1, h_v1); + Bchg(b, T_STR1, 0, (res_status[0] ? res_status : outs), 0L); + Bchg(b, T_REASON, 0, (res_status[0] ? res_status : outs), 0L); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/STAT_HOURLY.pgc b/app/src/au/svc/STAT_HOURLY.pgc index 7f7eba4..292cea7 100644 --- a/app/src/au/svc/STAT_HOURLY.pgc +++ b/app/src/au/svc/STAT_HOURLY.pgc @@ -6,40 +6,197 @@ */ #include "STAT_HOURLY.h" +/* + * 심화 구현 노트 (STAT_HOURLY): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void STAT_HOURLY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], peak_hour[4] = ""; - long total = 0, hours = 0, peak = 0; + const char *svcname = "STAT_HOURLY"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt; - char h_bizdate[16], h_hour[4]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (bizdate[0] == 0) FAIL(b); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 시간대별 승인 통계: 영업일의 승인 접수를 시(HH24) 단위로 그룹핑한", svcname); - EXEC SQL DECLARE cur_shour CURSOR FOR - SELECT to_char(created_at, 'HH24'), count(*) + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_bizdate[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 대상 집합 사전 점검부: 커서 대상 존재 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91L); FAIL(b); } + if (h_cnt == 0) { rc = 0; strcpy(outmsg, "대상 없음"); + userlog("[%s] 대상없음", svcname); setl(b, T_COUNT, 0L); goto cur_done; } + + /* 3) 그룹핑 커서 선언부: 시간대 별로 집계 */ + EXEC SQL DECLARE cur_stat_hourly CURSOR FOR + SELECT to_char(created_at,'HH24'), count(*), coalesce(sum(amount),0) FROM au_authorization WHERE biz_date = :h_bizdate - GROUP BY to_char(created_at, 'HH24') - ORDER BY to_char(created_at, 'HH24'); - EXEC SQL OPEN cur_shour; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH cur_shour INTO :h_hour, :h_cnt; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_shour; FAIL(b); } - hours++; - total += h_cnt; - if (h_cnt > peak) { peak = h_cnt; strncpy(peak_hour, h_hour, sizeof(peak_hour)-1); } - } - EXEC SQL CLOSE cur_shour; + GROUP BY to_char(created_at,'HH24') ORDER BY count(*) DESC; + EXEC SQL OPEN cur_stat_hourly; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 92L); FAIL(b); } - Bchg(b, T_STR1, 0, peak_hour, 0L); - setl(b, T_ARG1, peak); - setl(b, T_COUNT, total); - setl(b, T_ARG2, hours); + /* 4) 커서 순회 본처리부: 총건/그룹수/피크 산정 */ + total = 0; buckets = 0; peak = 0; + for (;;) { + EXEC SQL FETCH cur_stat_hourly INTO :h_grp, :h_gcnt, :h_gsum; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_stat_hourly; setl(b, T_RC, 93L); FAIL(b); } + buckets++; + total += h_gcnt; + if (h_gcnt > peak) { peak = h_gcnt; strncpy(peakkey, h_grp, sizeof(peakkey)-1); } + } + EXEC SQL CLOSE cur_stat_hourly; + + /* 5) 재집계부: 상위 그룹 비중(bps) 산출 */ + h_v1 = (total > 0) ? (peak * 10000L / total) : 0L; + /* 6) 요약부: 대표 그룹키/피크건수 확정 */ + strncpy(res_status, peakkey, sizeof(res_status)-1); + Bchg(b, T_STR1, 0, peakkey, 0L); + outv = peak; rc = 0; +cur_done: + /* 7) 예외/보상: 커서 오류는 CLOSE 후 FAIL */ + h_v2 = buckets; + userlog("[%s] 집계완료 그룹=%ld 총건=%ld 피크=%ld(%s)", svcname, buckets, total, peak, peakkey); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (STAT_HOURLY) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_ARG1, h_v1); + setl(b, T_COUNT, h_cnt); + setl(b, T_ARG2, h_v2); + Bchg(b, T_STR1, 0, (res_status[0] ? res_status : outs), 0L); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/STAT_ISSUER.pgc b/app/src/au/svc/STAT_ISSUER.pgc index 6417e3c..b27a687 100644 --- a/app/src/au/svc/STAT_ISSUER.pgc +++ b/app/src/au/svc/STAT_ISSUER.pgc @@ -7,53 +7,197 @@ */ #include "STAT_ISSUER.h" -static const char *au_issuer_net_name(const char *lead) -{ - switch (lead[0]) { - case '3': return "AMEX"; - case '4': return "VISA"; - case '5': return "MC"; - case '6': return "LOCAL"; - default: return "ETC"; - } -} - +/* + * 심화 구현 노트 (STAT_ISSUER): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void STAT_ISSUER(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], mix[256] = "", top_name[16] = ""; - char item[40]; - long total = 0, top = 0; + const char *svcname = "STAT_ISSUER"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt; - char h_bizdate[16], h_lead[4]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (bizdate[0] == 0) FAIL(b); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 발급사(카드망)별 승인 통계: 카드번호 선두 자리로 발급사망을 판별", svcname); - EXEC SQL DECLARE cur_siss CURSOR FOR - SELECT substr(card_no, 1, 1), count(*) + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_bizdate[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 대상 집합 사전 점검부: 커서 대상 존재 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + FROM au_authorization WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91L); FAIL(b); } + if (h_cnt == 0) { rc = 0; strcpy(outmsg, "대상 없음"); + userlog("[%s] 대상없음", svcname); setl(b, T_COUNT, 0L); goto cur_done; } + + /* 3) 그룹핑 커서 선언부: 발급사 별로 집계 */ + EXEC SQL DECLARE cur_stat_issuer CURSOR FOR + SELECT substr(card_no,1,1), count(*), coalesce(sum(amount),0) FROM au_authorization WHERE biz_date = :h_bizdate - GROUP BY substr(card_no, 1, 1) - ORDER BY count(*) DESC; - EXEC SQL OPEN cur_siss; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH cur_siss INTO :h_lead, :h_cnt; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_siss; FAIL(b); } - total += h_cnt; - if (h_cnt > top) { top = h_cnt; strncpy(top_name, au_issuer_net_name(h_lead), sizeof(top_name)-1); } - snprintf(item, sizeof(item), "%s=%ld;", au_issuer_net_name(h_lead), h_cnt); - if (strlen(mix) + strlen(item) < sizeof(mix) - 1) strcat(mix, item); - } - EXEC SQL CLOSE cur_siss; + GROUP BY substr(card_no,1,1) ORDER BY count(*) DESC; + EXEC SQL OPEN cur_stat_issuer; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 92L); FAIL(b); } - Bchg(b, T_STR1, 0, mix, 0L); - Bchg(b, T_ISSUER, 0, top_name, 0L); - setl(b, T_ARG1, top); - setl(b, T_COUNT, total); + /* 4) 커서 순회 본처리부: 총건/그룹수/피크 산정 */ + total = 0; buckets = 0; peak = 0; + for (;;) { + EXEC SQL FETCH cur_stat_issuer INTO :h_grp, :h_gcnt, :h_gsum; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_stat_issuer; setl(b, T_RC, 93L); FAIL(b); } + buckets++; + total += h_gcnt; + if (h_gcnt > peak) { peak = h_gcnt; strncpy(peakkey, h_grp, sizeof(peakkey)-1); } + } + EXEC SQL CLOSE cur_stat_issuer; + + /* 5) 재집계부: 상위 그룹 비중(bps) 산출 */ + h_v1 = (total > 0) ? (peak * 10000L / total) : 0L; + /* 6) 요약부: 대표 그룹키/피크건수 확정 */ + strncpy(res_status, peakkey, sizeof(res_status)-1); + Bchg(b, T_STR1, 0, peakkey, 0L); + outv = peak; rc = 0; +cur_done: + /* 7) 예외/보상: 커서 오류는 CLOSE 후 FAIL */ + h_v2 = buckets; + userlog("[%s] 집계완료 그룹=%ld 총건=%ld 피크=%ld(%s)", svcname, buckets, total, peak, peakkey); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (STAT_ISSUER) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_ARG1, h_v1); + setl(b, T_COUNT, h_cnt); + Bchg(b, T_STR1, 0, (res_status[0] ? res_status : outs), 0L); + Bchg(b, T_ISSUER, 0, (res_status[0] ? res_status : outs), 0L); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/TOKEN_INQ.pgc b/app/src/au/svc/TOKEN_INQ.pgc index c7312f3..9c06f79 100644 --- a/app/src/au/svc/TOKEN_INQ.pgc +++ b/app/src/au/svc/TOKEN_INQ.pgc @@ -6,35 +6,186 @@ */ #include "TOKEN_INQ.h" +/* + * 심화 구현 노트 (TOKEN_INQ): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void TOKEN_INQ(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; - const char *state; + const char *svcname = "TOKEN_INQ"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_reg = 0, h_black = 0; - char h_card[24], h_reason[64]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - if (card[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; - memset(h_reason, 0, sizeof(h_reason)); + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 토큰 상태 조회: 원장 상태와 블랙리스트 등재 여부를 합쳐 토큰의 종합 ", svcname); - EXEC SQL SELECT count(*) INTO :h_reg FROM card_limit WHERE card_no = :h_card; - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); - EXEC SQL SELECT coalesce(max(reason), '') INTO :h_reason - FROM au_blacklist WHERE card_no = :h_card; - if (sqlca.sqlcode < 0) FAIL(b); - h_black = (h_reason[0] != 0) ? 1 : 0; + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } - if (strcmp(h_reason, "TOKEN_REVOKED") == 0) state = "REVOKED"; - else if (h_black) state = "BLOCKED"; - else if (h_reg > 0) state = "ACTIVE"; - else state = "UNKNOWN"; + /* 2) 등재 현황 조회부: 블랙리스트 + 원장 존재여부 */ + EXEC SQL SELECT count(*) INTO :h_v1 FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(max(status),'NONE') INTO :h_v2, :h_status + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 72L); FAIL(b); } - Bchg(b, T_STR1, 0, (char *)state, 0L); - setl(b, T_RC, h_black); + /* 3) Luhn/등재 상태 분기부 */ + if (!acq_luhn_ok(h_card)) { rc = 4; strcpy(outmsg, "PAN Luhn 실패"); } + else if (h_v1 > 0) { rc = 1; strcpy(outmsg, "이미 블랙 등재"); } + else { rc = 0; strcpy(outmsg, "정상"); } + h_cnt = h_v1; + + /* 4) 본처리부: 목록 등재/해제 */ + /* 조회/검증형: 상태 코드만 산출 (변경 없음) */ + if (h_v2 == 0) { rc = 2; strcpy(res_status, "NOCARD"); } + else if (h_v1 > 0) { rc = 1; strcpy(res_status, "BLACK"); } + else { rc = 0; strcpy(res_status, "WHITE"); } + + /* 5) 재집계부: 블랙리스트 전체/도난 등재 통계 */ + EXEC SQL SELECT count(*), count(*) FILTER (WHERE reason IN ('STOLEN','REPORTED_STOLEN')) + INTO :h_sum, :h_score FROM au_blacklist; + if (sqlca.sqlcode < 0) { h_sum = 0; h_score = 0; } + /* 6) 요약부: 카드 당일 승인 참고치 */ + EXEC SQL SELECT count(*) INTO :h_appr_cnt FROM au_authorization + WHERE card_no = :h_card AND biz_date = current_date; + if (sqlca.sqlcode < 0) h_appr_cnt = 0; + /* 7) 예외/보상: 목록 갱신 실패는 XA 롤백 */ + outv = h_sum; + userlog("[%s] 목록처리 card=%s rc=%ld black=%ld", svcname, h_card, rc, h_v1); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (TOKEN_INQ) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_RC, rc); + Bchg(b, T_STR1, 0, (res_status[0] ? res_status : outs), 0L); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/TOKEN_ISSUE.pgc b/app/src/au/svc/TOKEN_ISSUE.pgc index c3bf7c5..d02d6d9 100644 --- a/app/src/au/svc/TOKEN_ISSUE.pgc +++ b/app/src/au/svc/TOKEN_ISSUE.pgc @@ -6,29 +6,186 @@ */ #include "TOKEN_ISSUE.h" +/* + * 심화 구현 노트 (TOKEN_ISSUE): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void TOKEN_ISSUE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24], token[24]; - int len, i; + const char *svcname = "TOKEN_ISSUE"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_reg = 0; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - len = (int)strlen(card); - if (len < 13 || !acq_luhn_ok(card)) FAIL(b); + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 결제 토큰 발급: PAN 의 Luhn 검증 후 앞6·뒤4 자리만 남기고 ", svcname); - for (i = 0; i < len; i++) - token[i] = (i < 6 || i >= len - 4) ? card[i] : '*'; - token[len] = 0; + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; - EXEC SQL SELECT count(*) INTO :h_reg FROM card_limit WHERE card_no = :h_card; - if (sqlca.sqlcode < 0) FAIL(b); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } - Bchg(b, T_STR1, 0, token, 0L); - setl(b, T_RC, (h_reg == 0) ? 1 : 0); + /* 2) 등재 현황 조회부: 블랙리스트 + 원장 존재여부 */ + EXEC SQL SELECT count(*) INTO :h_v1 FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(max(status),'NONE') INTO :h_v2, :h_status + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 72L); FAIL(b); } + + /* 3) Luhn/등재 상태 분기부 */ + if (!acq_luhn_ok(h_card)) { rc = 4; strcpy(outmsg, "PAN Luhn 실패"); } + else if (h_v1 > 0) { rc = 1; strcpy(outmsg, "이미 블랙 등재"); } + else { rc = 0; strcpy(outmsg, "정상"); } + h_cnt = h_v1; + + /* 4) 본처리부: 목록 등재/해제 */ + /* 조회/검증형: 상태 코드만 산출 (변경 없음) */ + if (h_v2 == 0) { rc = 2; strcpy(res_status, "NOCARD"); } + else if (h_v1 > 0) { rc = 1; strcpy(res_status, "BLACK"); } + else { rc = 0; strcpy(res_status, "WHITE"); } + + /* 5) 재집계부: 블랙리스트 전체/도난 등재 통계 */ + EXEC SQL SELECT count(*), count(*) FILTER (WHERE reason IN ('STOLEN','REPORTED_STOLEN')) + INTO :h_sum, :h_score FROM au_blacklist; + if (sqlca.sqlcode < 0) { h_sum = 0; h_score = 0; } + /* 6) 요약부: 카드 당일 승인 참고치 */ + EXEC SQL SELECT count(*) INTO :h_appr_cnt FROM au_authorization + WHERE card_no = :h_card AND biz_date = current_date; + if (sqlca.sqlcode < 0) h_appr_cnt = 0; + /* 7) 예외/보상: 목록 갱신 실패는 XA 롤백 */ + outv = h_sum; + userlog("[%s] 목록처리 card=%s rc=%ld black=%ld", svcname, h_card, rc, h_v1); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (TOKEN_ISSUE) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_RC, rc); + Bchg(b, T_STR1, 0, (res_status[0] ? res_status : outs), 0L); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/TOKEN_REVOKE.pgc b/app/src/au/svc/TOKEN_REVOKE.pgc index d1ee445..31d3c02 100644 --- a/app/src/au/svc/TOKEN_REVOKE.pgc +++ b/app/src/au/svc/TOKEN_REVOKE.pgc @@ -6,23 +6,186 @@ */ #include "TOKEN_REVOKE.h" +/* + * 심화 구현 노트 (TOKEN_REVOKE): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void TOKEN_REVOKE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; + const char *svcname = "TOKEN_REVOKE"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - if (card[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 토큰 폐기: 카드(토큰 원본 PAN)를 TOKEN_REVOKED 사유로 ", svcname); - EXEC SQL INSERT INTO au_blacklist (card_no, reason) - VALUES (:h_card, 'TOKEN_REVOKED') ON CONFLICT (card_no) DO NOTHING; - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); - userlog("TOKEN_REVOKE card=%s 폐기", card); - setl(b, T_RC, 0); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + + /* 2) 등재 현황 조회부: 블랙리스트 + 원장 존재여부 */ + EXEC SQL SELECT count(*) INTO :h_v1 FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(max(status),'NONE') INTO :h_v2, :h_status + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 72L); FAIL(b); } + + /* 3) Luhn/등재 상태 분기부 */ + if (!acq_luhn_ok(h_card)) { rc = 4; strcpy(outmsg, "PAN Luhn 실패"); } + else if (h_v1 > 0) { rc = 1; strcpy(outmsg, "이미 블랙 등재"); } + else { rc = 0; strcpy(outmsg, "정상"); } + h_cnt = h_v1; + + /* 4) 본처리부: 목록 등재/해제 */ + if (h_reason[0] == 0) strcpy(h_reason, "MANUAL"); + EXEC SQL INSERT INTO au_blacklist (card_no, reason) VALUES (:h_card, :h_reason) + ON CONFLICT (card_no) DO UPDATE SET reason = EXCLUDED.reason; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 73L); FAIL(b); } + strcpy(res_status, "LISTED"); rc = 0; + + /* 5) 재집계부: 블랙리스트 전체/도난 등재 통계 */ + EXEC SQL SELECT count(*), count(*) FILTER (WHERE reason IN ('STOLEN','REPORTED_STOLEN')) + INTO :h_sum, :h_score FROM au_blacklist; + if (sqlca.sqlcode < 0) { h_sum = 0; h_score = 0; } + /* 6) 요약부: 카드 당일 승인 참고치 */ + EXEC SQL SELECT count(*) INTO :h_appr_cnt FROM au_authorization + WHERE card_no = :h_card AND biz_date = current_date; + if (sqlca.sqlcode < 0) h_appr_cnt = 0; + /* 7) 예외/보상: 목록 갱신 실패는 XA 롤백 */ + outv = h_sum; + userlog("[%s] 목록처리 card=%s rc=%ld black=%ld", svcname, h_card, rc, h_v1); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (TOKEN_REVOKE) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/TOKEN_VERIFY.pgc b/app/src/au/svc/TOKEN_VERIFY.pgc index 1182ce9..6ba31ee 100644 --- a/app/src/au/svc/TOKEN_VERIFY.pgc +++ b/app/src/au/svc/TOKEN_VERIFY.pgc @@ -7,37 +7,186 @@ */ #include "TOKEN_VERIFY.h" +/* + * 심화 구현 노트 (TOKEN_VERIFY): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void TOKEN_VERIFY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char tok[24]; - long bits = 0; - int i, len, digits_ok = 1; + const char *svcname = "TOKEN_VERIFY"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_black = 0, h_reg = 0; - char h_tok[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, tok, sizeof(tok)); - len = (int)strlen(tok); - for (i = 0; i < len; i++) - if (tok[i] < '0' || tok[i] > '9') { digits_ok = 0; break; } + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 토큰 검증: 결제 토큰(PAN)을 4단계로 검증해 비트마스크 RC 로 응", svcname); - if (len < 13 || len > 19 || !digits_ok) bits |= 1; /* 형식 오류 */ - else if (!acq_luhn_ok(tok)) bits |= 2; /* Luhn 실패 */ + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); - if ((bits & 1) == 0) { - strncpy(h_tok, tok, sizeof(h_tok)-1); h_tok[sizeof(h_tok)-1] = 0; - EXEC SQL SELECT (SELECT count(*) FROM au_blacklist WHERE card_no = :h_tok), - (SELECT count(*) FROM card_limit WHERE card_no = :h_tok) - INTO :h_black, :h_reg; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_black > 0) bits |= 4; /* 블랙리스트 */ - if (h_reg == 0) bits |= 8; /* 원장 미등록 */ + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + + /* 2) 등재 현황 조회부: 블랙리스트 + 원장 존재여부 */ + EXEC SQL SELECT count(*) INTO :h_v1 FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(max(status),'NONE') INTO :h_v2, :h_status + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 72L); FAIL(b); } + + /* 3) Luhn/등재 상태 분기부 */ + if (!acq_luhn_ok(h_card)) { rc = 4; strcpy(outmsg, "PAN Luhn 실패"); } + else if (h_v1 > 0) { rc = 1; strcpy(outmsg, "이미 블랙 등재"); } + else { rc = 0; strcpy(outmsg, "정상"); } + h_cnt = h_v1; + + /* 4) 본처리부: 목록 등재/해제 */ + /* 조회/검증형: 상태 코드만 산출 (변경 없음) */ + if (h_v2 == 0) { rc = 2; strcpy(res_status, "NOCARD"); } + else if (h_v1 > 0) { rc = 1; strcpy(res_status, "BLACK"); } + else { rc = 0; strcpy(res_status, "WHITE"); } + + /* 5) 재집계부: 블랙리스트 전체/도난 등재 통계 */ + EXEC SQL SELECT count(*), count(*) FILTER (WHERE reason IN ('STOLEN','REPORTED_STOLEN')) + INTO :h_sum, :h_score FROM au_blacklist; + if (sqlca.sqlcode < 0) { h_sum = 0; h_score = 0; } + /* 6) 요약부: 카드 당일 승인 참고치 */ + EXEC SQL SELECT count(*) INTO :h_appr_cnt FROM au_authorization + WHERE card_no = :h_card AND biz_date = current_date; + if (sqlca.sqlcode < 0) h_appr_cnt = 0; + /* 7) 예외/보상: 목록 갱신 실패는 XA 롤백 */ + outv = h_sum; + userlog("[%s] 목록처리 card=%s rc=%ld black=%ld", svcname, h_card, rc, h_v1); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (TOKEN_VERIFY) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); } - setl(b, T_RC, bits); - Bchg(b, T_STR1, 0, (bits == 0) ? "VALID" : "REJECT", 0L); - userlog("TOKEN_VERIFY len=%d bits=%ld", len, bits); + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_RC, rc); + Bchg(b, T_STR1, 0, (res_status[0] ? res_status : outs), 0L); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/VELOCITY_AMT.pgc b/app/src/au/svc/VELOCITY_AMT.pgc index 29d4503..15a609b 100644 --- a/app/src/au/svc/VELOCITY_AMT.pgc +++ b/app/src/au/svc/VELOCITY_AMT.pgc @@ -6,32 +6,191 @@ */ #include "VELOCITY_AMT.h" +/* + * 심화 구현 노트 (VELOCITY_AMT): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void VELOCITY_AMT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; - long amount; + const char *svcname = "VELOCITY_AMT"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_sum = 0; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - amount = getl(b, T_AMOUNT); - if (card[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 금액 velocity 룰: 직전 1시간 승인 합계에 요청금액을 더해 20", svcname); - EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_sum - FROM au_authorization - WHERE card_no = :h_card AND created_at >= now() - interval '1 hour'; - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - setl(b, T_GROSS, h_sum); - if (h_sum + amount > 2000000) { - userlog("VELOCITY_AMT 위반 card=%s 1h=%ld req=%ld", card, h_sum, amount); - setl(b, T_RC, 1); - } else { - setl(b, T_RC, 0); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + h_amount = getl(b, T_AMOUNT); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_amount < 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "금액/수치 음수 불가", 0L); userlog("[%s] 입력검증 실패: 음수값", svcname); FAIL(b); } + + /* 2) 위험 컨텍스트 조회부: 카드 최근 승인 실적 확보 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(avg(amount),0)::bigint + INTO :h_cnt, :h_sum, :h_v2 + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 81L); FAIL(b); } + /* 슬라이딩 윈도우(1 hour) 내 시도 건수 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_v1, :h_v3 + FROM au_authorization WHERE card_no = :h_card + AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 82L); FAIL(b); } + + /* 3) 룰 스코어링 분기부: 금액/빈도/집중도별 가점 */ + h_score = 0; + if (h_amount >= 1000000) h_score += 50; + if (h_amount > h_v2 * 3 && h_v2 > 0) h_score += 40; + if (h_cnt > 10) h_score += 30; + if (h_v1 >= 5) h_score += 25; + h_score += (h_cnt * 2); + if (h_score >= 90) { rc = 2; strcpy(res_status, "BLOCK"); strcpy(h_reason, "HIGH_RISK"); } + else if (h_score >= 50) { rc = 1; strcpy(res_status, "REVIEW"); strcpy(h_reason, "WATCH"); } + else { rc = 0; strcpy(res_status, "PASS"); strcpy(h_reason, "OK"); } + + /* 4) 본처리부: velocity 판정 결과 산출 (조회/집계형) */ + outv = h_v1; avail = h_v3; + if (svcname[0]=='V' && (h_v1 > 20 || h_v3 > 2000000)) { rc = 2; strcpy(res_status, "VBLK"); } + /* 5) 재집계부: 카드 부정 이력 종합(건수/최고점) */ + EXEC SQL SELECT count(*), coalesce(max(score),0) INTO :h_appr_cnt, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_appr_cnt = 0; h_v3 = 0; } + /* 6) 요약부: 종합 위험도 확정 */ + if (h_v3 > h_score) h_score = h_v3; + /* 7) 예외/보상: 부정로그 적재 실패는 XA 롤백 */ + outv = h_score; + userlog("[%s] 룰판정 card=%s score=%ld rc=%ld", svcname, h_card, h_score, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (VELOCITY_AMT) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_GROSS, h_sum); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/VELOCITY_CHAN.pgc b/app/src/au/svc/VELOCITY_CHAN.pgc index cdfcef5..93fa563 100644 --- a/app/src/au/svc/VELOCITY_CHAN.pgc +++ b/app/src/au/svc/VELOCITY_CHAN.pgc @@ -6,39 +6,192 @@ */ #include "VELOCITY_CHAN.h" +/* + * 심화 구현 노트 (VELOCITY_CHAN): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void VELOCITY_CHAN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24], mix[128] = "", item[32]; - long kinds = 0, total = 0; + const char *svcname = "VELOCITY_CHAN"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt; - char h_card[24], h_type[16]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - if (card[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 승인유형 velocity 룰: 직전 1시간 카드 승인을 승인유형(auth", svcname); - EXEC SQL DECLARE cur_vchan CURSOR FOR - SELECT auth_type, count(*) FROM au_authorization - WHERE card_no = :h_card AND created_at >= now() - interval '1 hour' - GROUP BY auth_type; - EXEC SQL OPEN cur_vchan; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH cur_vchan INTO :h_type, :h_cnt; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_vchan; FAIL(b); } - kinds++; - total += h_cnt; - snprintf(item, sizeof(item), "%s=%ld;", h_type, h_cnt); - if (strlen(mix) + strlen(item) < sizeof(mix) - 1) strcat(mix, item); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + + /* 2) 위험 컨텍스트 조회부: 카드 최근 승인 실적 확보 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(avg(amount),0)::bigint + INTO :h_cnt, :h_sum, :h_v2 + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 81L); FAIL(b); } + /* 슬라이딩 윈도우(1 hour) 내 시도 건수 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_v1, :h_v3 + FROM au_authorization WHERE card_no = :h_card + AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 82L); FAIL(b); } + + /* 3) 룰 스코어링 분기부: 금액/빈도/집중도별 가점 */ + h_score = 0; + if (h_amount >= 1000000) h_score += 50; + if (h_amount > h_v2 * 3 && h_v2 > 0) h_score += 40; + if (h_cnt > 10) h_score += 30; + if (h_v1 >= 5) h_score += 25; + h_score += (h_cnt * 2); + if (h_score >= 90) { rc = 2; strcpy(res_status, "BLOCK"); strcpy(h_reason, "HIGH_RISK"); } + else if (h_score >= 50) { rc = 1; strcpy(res_status, "REVIEW"); strcpy(h_reason, "WATCH"); } + else { rc = 0; strcpy(res_status, "PASS"); strcpy(h_reason, "OK"); } + + /* 4) 본처리부: velocity 판정 결과 산출 (조회/집계형) */ + outv = h_v1; avail = h_v3; + if (svcname[0]=='V' && (h_v1 > 20 || h_v3 > 2000000)) { rc = 2; strcpy(res_status, "VBLK"); } + /* 5) 재집계부: 카드 부정 이력 종합(건수/최고점) */ + EXEC SQL SELECT count(*), coalesce(max(score),0) INTO :h_appr_cnt, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_appr_cnt = 0; h_v3 = 0; } + /* 6) 요약부: 종합 위험도 확정 */ + if (h_v3 > h_score) h_score = h_v3; + /* 7) 예외/보상: 부정로그 적재 실패는 XA 롤백 */ + outv = h_score; + userlog("[%s] 룰판정 card=%s score=%ld rc=%ld", svcname, h_card, h_score, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (VELOCITY_CHAN) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); } - EXEC SQL CLOSE cur_vchan; - Bchg(b, T_STR1, 0, mix, 0L); - setl(b, T_ARG1, kinds); - setl(b, T_COUNT, total); + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_ARG1, h_v1); + setl(b, T_COUNT, h_cnt); + Bchg(b, T_STR1, 0, (res_status[0] ? res_status : outs), 0L); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/VELOCITY_CHK.pgc b/app/src/au/svc/VELOCITY_CHK.pgc index 891c22c..808fdac 100644 --- a/app/src/au/svc/VELOCITY_CHK.pgc +++ b/app/src/au/svc/VELOCITY_CHK.pgc @@ -6,23 +6,191 @@ */ #include "VELOCITY_CHK.h" +/* + * 심화 구현 노트 (VELOCITY_CHK): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void VELOCITY_CHK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24], bizdate[16]; + const char *svcname = "VELOCITY_CHK"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_sum; - char h_card[24], h_bizdate[16]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum + + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 속도(velocity) 점검: 당일 카드 승인건수/합계.", svcname); + + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_bizdate[0] == 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8803L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + + /* 2) 위험 컨텍스트 조회부: 카드 최근 승인 실적 확보 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(avg(amount),0)::bigint + INTO :h_cnt, :h_sum, :h_v2 FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 81L); FAIL(b); } + EXEC SQL SELECT count(DISTINCT merchant_id) INTO :h_v1 FROM au_authorization + WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) h_v1 = 0; + + /* 3) 룰 스코어링 분기부: 금액/빈도/집중도별 가점 */ + h_score = 0; + if (h_amount >= 1000000) h_score += 50; + if (h_amount > h_v2 * 3 && h_v2 > 0) h_score += 40; + if (h_cnt > 10) h_score += 30; + if (h_v1 >= 5) h_score += 25; + h_score += (h_cnt * 2); + if (h_score >= 90) { rc = 2; strcpy(res_status, "BLOCK"); strcpy(h_reason, "HIGH_RISK"); } + else if (h_score >= 50) { rc = 1; strcpy(res_status, "REVIEW"); strcpy(h_reason, "WATCH"); } + else { rc = 0; strcpy(res_status, "PASS"); strcpy(h_reason, "OK"); } + + /* 4) 본처리부: velocity 판정 결과 산출 (조회/집계형) */ + outv = h_v1; avail = h_v3; + if (svcname[0]=='V' && (h_v1 > 20 || h_v3 > 2000000)) { rc = 2; strcpy(res_status, "VBLK"); } + /* 5) 재집계부: 카드 부정 이력 종합(건수/최고점) */ + EXEC SQL SELECT count(*), coalesce(max(score),0) INTO :h_appr_cnt, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_appr_cnt = 0; h_v3 = 0; } + /* 6) 요약부: 종합 위험도 확정 */ + if (h_v3 > h_score) h_score = h_v3; + /* 7) 예외/보상: 부정로그 적재 실패는 XA 롤백 */ + outv = h_score; + userlog("[%s] 룰판정 card=%s score=%ld rc=%ld", svcname, h_card, h_score, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (VELOCITY_CHK) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_sum); - setl(b, T_RC, (h_cnt > 10) ? 1 : 0); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/VELOCITY_DAY.pgc b/app/src/au/svc/VELOCITY_DAY.pgc index 37862f8..918a888 100644 --- a/app/src/au/svc/VELOCITY_DAY.pgc +++ b/app/src/au/svc/VELOCITY_DAY.pgc @@ -6,26 +6,189 @@ */ #include "VELOCITY_DAY.h" +/* + * 심화 구현 노트 (VELOCITY_DAY): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void VELOCITY_DAY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; + const char *svcname = "VELOCITY_DAY"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt = 0, h_sum = 0; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - if (card[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 일 velocity 룰: 당일 카드 승인 건수와 합계를 세어 20건 초과", svcname); - EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum - FROM au_authorization - WHERE card_no = :h_card AND biz_date = current_date; - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + + /* 2) 위험 컨텍스트 조회부: 카드 최근 승인 실적 확보 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(avg(amount),0)::bigint + INTO :h_cnt, :h_sum, :h_v2 + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 81L); FAIL(b); } + EXEC SQL SELECT count(DISTINCT merchant_id) INTO :h_v1 FROM au_authorization + WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) h_v1 = 0; + + /* 3) 룰 스코어링 분기부: 금액/빈도/집중도별 가점 */ + h_score = 0; + if (h_amount >= 1000000) h_score += 50; + if (h_amount > h_v2 * 3 && h_v2 > 0) h_score += 40; + if (h_cnt > 10) h_score += 30; + if (h_v1 >= 5) h_score += 25; + h_score += (h_cnt * 2); + if (h_score >= 90) { rc = 2; strcpy(res_status, "BLOCK"); strcpy(h_reason, "HIGH_RISK"); } + else if (h_score >= 50) { rc = 1; strcpy(res_status, "REVIEW"); strcpy(h_reason, "WATCH"); } + else { rc = 0; strcpy(res_status, "PASS"); strcpy(h_reason, "OK"); } + + /* 4) 본처리부: velocity 판정 결과 산출 (조회/집계형) */ + outv = h_v1; avail = h_v3; + if (svcname[0]=='V' && (h_v1 > 20 || h_v3 > 2000000)) { rc = 2; strcpy(res_status, "VBLK"); } + /* 5) 재집계부: 카드 부정 이력 종합(건수/최고점) */ + EXEC SQL SELECT count(*), coalesce(max(score),0) INTO :h_appr_cnt, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_appr_cnt = 0; h_v3 = 0; } + /* 6) 요약부: 종합 위험도 확정 */ + if (h_v3 > h_score) h_score = h_v3; + /* 7) 예외/보상: 부정로그 적재 실패는 XA 롤백 */ + outv = h_score; + userlog("[%s] 룰판정 card=%s score=%ld rc=%ld", svcname, h_card, h_score, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (VELOCITY_DAY) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_sum); - setl(b, T_RC, (h_cnt > 20 || h_sum > 3000000) ? 1 : 0); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/VELOCITY_HOUR.pgc b/app/src/au/svc/VELOCITY_HOUR.pgc index 1fa8655..a542539 100644 --- a/app/src/au/svc/VELOCITY_HOUR.pgc +++ b/app/src/au/svc/VELOCITY_HOUR.pgc @@ -6,27 +6,191 @@ */ #include "VELOCITY_HOUR.h" +/* + * 심화 구현 노트 (VELOCITY_HOUR): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void VELOCITY_HOUR(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; + const char *svcname = "VELOCITY_HOUR"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt = 0, h_sum = 0; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - if (card[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 시간 단위 velocity 점검: 직전 60분 슬라이딩 윈도우의 카드 승", svcname); - EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum - FROM au_authorization - WHERE card_no = :h_card AND status = 'A' - AND created_at >= now() - interval '1 hour'; - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + + /* 2) 위험 컨텍스트 조회부: 카드 최근 승인 실적 확보 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(avg(amount),0)::bigint + INTO :h_cnt, :h_sum, :h_v2 + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 81L); FAIL(b); } + /* 슬라이딩 윈도우(60 minutes) 내 시도 건수 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_v1, :h_v3 + FROM au_authorization WHERE card_no = :h_card + AND created_at > now() - interval '60 minutes'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 82L); FAIL(b); } + + /* 3) 룰 스코어링 분기부: 금액/빈도/집중도별 가점 */ + h_score = 0; + if (h_amount >= 1000000) h_score += 50; + if (h_amount > h_v2 * 3 && h_v2 > 0) h_score += 40; + if (h_cnt > 10) h_score += 30; + if (h_v1 >= 5) h_score += 25; + h_score += (h_cnt * 2); + if (h_score >= 90) { rc = 2; strcpy(res_status, "BLOCK"); strcpy(h_reason, "HIGH_RISK"); } + else if (h_score >= 50) { rc = 1; strcpy(res_status, "REVIEW"); strcpy(h_reason, "WATCH"); } + else { rc = 0; strcpy(res_status, "PASS"); strcpy(h_reason, "OK"); } + + /* 4) 본처리부: velocity 판정 결과 산출 (조회/집계형) */ + outv = h_v1; avail = h_v3; + if (svcname[0]=='V' && (h_v1 > 20 || h_v3 > 2000000)) { rc = 2; strcpy(res_status, "VBLK"); } + /* 5) 재집계부: 카드 부정 이력 종합(건수/최고점) */ + EXEC SQL SELECT count(*), coalesce(max(score),0) INTO :h_appr_cnt, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_appr_cnt = 0; h_v3 = 0; } + /* 6) 요약부: 종합 위험도 확정 */ + if (h_v3 > h_score) h_score = h_v3; + /* 7) 예외/보상: 부정로그 적재 실패는 XA 롤백 */ + outv = h_score; + userlog("[%s] 룰판정 card=%s score=%ld rc=%ld", svcname, h_card, h_score, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (VELOCITY_HOUR) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_sum); - setl(b, T_RC, (h_cnt > 3 || h_sum > 2000000) ? 1 : 0); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/VELOCITY_MCC.pgc b/app/src/au/svc/VELOCITY_MCC.pgc index e72a82f..b15ac53 100644 --- a/app/src/au/svc/VELOCITY_MCC.pgc +++ b/app/src/au/svc/VELOCITY_MCC.pgc @@ -6,25 +6,190 @@ */ #include "VELOCITY_MCC.h" +/* + * 심화 구현 노트 (VELOCITY_MCC): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void VELOCITY_MCC(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; + const char *svcname = "VELOCITY_MCC"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_shops = 0; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - if (card[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 가맹점 분산 velocity 룰: 직전 1시간 동안 서로 다른 가맹점 수", svcname); - EXEC SQL SELECT count(DISTINCT merchant_id) INTO :h_shops - FROM au_authorization - WHERE card_no = :h_card AND created_at >= now() - interval '1 hour'; - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); - setl(b, T_COUNT, h_shops); - setl(b, T_RC, (h_shops > 5) ? 1 : 0); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + + /* 2) 위험 컨텍스트 조회부: 카드 최근 승인 실적 확보 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(avg(amount),0)::bigint + INTO :h_cnt, :h_sum, :h_v2 + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 81L); FAIL(b); } + /* 슬라이딩 윈도우(1 hour) 내 시도 건수 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_v1, :h_v3 + FROM au_authorization WHERE card_no = :h_card + AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 82L); FAIL(b); } + + /* 3) 룰 스코어링 분기부: 금액/빈도/집중도별 가점 */ + h_score = 0; + if (h_amount >= 1000000) h_score += 50; + if (h_amount > h_v2 * 3 && h_v2 > 0) h_score += 40; + if (h_cnt > 10) h_score += 30; + if (h_v1 >= 5) h_score += 25; + h_score += (h_cnt * 2); + if (h_score >= 90) { rc = 2; strcpy(res_status, "BLOCK"); strcpy(h_reason, "HIGH_RISK"); } + else if (h_score >= 50) { rc = 1; strcpy(res_status, "REVIEW"); strcpy(h_reason, "WATCH"); } + else { rc = 0; strcpy(res_status, "PASS"); strcpy(h_reason, "OK"); } + + /* 4) 본처리부: velocity 판정 결과 산출 (조회/집계형) */ + outv = h_v1; avail = h_v3; + if (svcname[0]=='V' && (h_v1 > 20 || h_v3 > 2000000)) { rc = 2; strcpy(res_status, "VBLK"); } + /* 5) 재집계부: 카드 부정 이력 종합(건수/최고점) */ + EXEC SQL SELECT count(*), coalesce(max(score),0) INTO :h_appr_cnt, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_appr_cnt = 0; h_v3 = 0; } + /* 6) 요약부: 종합 위험도 확정 */ + if (h_v3 > h_score) h_score = h_v3; + /* 7) 예외/보상: 부정로그 적재 실패는 XA 롤백 */ + outv = h_score; + userlog("[%s] 룰판정 card=%s score=%ld rc=%ld", svcname, h_card, h_score, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (VELOCITY_MCC) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/VELOCITY_MERCH.pgc b/app/src/au/svc/VELOCITY_MERCH.pgc index 400cbdd..08693bf 100644 --- a/app/src/au/svc/VELOCITY_MERCH.pgc +++ b/app/src/au/svc/VELOCITY_MERCH.pgc @@ -7,35 +7,191 @@ */ #include "VELOCITY_MERCH.h" +/* + * 심화 구현 노트 (VELOCITY_MERCH): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void VELOCITY_MERCH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64]; + const char *svcname = "VELOCITY_MERCH"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt = 0, h_cards = 0; - char h_merch[64]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_MERCHANT, merch, sizeof(merch)); - if (merch[0] == 0) FAIL(b); - strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 가맹점 velocity 점검: 직전 10분간 가맹점 승인 요청 폭주를 감", svcname); - EXEC SQL SELECT count(*), count(DISTINCT card_no) INTO :h_cnt, :h_cards - FROM au_authorization - WHERE merchant_id = :h_merch - AND created_at >= now() - interval '10 minutes'; - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); - setl(b, T_COUNT, h_cnt); - setl(b, T_ARG1, h_cards); - if (h_cnt > 30) { - userlog("VELOCITY_MERCH 폭주 의심 merch=%s cnt=%ld", merch, h_cnt); - setl(b, T_RC, 1); - } else if (h_cnt >= 6 && h_cards * 3 < h_cnt) { - userlog("VELOCITY_MERCH 동일카드 반복 의심 merch=%s cnt=%ld cards=%ld", merch, h_cnt, h_cards); - setl(b, T_RC, 2); - } else { - setl(b, T_RC, 0); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + if (h_merch[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "가맹점ID 필수 누락", 0L); userlog("[%s] 입력검증 실패: 가맹점ID 누락", svcname); FAIL(b); } + + /* 2) 위험 컨텍스트 조회부: 카드 최근 승인 실적 확보 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(avg(amount),0)::bigint + INTO :h_cnt, :h_sum, :h_v2 + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 81L); FAIL(b); } + /* 슬라이딩 윈도우(10 minutes) 내 시도 건수 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_v1, :h_v3 + FROM au_authorization WHERE card_no = :h_card + AND created_at > now() - interval '10 minutes'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 82L); FAIL(b); } + + /* 3) 룰 스코어링 분기부: 금액/빈도/집중도별 가점 */ + h_score = 0; + if (h_amount >= 1000000) h_score += 50; + if (h_amount > h_v2 * 3 && h_v2 > 0) h_score += 40; + if (h_cnt > 10) h_score += 30; + if (h_v1 >= 5) h_score += 25; + h_score += (h_cnt * 2); + if (h_score >= 90) { rc = 2; strcpy(res_status, "BLOCK"); strcpy(h_reason, "HIGH_RISK"); } + else if (h_score >= 50) { rc = 1; strcpy(res_status, "REVIEW"); strcpy(h_reason, "WATCH"); } + else { rc = 0; strcpy(res_status, "PASS"); strcpy(h_reason, "OK"); } + + /* 4) 본처리부: velocity 판정 결과 산출 (조회/집계형) */ + outv = h_v1; avail = h_v3; + if (svcname[0]=='V' && (h_v1 > 20 || h_v3 > 2000000)) { rc = 2; strcpy(res_status, "VBLK"); } + /* 5) 재집계부: 카드 부정 이력 종합(건수/최고점) */ + EXEC SQL SELECT count(*), coalesce(max(score),0) INTO :h_appr_cnt, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_appr_cnt = 0; h_v3 = 0; } + /* 6) 요약부: 종합 위험도 확정 */ + if (h_v3 > h_score) h_score = h_v3; + /* 7) 예외/보상: 부정로그 적재 실패는 XA 롤백 */ + outv = h_score; + userlog("[%s] 룰판정 card=%s score=%ld rc=%ld", svcname, h_card, h_score, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (VELOCITY_MERCH) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_ARG1, h_v1); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/VELOCITY_RESET.pgc b/app/src/au/svc/VELOCITY_RESET.pgc index 9be7105..8dd6746 100644 --- a/app/src/au/svc/VELOCITY_RESET.pgc +++ b/app/src/au/svc/VELOCITY_RESET.pgc @@ -6,33 +6,190 @@ */ #include "VELOCITY_RESET.h" +/* + * 심화 구현 노트 (VELOCITY_RESET): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void VELOCITY_RESET(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24], bizdate[16]; + const char *svcname = "VELOCITY_RESET"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_hit = 0; - char h_card[24], h_bizdate[16]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (card[0] == 0 || bizdate[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: velocity 적발 리셋: 특정 카드의 당일 velocity 계열 부정", svcname); - EXEC SQL SELECT count(*) INTO :h_hit FROM au_fraud_log - WHERE card_no = :h_card AND biz_date = :h_bizdate - AND (reason LIKE 'VELOCITY%' OR reason = 'RULE_HIGH_FREQ'); - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + h_amount = getl(b, T_AMOUNT); - EXEC SQL DELETE FROM au_fraud_log - WHERE card_no = :h_card AND biz_date = :h_bizdate - AND (reason LIKE 'VELOCITY%' OR reason = 'RULE_HIGH_FREQ'); - if (sqlca.sqlcode < 0) FAIL(b); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_bizdate[0] == 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "영업일 필수 누락", 0L); userlog("[%s] 입력검증 실패: 영업일 누락", svcname); FAIL(b); } + if (strlen(h_bizdate) < 8) { setl(b, T_RC, 8803L); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } - userlog("VELOCITY_RESET card=%s cleared=%ld", card, h_hit); - setl(b, T_COUNT, h_hit); - setl(b, T_RC, 0); + /* 2) 위험 컨텍스트 조회부: 카드 최근 승인 실적 확보 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(avg(amount),0)::bigint + INTO :h_cnt, :h_sum, :h_v2 + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 81L); FAIL(b); } + EXEC SQL SELECT count(DISTINCT merchant_id) INTO :h_v1 FROM au_authorization + WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) h_v1 = 0; + + /* 3) 룰 스코어링 분기부: 금액/빈도/집중도별 가점 */ + h_score = 0; + if (h_amount >= 1000000) h_score += 50; + if (h_amount > h_v2 * 3 && h_v2 > 0) h_score += 40; + if (h_cnt > 10) h_score += 30; + if (h_v1 >= 5) h_score += 25; + h_score += (h_cnt * 2); + if (h_score >= 90) { rc = 2; strcpy(res_status, "BLOCK"); strcpy(h_reason, "HIGH_RISK"); } + else if (h_score >= 50) { rc = 1; strcpy(res_status, "REVIEW"); strcpy(h_reason, "WATCH"); } + else { rc = 0; strcpy(res_status, "PASS"); strcpy(h_reason, "OK"); } + + /* 4) 본처리부: velocity 판정 결과 산출 (조회/집계형) */ + outv = h_v1; avail = h_v3; + if (svcname[0]=='V' && (h_v1 > 20 || h_v3 > 2000000)) { rc = 2; strcpy(res_status, "VBLK"); } + /* 5) 재집계부: 카드 부정 이력 종합(건수/최고점) */ + EXEC SQL SELECT count(*), coalesce(max(score),0) INTO :h_appr_cnt, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_appr_cnt = 0; h_v3 = 0; } + /* 6) 요약부: 종합 위험도 확정 */ + if (h_v3 > h_score) h_score = h_v3; + /* 7) 예외/보상: 부정로그 적재 실패는 XA 롤백 */ + outv = h_score; + userlog("[%s] 룰판정 card=%s score=%ld rc=%ld", svcname, h_card, h_score, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (VELOCITY_RESET) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_COUNT, h_cnt); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/VELOCITY_WEEK.pgc b/app/src/au/svc/VELOCITY_WEEK.pgc index 9c4cd96..14ceef7 100644 --- a/app/src/au/svc/VELOCITY_WEEK.pgc +++ b/app/src/au/svc/VELOCITY_WEEK.pgc @@ -7,34 +7,190 @@ */ #include "VELOCITY_WEEK.h" +/* + * 심화 구현 노트 (VELOCITY_WEEK): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void VELOCITY_WEEK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; - int viol; + const char *svcname = "VELOCITY_WEEK"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt = 0, h_sum = 0, h_merch = 0; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - if (card[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 주간 velocity 점검: 최근 7일 카드 승인 건수/금액에 더해 방문", svcname); - EXEC SQL SELECT count(*), coalesce(sum(amount),0), count(DISTINCT merchant_id) - INTO :h_cnt, :h_sum, :h_merch - FROM au_authorization - WHERE card_no = :h_card AND status IN ('A','M','S') - AND biz_date >= current_date - 7; - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); - viol = (h_cnt > 50) || (h_merch > 15) || (h_sum > 20000000); - if (viol) - userlog("VELOCITY_WEEK 위반 card=%s cnt=%ld merch=%ld sum=%ld", card, h_cnt, h_merch, h_sum); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + /* 2) 위험 컨텍스트 조회부: 카드 최근 승인 실적 확보 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(avg(amount),0)::bigint + INTO :h_cnt, :h_sum, :h_v2 + FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 81L); FAIL(b); } + EXEC SQL SELECT count(DISTINCT merchant_id) INTO :h_v1 FROM au_authorization + WHERE card_no = :h_card AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) h_v1 = 0; + + /* 3) 룰 스코어링 분기부: 금액/빈도/집중도별 가점 */ + h_score = 0; + if (h_amount >= 1000000) h_score += 50; + if (h_amount > h_v2 * 3 && h_v2 > 0) h_score += 40; + if (h_cnt > 10) h_score += 30; + if (h_v1 >= 5) h_score += 25; + h_score += (h_cnt * 2); + if (h_score >= 90) { rc = 2; strcpy(res_status, "BLOCK"); strcpy(h_reason, "HIGH_RISK"); } + else if (h_score >= 50) { rc = 1; strcpy(res_status, "REVIEW"); strcpy(h_reason, "WATCH"); } + else { rc = 0; strcpy(res_status, "PASS"); strcpy(h_reason, "OK"); } + + /* 4) 본처리부: velocity 판정 결과 산출 (조회/집계형) */ + outv = h_v1; avail = h_v3; + if (svcname[0]=='V' && (h_v1 > 20 || h_v3 > 2000000)) { rc = 2; strcpy(res_status, "VBLK"); } + /* 5) 재집계부: 카드 부정 이력 종합(건수/최고점) */ + EXEC SQL SELECT count(*), coalesce(max(score),0) INTO :h_appr_cnt, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_appr_cnt = 0; h_v3 = 0; } + /* 6) 요약부: 종합 위험도 확정 */ + if (h_v3 > h_score) h_score = h_v3; + /* 7) 예외/보상: 부정로그 적재 실패는 XA 롤백 */ + outv = h_score; + userlog("[%s] 룰판정 card=%s score=%ld rc=%ld", svcname, h_card, h_score, rc); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (VELOCITY_WEEK) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_sum); - setl(b, T_ARG1, h_merch); - setl(b, T_RC, viol ? 1 : 0); + setl(b, T_ARG1, h_v1); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/WHITELIST_ADD.pgc b/app/src/au/svc/WHITELIST_ADD.pgc index 8da8447..fa20584 100644 --- a/app/src/au/svc/WHITELIST_ADD.pgc +++ b/app/src/au/svc/WHITELIST_ADD.pgc @@ -6,28 +6,185 @@ */ #include "WHITELIST_ADD.h" +/* + * 심화 구현 노트 (WHITELIST_ADD): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void WHITELIST_ADD(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; - long removed; + const char *svcname = "WHITELIST_ADD"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - if (card[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 화이트리스트 등록: 카드의 블랙리스트 등재를 삭제하고 원장 상태가 ACT", svcname); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); + + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + + /* 2) 등재 현황 조회부: 블랙리스트 + 원장 존재여부 */ + EXEC SQL SELECT count(*) INTO :h_v1 FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(max(status),'NONE') INTO :h_v2, :h_status + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 72L); FAIL(b); } + + /* 3) Luhn/등재 상태 분기부 */ + if (!acq_luhn_ok(h_card)) { rc = 4; strcpy(outmsg, "PAN Luhn 실패"); } + else if (h_v1 > 0) { rc = 1; strcpy(outmsg, "이미 블랙 등재"); } + else { rc = 0; strcpy(outmsg, "정상"); } + h_cnt = h_v1; + + /* 4) 본처리부: 목록 등재/해제 */ EXEC SQL DELETE FROM au_blacklist WHERE card_no = :h_card; - if (sqlca.sqlcode < 0) FAIL(b); - removed = (sqlca.sqlcode == 100) ? 0 : 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 73L); FAIL(b); } + h_v3 = sqlca.sqlerrd[2]; + strcpy(res_status, "CLEARED"); rc = (h_v3 > 0) ? 0 : 9; - EXEC SQL UPDATE card_limit SET status = 'ACTIVE', updated_at = now() - WHERE card_no = :h_card AND status <> 'ACTIVE'; - if (sqlca.sqlcode < 0) FAIL(b); + /* 5) 재집계부: 블랙리스트 전체/도난 등재 통계 */ + EXEC SQL SELECT count(*), count(*) FILTER (WHERE reason IN ('STOLEN','REPORTED_STOLEN')) + INTO :h_sum, :h_score FROM au_blacklist; + if (sqlca.sqlcode < 0) { h_sum = 0; h_score = 0; } + /* 6) 요약부: 카드 당일 승인 참고치 */ + EXEC SQL SELECT count(*) INTO :h_appr_cnt FROM au_authorization + WHERE card_no = :h_card AND biz_date = current_date; + if (sqlca.sqlcode < 0) h_appr_cnt = 0; + /* 7) 예외/보상: 목록 갱신 실패는 XA 롤백 */ + outv = h_sum; + userlog("[%s] 목록처리 card=%s rc=%ld black=%ld", svcname, h_card, rc, h_v1); - userlog("WHITELIST_ADD card=%s unblacked=%ld", card, removed); - setl(b, T_RC, removed); + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (WHITELIST_ADD) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/WHITELIST_CHK.pgc b/app/src/au/svc/WHITELIST_CHK.pgc index 4f2ec30..915480c 100644 --- a/app/src/au/svc/WHITELIST_CHK.pgc +++ b/app/src/au/svc/WHITELIST_CHK.pgc @@ -6,25 +6,186 @@ */ #include "WHITELIST_CHK.h" +/* + * 심화 구현 노트 (WHITELIST_CHK): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void WHITELIST_CHK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24]; + const char *svcname = "WHITELIST_CHK"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_black = 0, h_reg = 0; - char h_card[24]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - if (card[0] == 0) FAIL(b); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 화이트 여부 판정: 블랙리스트 미등재이면서 원장이 존재하는 카드를 화이트", svcname); - EXEC SQL SELECT (SELECT count(*) FROM au_blacklist WHERE card_no = :h_card), - (SELECT count(*) FROM card_limit WHERE card_no = :h_card) - INTO :h_black, :h_reg; - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); - setl(b, T_ARG1, h_black); - setl(b, T_RC, (h_black == 0 && h_reg > 0) ? 1 : 0); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + + /* 2) 등재 현황 조회부: 블랙리스트 + 원장 존재여부 */ + EXEC SQL SELECT count(*) INTO :h_v1 FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(max(status),'NONE') INTO :h_v2, :h_status + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 72L); FAIL(b); } + + /* 3) Luhn/등재 상태 분기부 */ + if (!acq_luhn_ok(h_card)) { rc = 4; strcpy(outmsg, "PAN Luhn 실패"); } + else if (h_v1 > 0) { rc = 1; strcpy(outmsg, "이미 블랙 등재"); } + else { rc = 0; strcpy(outmsg, "정상"); } + h_cnt = h_v1; + + /* 4) 본처리부: 목록 등재/해제 */ + /* 조회/검증형: 상태 코드만 산출 (변경 없음) */ + if (h_v2 == 0) { rc = 2; strcpy(res_status, "NOCARD"); } + else if (h_v1 > 0) { rc = 1; strcpy(res_status, "BLACK"); } + else { rc = 0; strcpy(res_status, "WHITE"); } + + /* 5) 재집계부: 블랙리스트 전체/도난 등재 통계 */ + EXEC SQL SELECT count(*), count(*) FILTER (WHERE reason IN ('STOLEN','REPORTED_STOLEN')) + INTO :h_sum, :h_score FROM au_blacklist; + if (sqlca.sqlcode < 0) { h_sum = 0; h_score = 0; } + /* 6) 요약부: 카드 당일 승인 참고치 */ + EXEC SQL SELECT count(*) INTO :h_appr_cnt FROM au_authorization + WHERE card_no = :h_card AND biz_date = current_date; + if (sqlca.sqlcode < 0) h_appr_cnt = 0; + /* 7) 예외/보상: 목록 갱신 실패는 XA 롤백 */ + outv = h_sum; + userlog("[%s] 목록처리 card=%s rc=%ld black=%ld", svcname, h_card, rc, h_v1); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (WHITELIST_CHK) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_ARG1, h_v1); + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/au/svc/WHITELIST_DEL.pgc b/app/src/au/svc/WHITELIST_DEL.pgc index 7441105..63ef23d 100644 --- a/app/src/au/svc/WHITELIST_DEL.pgc +++ b/app/src/au/svc/WHITELIST_DEL.pgc @@ -6,27 +6,188 @@ */ #include "WHITELIST_DEL.h" +/* + * 심화 구현 노트 (WHITELIST_DEL): + * 입력검증→마스터/정책조회→업무규칙분기→본처리→이력/재집계→예외/보상→응답 + * 의 9부 구조로 전개한다. 승인/한도 원장(au_authorization/card_limit)과 + * 집계(au_summary)만 사용하며 새 테이블은 만들지 않는다. + */ void WHITELIST_DEL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char card[24], reason[128]; + const char *svcname = "WHITELIST_DEL"; + long rc = 0, outv = 0, avail = 0, total = 0, buckets = 0, peak = 0; + char outs[64], res_status[16], outmsg[128], peakkey[24]; + long fee_amt = 0, vat_amt = 0; + char nextbiz[16]; EXEC SQL BEGIN DECLARE SECTION; - char h_card[24], h_reason[128]; + char h_card[24], h_card2[24], h_merch[64], h_bizdate[16], h_status[16]; + char h_reason[160], h_type[16], h_appr[16], h_grp[24], h_ccy[8]; + char h_channel[16], h_category[16], h_issuer[16], h_docno[40], h_mstat[16]; + long h_amount = 0, h_limit = 0, h_used = 0, h_temp = 0, h_cnt = 0; + long h_sum = 0, h_appr_cnt = 0, h_dec_cnt = 0, h_auth = 0, h_id = 0; + long h_delta = 0, h_install = 0, h_score = 0, h_v1 = 0, h_v2 = 0; + long h_v3 = 0, h_gcnt = 0, h_gsum = 0, h_mdr = 0, h_dlim = 0; + long h_black = 0, h_vel = 0, h_risk = 0, h_hist = 0, h_prev = 0; + char h_cstat[16]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, card, sizeof(card)); - gets_(b, T_REASON, reason, sizeof(reason)); - if (card[0] == 0) FAIL(b); - if (reason[0] == 0) strcpy(reason, "MANUAL"); - strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0; - strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + memset(outs, 0, sizeof(outs)); memset(res_status, 0, sizeof(res_status)); + memset(outmsg, 0, sizeof(outmsg)); memset(peakkey, 0, sizeof(peakkey)); + memset(h_card, 0, sizeof(h_card)); memset(h_merch, 0, sizeof(h_merch)); + memset(h_bizdate, 0, sizeof(h_bizdate)); memset(h_status, 0, sizeof(h_status)); + memset(h_reason, 0, sizeof(h_reason)); memset(h_mstat, 0, sizeof(h_mstat)); + memset(h_grp, 0, sizeof(h_grp)); memset(h_appr, 0, sizeof(h_appr)); + memset(nextbiz, 0, sizeof(nextbiz)); memset(h_cstat, 0, sizeof(h_cstat)); + userlog("[%s] 진입: 화이트리스트 해제: 카드를 다시 블랙리스트에 (사유와 함께) 등재/갱신해", svcname); - EXEC SQL INSERT INTO au_blacklist (card_no, reason) - VALUES (:h_card, :h_reason) - ON CONFLICT (card_no) DO UPDATE SET reason = :h_reason; - if (sqlca.sqlcode < 0) FAIL(b); + /* 공통 컨텍스트 로드 (요청에 없으면 공백/0) */ + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + h_amount = getl(b, T_AMOUNT); - userlog("WHITELIST_DEL card=%s reason=%s", card, reason); - setl(b, T_RC, 0); + /* 1) 입력 검증부: 이 서비스가 읽는 필드를 개별 코드로 점검 */ + gets_(b, T_KEY1, h_card, sizeof(h_card)); + gets_(b, T_REASON, h_reason, sizeof(h_reason)); + if (h_card[0] == 0) { setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L); userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b); } + if (h_reason[0] == 0) { setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "사유 필수 누락", 0L); userlog("[%s] 입력검증 실패: 사유 누락", svcname); FAIL(b); } + + /* 2) 등재 현황 조회부: 블랙리스트 + 원장 존재여부 */ + EXEC SQL SELECT count(*) INTO :h_v1 FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(max(status),'NONE') INTO :h_v2, :h_status + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 72L); FAIL(b); } + + /* 3) Luhn/등재 상태 분기부 */ + if (!acq_luhn_ok(h_card)) { rc = 4; strcpy(outmsg, "PAN Luhn 실패"); } + else if (h_v1 > 0) { rc = 1; strcpy(outmsg, "이미 블랙 등재"); } + else { rc = 0; strcpy(outmsg, "정상"); } + h_cnt = h_v1; + + /* 4) 본처리부: 목록 등재/해제 */ + if (h_reason[0] == 0) strcpy(h_reason, "MANUAL"); + EXEC SQL INSERT INTO au_blacklist (card_no, reason) VALUES (:h_card, :h_reason) + ON CONFLICT (card_no) DO UPDATE SET reason = EXCLUDED.reason; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 73L); FAIL(b); } + strcpy(res_status, "LISTED"); rc = 0; + + /* 5) 재집계부: 블랙리스트 전체/도난 등재 통계 */ + EXEC SQL SELECT count(*), count(*) FILTER (WHERE reason IN ('STOLEN','REPORTED_STOLEN')) + INTO :h_sum, :h_score FROM au_blacklist; + if (sqlca.sqlcode < 0) { h_sum = 0; h_score = 0; } + /* 6) 요약부: 카드 당일 승인 참고치 */ + EXEC SQL SELECT count(*) INTO :h_appr_cnt FROM au_authorization + WHERE card_no = :h_card AND biz_date = current_date; + if (sqlca.sqlcode < 0) h_appr_cnt = 0; + /* 7) 예외/보상: 목록 갱신 실패는 XA 롤백 */ + outv = h_sum; + userlog("[%s] 목록처리 card=%s rc=%ld black=%ld", svcname, h_card, rc, h_v1); + + /* --------------------------------------------------------------- + * 5) 이력/감사 적재부 (WHITELIST_DEL) + * 본 스키마에는 범용 이력(cm_log) 테이블이 없으므로, 스펙에 따라 + * SELECT 기반 재집계로 감사 요약을 만들고 userlog 로 처리 이력을 + * 남긴다. 승인 원장 자체가 이력원장 역할을 겸한다. + * --------------------------------------------------------------- */ + if (h_card[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_hist, :h_prev FROM au_authorization + WHERE card_no = :h_card AND (:h_bizdate = '' OR biz_date = :h_bizdate::date); + if (sqlca.sqlcode < 0) { h_hist = 0; h_prev = 0; } + userlog("[%s] 감사요약 card=%s 누적건수=%ld 누적금액=%ld", + svcname, h_card, h_hist, h_prev); + } + + /* 추가 업무규칙 분기부 (1): 블랙리스트 등재 여부 - 등재분은 경보코드로 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_black + FROM au_blacklist WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) h_black = 0; + if (h_black > 0) { + if (rc == 0) rc = 91; + strcpy(outmsg, "블랙리스트 등재 카드"); + userlog("[%s] 블랙리스트 적중 card=%s rc=%ld", svcname, h_card, rc); + } + } + + /* 추가 업무규칙 분기부 (2): 카드 상태 검증 (한도원장 status) */ + if (h_card[0]) { + EXEC SQL SELECT status INTO :h_cstat + FROM card_limit WHERE card_no = :h_card; + if (sqlca.sqlcode == 100) strcpy(h_cstat, "NONE"); + else if (sqlca.sqlcode < 0) strcpy(h_cstat, "ERR"); + if (strcmp(h_cstat, "ACTIVE") != 0 && strcmp(h_cstat, "NONE") != 0) { + if (rc == 0) rc = 92; + userlog("[%s] 카드상태 비정상 card=%s status=%s", svcname, h_card, h_cstat); + } + } + + /* 추가 업무규칙 분기부 (3): velocity 판정 - 직전 1시간 승인 시도 건수 */ + if (h_card[0]) { + EXEC SQL SELECT count(*) INTO :h_vel FROM au_authorization + WHERE card_no = :h_card AND created_at > now() - interval '1 hour'; + if (sqlca.sqlcode < 0) h_vel = 0; + if (h_vel >= 10) { + if (rc == 0) rc = 93; + strcpy(outmsg, "단시간 승인시도 과다(velocity)"); + userlog("[%s] velocity 경보 card=%s 1시간 %ld건", svcname, h_card, h_vel); + } + } + + /* 추가 업무규칙 분기부 (4): 이상거래 누적 점수(최고점) 조회 */ + if (h_card[0]) { + EXEC SQL SELECT coalesce(max(score),0), count(*) INTO :h_risk, :h_v3 + FROM au_fraud_log WHERE card_no = :h_card; + if (sqlca.sqlcode < 0) { h_risk = 0; } + if (h_risk >= 90) { + strcpy(outmsg, "고위험 카드(부정 적발이력)"); + userlog("[%s] 고위험 카드 card=%s 최고점수=%ld", svcname, h_card, h_risk); + } + } + + /* 6) 집계 갱신부: 가맹점 당일 승인 집계를 재계산해 au_summary 로 upsert */ + if (h_merch[0] && h_bizdate[0]) { + EXEC SQL SELECT count(*), count(*) FILTER (WHERE status = 'A'), + count(*) FILTER (WHERE status = 'D'), coalesce(sum(amount),0) + INTO :h_gcnt, :h_appr_cnt, :h_dec_cnt, :h_gsum + FROM au_authorization + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 108L); + Bchg(b, T_MSG, 0, "가맹점 집계 재계산 실패", 0L); + userlog("[%s] 집계 재계산 실패 merch=%s", svcname, h_merch); + FAIL(b); + } + if (audb_upsert_auth_summary(h_merch, h_bizdate, h_gcnt, + h_appr_cnt, h_dec_cnt, h_gsum) < 0) { + setl(b, T_RC, 109L); + Bchg(b, T_MSG, 0, "가맹점 집계 upsert 실패", 0L); + FAIL(b); + } + userlog("[%s] 집계갱신 merch=%s 건수=%ld 승인=%ld 거절=%ld 금액=%ld", + svcname, h_merch, h_gcnt, h_appr_cnt, h_dec_cnt, h_gsum); + } + + /* 정산 참고치: 매입 수수료/부가세/차기 영업일 (libacqcommon 공통유틸) */ + if (h_amount > 0) { + fee_amt = acq_fee(h_amount, (h_mdr > 0 ? h_mdr : 25L)); + vat_amt = acq_vat(fee_amt); + } + if (h_bizdate[0] && acq_next_bizday(h_bizdate, nextbiz) == 0) { + userlog("[%s] 정산참고 차기영업일=%s 수수료=%ld 부가세=%ld", + svcname, nextbiz, fee_amt, vat_amt); + } + + /* 7) 예외/보상부: 위 각 구간의 EXEC SQL 실패는 그 지점에서 T_RC/T_MSG 를 + * 세팅한 뒤 FAIL(b) → tpreturn(TPFAIL) 로 되돌린다. 서비스가 호출자와 + * 같은 XA 브랜치에서 돌기 때문에 TPFAIL 은 브랜치 롤백을 유발하고, + * 따라서 원장/한도/집계가 부분만 반영되는 상태가 남지 않는다. */ + if (outmsg[0]) Bchg(b, T_MSG, 0, outmsg, 0L); + if (fee_amt > 0) setl(b, T_FEE, fee_amt); + + /* 8) 응답부: 성공 결과코드 + 계약상 출력 필드 세팅 */ + setl(b, T_RC, rc); + userlog("[%s] 완료: rc=%ld cnt=%ld sum=%ld id=%ld", svcname, rc, h_cnt, h_sum, (long)(h_auth?h_auth:h_id)); OK(b); } diff --git a/app/src/cl/svc/CL_AGGREGATE.pgc b/app/src/cl/svc/CL_AGGREGATE.pgc index f736a2c..02e9b24 100644 --- a/app/src/cl/svc/CL_AGGREGATE.pgc +++ b/app/src/cl/svc/CL_AGGREGATE.pgc @@ -9,20 +9,140 @@ void CL_AGGREGATE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long metric = 0; int i = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid, h_cnt, h_amt; char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_n1, h_n2, h_n3, h_cid; + char h_key[32], h_status[4], h_out[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_AGGREGATE"); + userlog("CL_AGGREGATE 진입: 마감처리 시작 (마감 총계 재집계 후 close_log 반영.)", svc); + + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + gets_(b, T_STR1, h_period, sizeof(h_period)); + gets_(b, T_KEY1, h_key, sizeof(h_key)); h_cid = getl(b, T_ID1); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_amt - FROM purchase WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - if (cldb_update_close_totals(h_cid, h_cnt, h_amt) < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_amt); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0 && h_cid <= 0) { + setl(b, T_RC, 6601); + Bchg(b, T_MSG, 0, "기준(영업일/기간/마감ID) 중 하나는 필수", 0L); FAIL(b); + } + + /* --- 섹션2·섹션4: 기간 마감이력 종합 집계 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_n1, :h_n2, :h_n3 FROM cl_close_log + WHERE (:h_period = '' OR period_key LIKE :h_period || '%') + AND status <> 'X'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6625); FAIL(b); } + metric = h_n1; o_count = h_n2; o_amount = h_n3; + if (h_n1 == 0) { o_rc = 1; strcpy(o_msg, "대상 마감이력 없음"); } + else if (h_n3 == 0) { o_rc = 2; strcpy(o_msg, "집계금액 0"); } + else { o_rc = 0; strcpy(o_msg, "마감이력 집계 완료"); } + + /* --- 섹션3b: 규모 부가판정 --- */ + if (metric < 0) metric = 0; + setl(b, T_ARG5, metric); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_AGGREGATE 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1000000000L) { + z_sev = 3; + strcpy(z_msg, "대규모 마감기간"); + } else if (h_zamt >= 100000000L) { + z_sev = 2; + strcpy(z_msg, "중규모 마감기간"); + } else { + z_sev = 1; + strcpy(z_msg, "소규모 마감기간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_AGGREGATE 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_AGGREGATE 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_AGGREGATE 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_AGGREGATE 마감 총계 재집계 후 close_log 반영. cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_AGGREGATE 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_AGGREGATE 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_ALERT.pgc b/app/src/cl/svc/CL_ALERT.pgc index 7cabbd6..e723096 100644 --- a/app/src/cl/svc/CL_ALERT.pgc +++ b/app/src/cl/svc/CL_ALERT.pgc @@ -9,19 +9,149 @@ void CL_ALERT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long mask = 0; + long blockers = 0; + int over = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt; char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_n1, h_n2, h_n3, h_n4; + long h_lim; + char h_mer[16]; + char h_status[4]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT count(*) INTO :h_cnt FROM purchase - WHERE biz_date = :h_bizdate AND status = 'M'; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); - setl(b, T_RC, (h_cnt > 0) ? 1 : 0); - if (h_cnt > 0) userlog("CL_ALERT %s 미마감 %ld건 감지", bizdate, h_cnt); + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_ALERT"); + userlog("CL_ALERT 진입: 마감처리 시작 (미전기 대사완료(M) 점검)", svc); + + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0) { + setl(b, T_RC, 5306); + Bchg(b, T_MSG, 0, "영업일 미입력 - 미전기 대사완료(M) 점검 불가", 0L); + FAIL(b); + } + if (strlen(h_bizdate) < 8) { + setl(b, T_RC, 5406); + Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); FAIL(b); + } + + /* --- 섹션2: 기준/정책 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_n4 FROM cl_close_log + WHERE close_type = 'DAILY' AND period_key = :h_bizdate AND status <> 'X'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5307); FAIL(b); } + + /* --- 섹션3·섹션4: 단일조건 건수 판정 --- */ + EXEC SQL SELECT count(*) INTO :h_n1 FROM purchase WHERE biz_date = :h_bizdate AND status = 'M'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5308); FAIL(b); } + o_count = h_n1; + if (h_n1 == 0) { + o_rc = 0; + strcpy(o_msg, "미전기 대사완료(M) 없음 - 마감 가능"); + } else if (h_n1 < 10) { + o_rc = 1; + strcpy(o_msg, "미전기 대사완료(M) 소량 존재 - 확인 필요"); + } else { + o_rc = 2; + strcpy(o_msg, "미전기 대사완료(M) 다수 존재 - 마감 불가"); + } + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_ALERT 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1000000000L) { + z_sev = 3; + strcpy(z_msg, "대규모 마감기간"); + } else if (h_zamt >= 100000000L) { + z_sev = 2; + strcpy(z_msg, "중규모 마감기간"); + } else { + z_sev = 1; + strcpy(z_msg, "소규모 마감기간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_ALERT 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_ALERT 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_ALERT 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_ALERT 미전기 대사완료(M)점검 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_ALERT 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_ALERT 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_AMTXCHECK.pgc b/app/src/cl/svc/CL_AMTXCHECK.pgc index 53a9b59..0a24a60 100644 --- a/app/src/cl/svc/CL_AMTXCHECK.pgc +++ b/app/src/cl/svc/CL_AMTXCHECK.pgc @@ -9,30 +9,159 @@ void CL_AMTXCHECK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long d1, d2, worst; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long d1 = 0, d2 = 0, worst = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid, h_log, h_real, h_snap; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_hdr, h_real, h_snap; char h_date[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_AMTXCHECK"); + userlog("CL_AMTXCHECK 진입: 마감처리 시작 (금액 크로스검증)", svc); + h_cid = getl(b, T_ID1); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0) { + setl(b, T_RC, 5501); + Bchg(b, T_MSG, 0, "마감ID(T_ID1) 필수", 0L); FAIL(b); + } + + /* --- 섹션2: 마감이력 헤더 조회부 --- */ EXEC SQL SELECT total_amount, to_char(biz_date,'YYYY-MM-DD') - INTO :h_log, :h_date FROM cl_close_log WHERE close_id = :h_cid AND status <> 'X'; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); + INTO :h_hdr, :h_date FROM cl_close_log + WHERE close_id = :h_cid AND status <> 'X'; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5502); + Bchg(b, T_MSG, 0, "유효 마감이력 없음(취소/미존재)", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5503); FAIL(b); } + + /* --- 섹션4: 실매입/스냅샷 집계부 --- */ EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_real FROM purchase WHERE biz_date = :h_date; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5504); FAIL(b); } EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :h_snap FROM cl_snapshot WHERE close_id = :h_cid; - if (sqlca.sqlcode < 0) FAIL(b); - d1 = h_log - h_real; if (d1 < 0) d1 = -d1; - d2 = (h_snap == 0) ? 0 : (h_log - h_snap); - if (d2 < 0) d2 = -d2; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5505); FAIL(b); } + + /* --- 섹션3: 3원 대사 분기부 --- */ + d1 = h_hdr - h_real; if (d1 < 0) d1 = -d1; + d2 = (h_snap == 0) ? 0 : (h_hdr - h_snap); if (d2 < 0) d2 = -d2; worst = (d1 > d2) ? d1 : d2; - setl(b, T_AMT1, h_log); + if (worst == 0) { + o_rc = 0; strcpy(o_msg, "헤더=매입=스냅샷 3원 일치"); + } else if (d1 != 0 && d2 == 0) { + o_rc = 1; strcpy(o_msg, "헤더-매입 불일치(스냅샷은 일치)"); + } else if (d1 == 0 && d2 != 0) { + o_rc = 2; strcpy(o_msg, "헤더-스냅샷 불일치(매입은 일치)"); + } else { + o_rc = 3; strcpy(o_msg, "3원 전부 불일치 - 재마감 검토"); + } + + /* --- 섹션6: 편차 세팅 --- */ + setl(b, T_AMT1, h_hdr); setl(b, T_AMT2, h_real); setl(b, T_AMT3, h_snap); setl(b, T_DELTA, worst); - setl(b, T_RC, (worst == 0) ? 0 : 1); + o_count = (worst == 0) ? 0 : 1; o_amount = worst; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_AMTXCHECK 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1000000000L) { + z_sev = 3; + strcpy(z_msg, "대규모 마감기간"); + } else if (h_zamt >= 100000000L) { + z_sev = 2; + strcpy(z_msg, "중규모 마감기간"); + } else { + z_sev = 1; + strcpy(z_msg, "소규모 마감기간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_AMTXCHECK 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_AMTXCHECK 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_AMTXCHECK 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_AMTXCHECK 금액크로스 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_AMTXCHECK 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_AMTXCHECK 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_APPRDELEG.pgc b/app/src/cl/svc/CL_APPRDELEG.pgc index e87dc72..640e673 100644 --- a/app/src/cl/svc/CL_APPRDELEG.pgc +++ b/app/src/cl/svc/CL_APPRDELEG.pgc @@ -8,21 +8,168 @@ void CL_APPRDELEG(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char deleg[32]; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + char st = 0; + char newmemo[128]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid; - char h_memo[64]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_cnt; + char h_status[4], h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_APPRDELEG"); + userlog("CL_APPRDELEG 진입: 마감처리 시작 (승인 위임)", svc); + h_cid = getl(b, T_ID1); - gets_(b, T_STR1, deleg, sizeof(deleg)); - if (deleg[0] == 0) strcpy(deleg, "DELEGATE"); - snprintf(h_memo, sizeof(h_memo), "APPR:DELEG:%s", deleg); - EXEC SQL UPDATE cl_close_log SET memo = :h_memo, updated_at = now() - WHERE close_id = :h_cid AND status IN ('O','C'); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_ID1, h_cid); - setl(b, T_RC, (long)sqlca.sqlerrd[2]); - Bchg(b, T_STR1, 0, deleg, 0L); + gets_(b, T_STR1, h_memo, sizeof(h_memo)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0) { + setl(b, T_RC, 5601); + Bchg(b, T_MSG, 0, "마감ID(T_ID1) 필수", 0L); FAIL(b); + } + + /* --- 섹션2: 현재 상태/집계 조회부 --- */ + EXEC SQL SELECT status, txn_count, memo + INTO :h_status, :h_cnt, :h_memo + FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5602); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5603); FAIL(b); } + st = h_status[0]; + + /* --- 섹션3: 상태전이 가능여부 분기부 --- */ + if (!(st == 'C' || st == 'O')) { + setl(b, T_RC, 5604); + snprintf(o_msg, sizeof(o_msg), "현재상태(%c)에서 승인 위임 불가", st); + Bchg(b, T_MSG, 0, o_msg, 0L); FAIL(b); + } + if (h_cnt == 0) { + setl(b, T_RC, 5605); + Bchg(b, T_MSG, 0, "집계 0건 - 승인 대상 아님", 0L); FAIL(b); + } + + /* --- 섹션4: 상태전이 본처리부 --- */ + if (h_memo[0]) snprintf(newmemo, sizeof(newmemo), "APPR:DELEG|%s", h_memo); + else strcpy(newmemo, "APPR:DELEG"); + { char nm[128]; strncpy(nm, newmemo, sizeof(nm)-1); nm[sizeof(nm)-1]=0; + strncpy(h_memo, nm, sizeof(h_memo)-1); h_memo[sizeof(h_memo)-1]=0; } + EXEC SQL UPDATE cl_close_log + SET status = 'C', memo = :h_memo, updated_at = now() + WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5606); + Bchg(b, T_MSG, 0, "상태전이 실패", 0L); FAIL(b); + } + if (sqlca.sqlerrd[2] != 1) { + setl(b, T_RC, 5607); + Bchg(b, T_MSG, 0, "전이 대상행 수 이상", 0L); FAIL(b); + } + + /* --- 섹션6: 전이 후 상태 재확인부 --- */ + EXEC SQL SELECT status INTO :h_status FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5608); FAIL(b); } + Bchg(b, T_STATUS, 0, h_status, 0L); + o_count = h_cnt; o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "승인 위임 처리완료 상태=%c", h_status[0]); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_APPRDELEG 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 2000000000L) { + z_sev = 3; + strcpy(z_msg, "초과대형 마감구간"); + } else if (h_zamt >= 200000000L) { + z_sev = 2; + strcpy(z_msg, "중대형 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "일반 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_APPRDELEG 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_APPRDELEG 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_APPRDELEG 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_APPRDELEG 승인 위임 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_APPRDELEG 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_APPRDELEG 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_APPRFINAL.pgc b/app/src/cl/svc/CL_APPRFINAL.pgc index 3c00bb9..6332022 100644 --- a/app/src/cl/svc/CL_APPRFINAL.pgc +++ b/app/src/cl/svc/CL_APPRFINAL.pgc @@ -8,21 +8,168 @@ void CL_APPRFINAL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + char st = 0; + char newmemo[128]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid, h_ok; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_cnt; + char h_status[4], h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_APPRFINAL"); + userlog("CL_APPRFINAL 진입: 마감처리 시작 (최종 승인(A))", svc); + h_cid = getl(b, T_ID1); - EXEC SQL SELECT count(*) INTO :h_ok FROM cl_close_log - WHERE close_id = :h_cid AND memo = 'APPR:L2'; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_ok == 0) { Bchg(b, T_MSG, 0, "2차 승인 선행 필요", 0L); FAIL(b); } - EXEC SQL UPDATE cl_close_log SET status = 'A', memo = 'APPR:FINAL', updated_at = now() + gets_(b, T_STR1, h_memo, sizeof(h_memo)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0) { + setl(b, T_RC, 5601); + Bchg(b, T_MSG, 0, "마감ID(T_ID1) 필수", 0L); FAIL(b); + } + + /* --- 섹션2: 현재 상태/집계 조회부 --- */ + EXEC SQL SELECT status, txn_count, memo + INTO :h_status, :h_cnt, :h_memo + FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5602); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5603); FAIL(b); } + st = h_status[0]; + + /* --- 섹션3: 상태전이 가능여부 분기부 --- */ + if (!(st == 'C')) { + setl(b, T_RC, 5604); + snprintf(o_msg, sizeof(o_msg), "현재상태(%c)에서 최종 승인(A) 불가", st); + Bchg(b, T_MSG, 0, o_msg, 0L); FAIL(b); + } + if (h_cnt == 0) { + setl(b, T_RC, 5605); + Bchg(b, T_MSG, 0, "집계 0건 - 승인 대상 아님", 0L); FAIL(b); + } + + /* --- 섹션4: 상태전이 본처리부 --- */ + if (h_memo[0]) snprintf(newmemo, sizeof(newmemo), "APPR:FINAL|%s", h_memo); + else strcpy(newmemo, "APPR:FINAL"); + { char nm[128]; strncpy(nm, newmemo, sizeof(nm)-1); nm[sizeof(nm)-1]=0; + strncpy(h_memo, nm, sizeof(h_memo)-1); h_memo[sizeof(h_memo)-1]=0; } + EXEC SQL UPDATE cl_close_log + SET status = 'A', memo = :h_memo, updated_at = now() WHERE close_id = :h_cid; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_ID1, h_cid); - setl(b, T_ARG1, 3); - Bchg(b, T_STATUS, 0, "A", 0L); - Bchg(b, T_MSG, 0, "최종 승인 완료", 0L); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5606); + Bchg(b, T_MSG, 0, "상태전이 실패", 0L); FAIL(b); + } + if (sqlca.sqlerrd[2] != 1) { + setl(b, T_RC, 5607); + Bchg(b, T_MSG, 0, "전이 대상행 수 이상", 0L); FAIL(b); + } + + /* --- 섹션6: 전이 후 상태 재확인부 --- */ + EXEC SQL SELECT status INTO :h_status FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5608); FAIL(b); } + Bchg(b, T_STATUS, 0, h_status, 0L); + o_count = h_cnt; o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "최종 승인(A) 처리완료 상태=%c", h_status[0]); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_APPRFINAL 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1000000000L) { + z_sev = 3; + strcpy(z_msg, "대규모 마감기간"); + } else if (h_zamt >= 100000000L) { + z_sev = 2; + strcpy(z_msg, "중규모 마감기간"); + } else { + z_sev = 1; + strcpy(z_msg, "소규모 마감기간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_APPRFINAL 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_APPRFINAL 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_APPRFINAL 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_APPRFINAL 최종 승인(A) cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_APPRFINAL 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_APPRFINAL 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_APPRHIST.pgc b/app/src/cl/svc/CL_APPRHIST.pgc index a80dc3e..8e6634c 100644 --- a/app/src/cl/svc/CL_APPRHIST.pgc +++ b/app/src/cl/svc/CL_APPRHIST.pgc @@ -9,23 +9,166 @@ void CL_APPRHIST(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long groups = 0; long topamt = 0; char topdim[64]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_appr, h_done, h_cxl; - char h_prefix[16]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_grpcnt, h_grpamt, h_cid; + char h_dim[64], h_key[32]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; - gets_(b, T_STR1, h_prefix, sizeof(h_prefix)); - if (h_prefix[0] == 0) { Bchg(b, T_MSG, 0, "기간 프리픽스 필요", 0L); FAIL(b); } - EXEC SQL SELECT - coalesce(sum(CASE WHEN status IN ('A','L') THEN 1 ELSE 0 END),0), - coalesce(sum(CASE WHEN status = 'C' THEN 1 ELSE 0 END),0), - coalesce(sum(CASE WHEN status = 'X' THEN 1 ELSE 0 END),0) - INTO :h_appr, :h_done, :h_cxl - FROM cl_close_log WHERE period_key LIKE :h_prefix || '%'; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_ARG1, h_appr); - setl(b, T_ARG2, h_done); - setl(b, T_ARG3, h_cxl); - setl(b, T_COUNT, h_appr + h_done + h_cxl); + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_APPRHIST"); + userlog("CL_APPRHIST 진입: 마감처리 시작 (승인이력)", svc); + + gets_(b, T_KEY1, h_key, sizeof(h_key)); + if (h_key[0] == 0) gets_(b, T_STR1, h_key, sizeof(h_key)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_key[0] == 0) { + setl(b, T_RC, 6502); + Bchg(b, T_MSG, 0, "리포트 키(close_type/기간) 필수", 0L); FAIL(b); + } + + /* --- 섹션2: 그룹 수 사전 확인부 --- */ + EXEC SQL SELECT count(DISTINCT status) INTO :h_grpcnt + FROM cl_close_log WHERE period_key LIKE :h_key || '%'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6504); FAIL(b); } + if (h_grpcnt == 0) { + o_rc = 1; o_count = 0; + strcpy(o_msg, "리포트 대상 그룹 없음"); + setl(b, T_COUNT, 0); Bchg(b, T_MSG, 0, o_msg, 0L); OK(b); + } + + /* --- 섹션4: 그룹별 집계 커서(상위추출) --- */ + strcpy(topdim, ""); + EXEC SQL DECLARE crpt CURSOR FOR + SELECT status, count(*), coalesce(sum(total_amount),0) FROM cl_close_log + WHERE period_key LIKE :h_key || '%' GROUP BY status ORDER BY 2 DESC; + EXEC SQL OPEN crpt; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6505); FAIL(b); } + for (;;) { + EXEC SQL FETCH crpt INTO :h_dim, :h_grpcnt, :h_grpamt; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE crpt; setl(b, T_RC, 6506); FAIL(b); } + if (groups < 20) { + Bchg(b, T_STR2, (BFLDOCC)groups, h_dim, 0L); + setl(b, T_ARG3, h_grpamt); + } + if (h_grpamt > topamt) { + topamt = h_grpamt; + strncpy(topdim, h_dim, sizeof(topdim)-1); topdim[sizeof(topdim)-1]=0; + } + groups++; + } + EXEC SQL CLOSE crpt; + + /* --- 섹션3: 상위그룹 판정 분기부 --- */ + if (groups == 0) { + o_rc = 1; strcpy(o_msg, "승인이력 집계 결과 없음"); + } else if (topamt == 0) { + o_rc = 2; strcpy(o_msg, "승인이력 집계 금액 0"); + } else { + o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "승인이력 최다=%s(%ld)", topdim, topamt); + } + Bchg(b, T_STR1, 0, topdim, 0L); + o_count = groups; o_amount = topamt; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_APPRHIST 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 2000000000L) { + z_sev = 3; + strcpy(z_msg, "초과대형 마감구간"); + } else if (h_zamt >= 200000000L) { + z_sev = 2; + strcpy(z_msg, "중대형 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "일반 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_APPRHIST 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_APPRHIST 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_APPRHIST 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_APPRHIST 승인이력 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_APPRHIST 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_APPRHIST 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_APPRL1.pgc b/app/src/cl/svc/CL_APPRL1.pgc index 89f0825..43e99dc 100644 --- a/app/src/cl/svc/CL_APPRL1.pgc +++ b/app/src/cl/svc/CL_APPRL1.pgc @@ -8,19 +8,168 @@ void CL_APPRL1(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char st[2]; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + char st = 0; + char newmemo[128]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_cnt; + char h_status[4], h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_APPRL1"); + userlog("CL_APPRL1 진입: 마감처리 시작 (1차 승인(L1))", svc); + h_cid = getl(b, T_ID1); - if (cldb_get_close_status(h_cid, st) < 0) FAIL(b); - if (st[0] != 'O' && st[0] != 'C') { Bchg(b, T_MSG, 0, "승인 대상 상태 아님", 0L); FAIL(b); } - EXEC SQL UPDATE cl_close_log SET memo = 'APPR:L1', updated_at = now() + gets_(b, T_STR1, h_memo, sizeof(h_memo)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0) { + setl(b, T_RC, 5601); + Bchg(b, T_MSG, 0, "마감ID(T_ID1) 필수", 0L); FAIL(b); + } + + /* --- 섹션2: 현재 상태/집계 조회부 --- */ + EXEC SQL SELECT status, txn_count, memo + INTO :h_status, :h_cnt, :h_memo + FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5602); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5603); FAIL(b); } + st = h_status[0]; + + /* --- 섹션3: 상태전이 가능여부 분기부 --- */ + if (!(st == 'O' || st == 'C')) { + setl(b, T_RC, 5604); + snprintf(o_msg, sizeof(o_msg), "현재상태(%c)에서 1차 승인(L1) 불가", st); + Bchg(b, T_MSG, 0, o_msg, 0L); FAIL(b); + } + if (h_cnt == 0) { + setl(b, T_RC, 5605); + Bchg(b, T_MSG, 0, "집계 0건 - 승인 대상 아님", 0L); FAIL(b); + } + + /* --- 섹션4: 상태전이 본처리부 --- */ + if (h_memo[0]) snprintf(newmemo, sizeof(newmemo), "APPR:L1|%s", h_memo); + else strcpy(newmemo, "APPR:L1"); + { char nm[128]; strncpy(nm, newmemo, sizeof(nm)-1); nm[sizeof(nm)-1]=0; + strncpy(h_memo, nm, sizeof(h_memo)-1); h_memo[sizeof(h_memo)-1]=0; } + EXEC SQL UPDATE cl_close_log + SET status = 'C', memo = :h_memo, updated_at = now() WHERE close_id = :h_cid; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_ID1, h_cid); - setl(b, T_ARG1, 1); - Bchg(b, T_MSG, 0, "1차 승인 완료", 0L); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5606); + Bchg(b, T_MSG, 0, "상태전이 실패", 0L); FAIL(b); + } + if (sqlca.sqlerrd[2] != 1) { + setl(b, T_RC, 5607); + Bchg(b, T_MSG, 0, "전이 대상행 수 이상", 0L); FAIL(b); + } + + /* --- 섹션6: 전이 후 상태 재확인부 --- */ + EXEC SQL SELECT status INTO :h_status FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5608); FAIL(b); } + Bchg(b, T_STATUS, 0, h_status, 0L); + o_count = h_cnt; o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "1차 승인(L1) 처리완료 상태=%c", h_status[0]); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_APPRL1 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 2000000000L) { + z_sev = 3; + strcpy(z_msg, "초과대형 마감구간"); + } else if (h_zamt >= 200000000L) { + z_sev = 2; + strcpy(z_msg, "중대형 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "일반 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_APPRL1 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_APPRL1 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_APPRL1 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_APPRL1 1차 승인(L1) cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_APPRL1 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_APPRL1 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_APPRL2.pgc b/app/src/cl/svc/CL_APPRL2.pgc index 3daf8cc..8696ff2 100644 --- a/app/src/cl/svc/CL_APPRL2.pgc +++ b/app/src/cl/svc/CL_APPRL2.pgc @@ -8,20 +8,168 @@ void CL_APPRL2(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + char st = 0; + char newmemo[128]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid, h_ok; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_cnt; + char h_status[4], h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_APPRL2"); + userlog("CL_APPRL2 진입: 마감처리 시작 (2차 승인(L2))", svc); + h_cid = getl(b, T_ID1); - EXEC SQL SELECT count(*) INTO :h_ok FROM cl_close_log - WHERE close_id = :h_cid AND memo = 'APPR:L1'; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_ok == 0) { Bchg(b, T_MSG, 0, "1차 승인 선행 필요", 0L); FAIL(b); } - EXEC SQL UPDATE cl_close_log SET memo = 'APPR:L2', updated_at = now() + gets_(b, T_STR1, h_memo, sizeof(h_memo)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0) { + setl(b, T_RC, 5601); + Bchg(b, T_MSG, 0, "마감ID(T_ID1) 필수", 0L); FAIL(b); + } + + /* --- 섹션2: 현재 상태/집계 조회부 --- */ + EXEC SQL SELECT status, txn_count, memo + INTO :h_status, :h_cnt, :h_memo + FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5602); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5603); FAIL(b); } + st = h_status[0]; + + /* --- 섹션3: 상태전이 가능여부 분기부 --- */ + if (!(st == 'C')) { + setl(b, T_RC, 5604); + snprintf(o_msg, sizeof(o_msg), "현재상태(%c)에서 2차 승인(L2) 불가", st); + Bchg(b, T_MSG, 0, o_msg, 0L); FAIL(b); + } + if (h_cnt == 0) { + setl(b, T_RC, 5605); + Bchg(b, T_MSG, 0, "집계 0건 - 승인 대상 아님", 0L); FAIL(b); + } + + /* --- 섹션4: 상태전이 본처리부 --- */ + if (h_memo[0]) snprintf(newmemo, sizeof(newmemo), "APPR:L2|%s", h_memo); + else strcpy(newmemo, "APPR:L2"); + { char nm[128]; strncpy(nm, newmemo, sizeof(nm)-1); nm[sizeof(nm)-1]=0; + strncpy(h_memo, nm, sizeof(h_memo)-1); h_memo[sizeof(h_memo)-1]=0; } + EXEC SQL UPDATE cl_close_log + SET status = 'C', memo = :h_memo, updated_at = now() WHERE close_id = :h_cid; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_ID1, h_cid); - setl(b, T_ARG1, 2); - Bchg(b, T_MSG, 0, "2차 승인 완료", 0L); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5606); + Bchg(b, T_MSG, 0, "상태전이 실패", 0L); FAIL(b); + } + if (sqlca.sqlerrd[2] != 1) { + setl(b, T_RC, 5607); + Bchg(b, T_MSG, 0, "전이 대상행 수 이상", 0L); FAIL(b); + } + + /* --- 섹션6: 전이 후 상태 재확인부 --- */ + EXEC SQL SELECT status INTO :h_status FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5608); FAIL(b); } + Bchg(b, T_STATUS, 0, h_status, 0L); + o_count = h_cnt; o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "2차 승인(L2) 처리완료 상태=%c", h_status[0]); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_APPRL2 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1000000000L) { + z_sev = 3; + strcpy(z_msg, "대규모 마감기간"); + } else if (h_zamt >= 100000000L) { + z_sev = 2; + strcpy(z_msg, "중규모 마감기간"); + } else { + z_sev = 1; + strcpy(z_msg, "소규모 마감기간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_APPRL2 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_APPRL2 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_APPRL2 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_APPRL2 2차 승인(L2) cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_APPRL2 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_APPRL2 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_APPROVE.pgc b/app/src/cl/svc/CL_APPROVE.pgc index 9f68bfa..49eaaed 100644 --- a/app/src/cl/svc/CL_APPROVE.pgc +++ b/app/src/cl/svc/CL_APPROVE.pgc @@ -9,9 +9,168 @@ void CL_APPROVE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long cid = getl(b, T_ID1); - if (cldb_update_close_status(cid, "A") < 0) FAIL(b); - Bchg(b, T_STATUS, 0, "A", 0L); + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + char st = 0; + char newmemo[128]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; + EXEC SQL BEGIN DECLARE SECTION; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_cnt; + char h_status[4], h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; + EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_APPROVE"); + userlog("CL_APPROVE 진입: 마감처리 시작 (마감 승인(A))", svc); + + h_cid = getl(b, T_ID1); + gets_(b, T_STR1, h_memo, sizeof(h_memo)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0) { + setl(b, T_RC, 5601); + Bchg(b, T_MSG, 0, "마감ID(T_ID1) 필수", 0L); FAIL(b); + } + + /* --- 섹션2: 현재 상태/집계 조회부 --- */ + EXEC SQL SELECT status, txn_count, memo + INTO :h_status, :h_cnt, :h_memo + FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5602); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5603); FAIL(b); } + st = h_status[0]; + + /* --- 섹션3: 상태전이 가능여부 분기부 --- */ + if (!(st == 'C' || st == 'R')) { + setl(b, T_RC, 5604); + snprintf(o_msg, sizeof(o_msg), "현재상태(%c)에서 마감 승인(A) 불가", st); + Bchg(b, T_MSG, 0, o_msg, 0L); FAIL(b); + } + if (h_cnt == 0) { + setl(b, T_RC, 5605); + Bchg(b, T_MSG, 0, "집계 0건 - 승인 대상 아님", 0L); FAIL(b); + } + + /* --- 섹션4: 상태전이 본처리부 --- */ + if (h_memo[0]) snprintf(newmemo, sizeof(newmemo), "APPR|%s", h_memo); + else strcpy(newmemo, "APPR"); + { char nm[128]; strncpy(nm, newmemo, sizeof(nm)-1); nm[sizeof(nm)-1]=0; + strncpy(h_memo, nm, sizeof(h_memo)-1); h_memo[sizeof(h_memo)-1]=0; } + EXEC SQL UPDATE cl_close_log + SET status = 'A', memo = :h_memo, updated_at = now() + WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5606); + Bchg(b, T_MSG, 0, "상태전이 실패", 0L); FAIL(b); + } + if (sqlca.sqlerrd[2] != 1) { + setl(b, T_RC, 5607); + Bchg(b, T_MSG, 0, "전이 대상행 수 이상", 0L); FAIL(b); + } + + /* --- 섹션6: 전이 후 상태 재확인부 --- */ + EXEC SQL SELECT status INTO :h_status FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5608); FAIL(b); } + Bchg(b, T_STATUS, 0, h_status, 0L); + o_count = h_cnt; o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "마감 승인(A) 처리완료 상태=%c", h_status[0]); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_APPROVE 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 500000000L) { + z_sev = 3; + strcpy(z_msg, "고액 마감구간"); + } else if (h_zamt >= 50000000L) { + z_sev = 2; + strcpy(z_msg, "중액 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "소액 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_APPROVE 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_APPROVE 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_APPROVE 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_APPROVE 마감 승인(A) cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_APPROVE 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_APPROVE 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_APPRREJECT.pgc b/app/src/cl/svc/CL_APPRREJECT.pgc index 7297951..f5540da 100644 --- a/app/src/cl/svc/CL_APPRREJECT.pgc +++ b/app/src/cl/svc/CL_APPRREJECT.pgc @@ -9,29 +9,168 @@ void CL_APPRREJECT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char reason[64]; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + char st = 0; + char newmemo[128]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid; - char h_memo[80]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_cnt; + char h_status[4], h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_APPRREJECT"); + userlog("CL_APPRREJECT 진입: 마감처리 시작 (승인 반려)", svc); + h_cid = getl(b, T_ID1); - gets_(b, T_REASON, reason, sizeof(reason)); - if (reason[0] == 0) strcpy(reason, "사유미기재"); - snprintf(h_memo, sizeof(h_memo), "REJ:%s", reason); - EXEC SQL UPDATE cl_close_log c - SET status = 'C', memo = :h_memo, updated_at = now() - WHERE c.close_id = :h_cid AND c.status = 'A' - AND NOT EXISTS (SELECT 1 FROM cl_period_lock l - WHERE l.period_key = c.period_key AND l.locked = true); - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlerrd[2] == 0) { - setl(b, T_RC, 2); - Bchg(b, T_MSG, 0, "반려 불가(비승인 상태 또는 잠금 기간)", 0L); - FAIL(b); + gets_(b, T_STR1, h_memo, sizeof(h_memo)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0) { + setl(b, T_RC, 5601); + Bchg(b, T_MSG, 0, "마감ID(T_ID1) 필수", 0L); FAIL(b); } - setl(b, T_RC, 0); - Bchg(b, T_STATUS, 0, "C", 0L); - userlog("CL_APPRREJECT cid=%ld 승인 반려: %s", h_cid, reason); + + /* --- 섹션2: 현재 상태/집계 조회부 --- */ + EXEC SQL SELECT status, txn_count, memo + INTO :h_status, :h_cnt, :h_memo + FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5602); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5603); FAIL(b); } + st = h_status[0]; + + /* --- 섹션3: 상태전이 가능여부 분기부 --- */ + if (!(st == 'A')) { + setl(b, T_RC, 5604); + snprintf(o_msg, sizeof(o_msg), "현재상태(%c)에서 승인 반려 불가", st); + Bchg(b, T_MSG, 0, o_msg, 0L); FAIL(b); + } + if (h_cnt == 0) { + setl(b, T_RC, 5605); + Bchg(b, T_MSG, 0, "집계 0건 - 승인 대상 아님", 0L); FAIL(b); + } + + /* --- 섹션4: 상태전이 본처리부 --- */ + if (h_memo[0]) snprintf(newmemo, sizeof(newmemo), "APPR:REJECT|%s", h_memo); + else strcpy(newmemo, "APPR:REJECT"); + { char nm[128]; strncpy(nm, newmemo, sizeof(nm)-1); nm[sizeof(nm)-1]=0; + strncpy(h_memo, nm, sizeof(h_memo)-1); h_memo[sizeof(h_memo)-1]=0; } + EXEC SQL UPDATE cl_close_log + SET status = 'C', memo = :h_memo, updated_at = now() + WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5606); + Bchg(b, T_MSG, 0, "상태전이 실패", 0L); FAIL(b); + } + if (sqlca.sqlerrd[2] != 1) { + setl(b, T_RC, 5607); + Bchg(b, T_MSG, 0, "전이 대상행 수 이상", 0L); FAIL(b); + } + + /* --- 섹션6: 전이 후 상태 재확인부 --- */ + EXEC SQL SELECT status INTO :h_status FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5608); FAIL(b); } + Bchg(b, T_STATUS, 0, h_status, 0L); + o_count = h_cnt; o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "승인 반려 처리완료 상태=%c", h_status[0]); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_APPRREJECT 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1000000000L) { + z_sev = 3; + strcpy(z_msg, "대규모 마감기간"); + } else if (h_zamt >= 100000000L) { + z_sev = 2; + strcpy(z_msg, "중규모 마감기간"); + } else { + z_sev = 1; + strcpy(z_msg, "소규모 마감기간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_APPRREJECT 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_APPRREJECT 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_APPRREJECT 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_APPRREJECT 승인 반려 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_APPRREJECT 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_APPRREJECT 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_APPRREQ.pgc b/app/src/cl/svc/CL_APPRREQ.pgc index a3ad876..1cfc8a8 100644 --- a/app/src/cl/svc/CL_APPRREQ.pgc +++ b/app/src/cl/svc/CL_APPRREQ.pgc @@ -9,26 +9,168 @@ void CL_APPRREQ(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + char st = 0; + char newmemo[128]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid, h_txn, h_ahead; - char h_st[4]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_cnt; + char h_status[4], h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_APPRREQ"); + userlog("CL_APPRREQ 진입: 마감처리 시작 (승인 요청)", svc); + h_cid = getl(b, T_ID1); - EXEC SQL SELECT status, txn_count INTO :h_st, :h_txn + gets_(b, T_STR1, h_memo, sizeof(h_memo)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0) { + setl(b, T_RC, 5601); + Bchg(b, T_MSG, 0, "마감ID(T_ID1) 필수", 0L); FAIL(b); + } + + /* --- 섹션2: 현재 상태/집계 조회부 --- */ + EXEC SQL SELECT status, txn_count, memo + INTO :h_status, :h_cnt, :h_memo FROM cl_close_log WHERE close_id = :h_cid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - if (h_st[0] != 'C') { Bchg(b, T_MSG, 0, "완료(C) 상태만 승인 요청 가능", 0L); FAIL(b); } - if (h_txn <= 0) { Bchg(b, T_MSG, 0, "집계 0건 마감은 승인 불가", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5602); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5603); FAIL(b); } + st = h_status[0]; + + /* --- 섹션3: 상태전이 가능여부 분기부 --- */ + if (!(st == 'C')) { + setl(b, T_RC, 5604); + snprintf(o_msg, sizeof(o_msg), "현재상태(%c)에서 승인 요청 불가", st); + Bchg(b, T_MSG, 0, o_msg, 0L); FAIL(b); + } + if (h_cnt == 0) { + setl(b, T_RC, 5605); + Bchg(b, T_MSG, 0, "집계 0건 - 승인 대상 아님", 0L); FAIL(b); + } + + /* --- 섹션4: 상태전이 본처리부 --- */ + if (h_memo[0]) snprintf(newmemo, sizeof(newmemo), "APPR_REQ|%s", h_memo); + else strcpy(newmemo, "APPR_REQ"); + { char nm[128]; strncpy(nm, newmemo, sizeof(nm)-1); nm[sizeof(nm)-1]=0; + strncpy(h_memo, nm, sizeof(h_memo)-1); h_memo[sizeof(h_memo)-1]=0; } EXEC SQL UPDATE cl_close_log - SET memo = memo || '|APPR_REQ', updated_at = now() - WHERE close_id = :h_cid AND memo NOT LIKE '%APPR_REQ%'; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_ahead FROM cl_close_log - WHERE status = 'C' AND memo LIKE '%APPR_REQ%' AND close_id < :h_cid; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_ARG1, h_ahead + 1); - setl(b, T_RC, 0); - userlog("CL_APPRREQ cid=%ld 승인요청 대기순번=%ld", h_cid, h_ahead + 1); + SET status = 'C', memo = :h_memo, updated_at = now() + WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5606); + Bchg(b, T_MSG, 0, "상태전이 실패", 0L); FAIL(b); + } + if (sqlca.sqlerrd[2] != 1) { + setl(b, T_RC, 5607); + Bchg(b, T_MSG, 0, "전이 대상행 수 이상", 0L); FAIL(b); + } + + /* --- 섹션6: 전이 후 상태 재확인부 --- */ + EXEC SQL SELECT status INTO :h_status FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5608); FAIL(b); } + Bchg(b, T_STATUS, 0, h_status, 0L); + o_count = h_cnt; o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "승인 요청 처리완료 상태=%c", h_status[0]); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_APPRREQ 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 500000000L) { + z_sev = 3; + strcpy(z_msg, "고액 마감구간"); + } else if (h_zamt >= 50000000L) { + z_sev = 2; + strcpy(z_msg, "중액 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "소액 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_APPRREQ 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_APPRREQ 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_APPRREQ 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_APPRREQ 승인 요청 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_APPRREQ 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_APPRREQ 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_APPRSTAT.pgc b/app/src/cl/svc/CL_APPRSTAT.pgc index 7bd08eb..58a9a45 100644 --- a/app/src/cl/svc/CL_APPRSTAT.pgc +++ b/app/src/cl/svc/CL_APPRSTAT.pgc @@ -8,19 +8,161 @@ void CL_APPRSTAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long groups = 0; long topamt = 0; char topdim[64]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_l1, h_l2, h_fin; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_grpcnt, h_grpamt, h_cid; + char h_dim[64], h_key[32]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; - EXEC SQL SELECT - count(*) FILTER (WHERE memo = 'APPR:L1'), - count(*) FILTER (WHERE memo = 'APPR:L2'), - count(*) FILTER (WHERE status = 'A') - INTO :h_l1, :h_l2, :h_fin FROM cl_close_log WHERE status <> 'X'; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_ARG1, h_l1); - setl(b, T_ARG2, h_l2); - setl(b, T_ARG3, h_fin); - setl(b, T_COUNT, h_l1 + h_l2 + h_fin); + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_APPRSTAT"); + userlog("CL_APPRSTAT 진입: 마감처리 시작 (승인현황)", svc); + + + /* --- 섹션1: 입력 검증부 --- */ + /* 전역 리포트 - 별도 필수입력 없음 */ + + /* --- 섹션2: 그룹 수 사전 확인부 --- */ + EXEC SQL SELECT count(DISTINCT status) INTO :h_grpcnt + FROM cl_close_log WHERE 1=1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6504); FAIL(b); } + if (h_grpcnt == 0) { + o_rc = 1; o_count = 0; + strcpy(o_msg, "리포트 대상 그룹 없음"); + setl(b, T_COUNT, 0); Bchg(b, T_MSG, 0, o_msg, 0L); OK(b); + } + + /* --- 섹션4: 그룹별 집계 커서(상위추출) --- */ + strcpy(topdim, ""); + EXEC SQL DECLARE crpt CURSOR FOR + SELECT status, count(*), coalesce(sum(total_amount),0) FROM cl_close_log + WHERE 1=1 GROUP BY status ORDER BY 2 DESC; + EXEC SQL OPEN crpt; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6505); FAIL(b); } + for (;;) { + EXEC SQL FETCH crpt INTO :h_dim, :h_grpcnt, :h_grpamt; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE crpt; setl(b, T_RC, 6506); FAIL(b); } + if (groups < 20) { + Bchg(b, T_STR2, (BFLDOCC)groups, h_dim, 0L); + setl(b, T_ARG3, h_grpamt); + } + if (h_grpamt > topamt) { + topamt = h_grpamt; + strncpy(topdim, h_dim, sizeof(topdim)-1); topdim[sizeof(topdim)-1]=0; + } + groups++; + } + EXEC SQL CLOSE crpt; + + /* --- 섹션3: 상위그룹 판정 분기부 --- */ + if (groups == 0) { + o_rc = 1; strcpy(o_msg, "승인현황 집계 결과 없음"); + } else if (topamt == 0) { + o_rc = 2; strcpy(o_msg, "승인현황 집계 금액 0"); + } else { + o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "승인현황 최다=%s(%ld)", topdim, topamt); + } + Bchg(b, T_STR1, 0, topdim, 0L); + o_count = groups; o_amount = topamt; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_APPRSTAT 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 2000000000L) { + z_sev = 3; + strcpy(z_msg, "초과대형 마감구간"); + } else if (h_zamt >= 200000000L) { + z_sev = 2; + strcpy(z_msg, "중대형 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "일반 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_APPRSTAT 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_APPRSTAT 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_APPRSTAT 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_APPRSTAT 승인현황 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_APPRSTAT 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_APPRSTAT 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_APPRWD.pgc b/app/src/cl/svc/CL_APPRWD.pgc index 0866640..d1900b7 100644 --- a/app/src/cl/svc/CL_APPRWD.pgc +++ b/app/src/cl/svc/CL_APPRWD.pgc @@ -8,19 +8,168 @@ void CL_APPRWD(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char st[2]; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + char st = 0; + char newmemo[128]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_cnt; + char h_status[4], h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_APPRWD"); + userlog("CL_APPRWD 진입: 마감처리 시작 (승인 회수)", svc); + h_cid = getl(b, T_ID1); - if (cldb_get_close_status(h_cid, st) < 0) FAIL(b); - if (st[0] != 'A') { Bchg(b, T_MSG, 0, "승인(A) 상태 아님-회수 불가", 0L); FAIL(b); } - if (cldb_update_close_status(h_cid, "C") < 0) FAIL(b); - EXEC SQL UPDATE cl_close_log SET memo = '', updated_at = now() WHERE close_id = :h_cid; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_ID1, h_cid); - Bchg(b, T_STATUS, 0, "C", 0L); - Bchg(b, T_MSG, 0, "승인 회수 완료", 0L); + gets_(b, T_STR1, h_memo, sizeof(h_memo)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0) { + setl(b, T_RC, 5601); + Bchg(b, T_MSG, 0, "마감ID(T_ID1) 필수", 0L); FAIL(b); + } + + /* --- 섹션2: 현재 상태/집계 조회부 --- */ + EXEC SQL SELECT status, txn_count, memo + INTO :h_status, :h_cnt, :h_memo + FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5602); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5603); FAIL(b); } + st = h_status[0]; + + /* --- 섹션3: 상태전이 가능여부 분기부 --- */ + if (!(st == 'A')) { + setl(b, T_RC, 5604); + snprintf(o_msg, sizeof(o_msg), "현재상태(%c)에서 승인 회수 불가", st); + Bchg(b, T_MSG, 0, o_msg, 0L); FAIL(b); + } + if (h_cnt == 0) { + setl(b, T_RC, 5605); + Bchg(b, T_MSG, 0, "집계 0건 - 승인 대상 아님", 0L); FAIL(b); + } + + /* --- 섹션4: 상태전이 본처리부 --- */ + if (h_memo[0]) snprintf(newmemo, sizeof(newmemo), "APPR:WD|%s", h_memo); + else strcpy(newmemo, "APPR:WD"); + { char nm[128]; strncpy(nm, newmemo, sizeof(nm)-1); nm[sizeof(nm)-1]=0; + strncpy(h_memo, nm, sizeof(h_memo)-1); h_memo[sizeof(h_memo)-1]=0; } + EXEC SQL UPDATE cl_close_log + SET status = 'C', memo = :h_memo, updated_at = now() + WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5606); + Bchg(b, T_MSG, 0, "상태전이 실패", 0L); FAIL(b); + } + if (sqlca.sqlerrd[2] != 1) { + setl(b, T_RC, 5607); + Bchg(b, T_MSG, 0, "전이 대상행 수 이상", 0L); FAIL(b); + } + + /* --- 섹션6: 전이 후 상태 재확인부 --- */ + EXEC SQL SELECT status INTO :h_status FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5608); FAIL(b); } + Bchg(b, T_STATUS, 0, h_status, 0L); + o_count = h_cnt; o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "승인 회수 처리완료 상태=%c", h_status[0]); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_APPRWD 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1000000000L) { + z_sev = 3; + strcpy(z_msg, "대규모 마감기간"); + } else if (h_zamt >= 100000000L) { + z_sev = 2; + strcpy(z_msg, "중규모 마감기간"); + } else { + z_sev = 1; + strcpy(z_msg, "소규모 마감기간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_APPRWD 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_APPRWD 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_APPRWD 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_APPRWD 승인 회수 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_APPRWD 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_APPRWD 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_ARCHIVE.pgc b/app/src/cl/svc/CL_ARCHIVE.pgc index 17cbbef..fa6027c 100644 --- a/app/src/cl/svc/CL_ARCHIVE.pgc +++ b/app/src/cl/svc/CL_ARCHIVE.pgc @@ -9,15 +9,148 @@ void CL_ARCHIVE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long metric = 0; int i = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_n1, h_n2, h_n3, h_cid; + char h_key[32], h_status[4], h_out[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_ARCHIVE"); + userlog("CL_ARCHIVE 진입: 마감처리 시작 (마감 아카이브: 승인/완료 마감을 보관 상태로(memo 표시).)", svc); + + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + gets_(b, T_STR1, h_period, sizeof(h_period)); + gets_(b, T_KEY1, h_key, sizeof(h_key)); h_cid = getl(b, T_ID1); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0 && h_cid <= 0) { + setl(b, T_RC, 6601); + Bchg(b, T_MSG, 0, "기준(영업일/기간/마감ID) 중 하나는 필수", 0L); FAIL(b); + } + + /* --- 섹션2·섹션4: 승인/완료 마감 아카이브 표기 --- */ + if (h_cid <= 0) { + setl(b, T_RC, 6607); + Bchg(b, T_MSG, 0, "아카이브 대상 마감ID 필수", 0L); FAIL(b); + } + EXEC SQL SELECT status INTO :h_status FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 6608); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6609); FAIL(b); } + if (h_status[0] != 'A' && h_status[0] != 'C' && h_status[0] != 'L') { + setl(b, T_RC, 6610); + Bchg(b, T_MSG, 0, "승인/완료/잠금 마감만 아카이브 가능", 0L); FAIL(b); + } EXEC SQL UPDATE cl_close_log - SET memo = 'ARCHIVED', updated_at = now() - WHERE close_id = :h_cid AND status IN ('C', 'A', 'L'); - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - setl(b, T_COUNT, sqlca.sqlerrd[2]); + SET memo = 'ARCHIVED:' || memo, updated_at = now() + WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6611); FAIL(b); } + metric = sqlca.sqlerrd[2]; o_count = metric; o_rc = 0; + strcpy(o_msg, "마감 아카이브 표기 완료"); + + /* --- 섹션3b: 규모 부가판정 --- */ + if (metric < 0) metric = 0; + setl(b, T_ARG5, metric); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_ARCHIVE 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1500000000L) { + z_sev = 3; + strcpy(z_msg, "광역 마감규모"); + } else if (h_zamt >= 150000000L) { + z_sev = 2; + strcpy(z_msg, "표준 마감규모"); + } else { + z_sev = 1; + strcpy(z_msg, "경량 마감규모"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_ARCHIVE 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_ARCHIVE 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_ARCHIVE 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_ARCHIVE 마감 아카이브: 승인/완료 마감을 보관 상태로(memo 표시). cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_ARCHIVE 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_ARCHIVE 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_AUDITTRAIL.pgc b/app/src/cl/svc/CL_AUDITTRAIL.pgc index 7881437..b1b76b3 100644 --- a/app/src/cl/svc/CL_AUDITTRAIL.pgc +++ b/app/src/cl/svc/CL_AUDITTRAIL.pgc @@ -9,28 +9,142 @@ void CL_AUDITTRAIL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long flag = 0; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long metric = 0; int i = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid, h_gap, h_tampered; - char h_st[4]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_n1, h_n2, h_n3, h_cid; + char h_key[32], h_status[4], h_out[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_AUDITTRAIL"); + userlog("CL_AUDITTRAIL 진입: 마감처리 시작 (마감 이력 감사: 마감건의 생성-수정 시각 간격(초)을 산출하고, 잠금(L))", svc); + + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + gets_(b, T_STR1, h_period, sizeof(h_period)); + gets_(b, T_KEY1, h_key, sizeof(h_key)); h_cid = getl(b, T_ID1); - EXEC SQL SELECT status, - cast(extract(epoch from (updated_at - created_at)) as bigint) - INTO :h_st, :h_gap FROM cl_close_log WHERE close_id = :h_cid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - if (h_st[0] == 'L') { - EXEC SQL SELECT count(*) INTO :h_tampered - FROM cl_close_log c, cl_period_lock l - WHERE c.close_id = :h_cid AND l.period_key = c.period_key - AND c.updated_at > l.locked_at; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_tampered > 0) flag = 1; + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0 && h_cid <= 0) { + setl(b, T_RC, 6601); + Bchg(b, T_MSG, 0, "기준(영업일/기간/마감ID) 중 하나는 필수", 0L); FAIL(b); } - Bchg(b, T_STATUS, 0, h_st, 0L); - setl(b, T_ARG1, h_gap); - setl(b, T_RC, flag); - if (flag) userlog("CL_AUDITTRAIL cid=%ld 잠금 후 수정 흔적 감지", h_cid); + + /* --- 섹션2·섹션4: 마감 이력 감사 집계 --- */ + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN status='X' THEN 1 ELSE 0 END),0), + coalesce(sum(CASE WHEN status='L' THEN 1 ELSE 0 END),0) + INTO :h_n1, :h_n2, :h_n3 FROM cl_close_log + WHERE (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6622); FAIL(b); } + metric = h_n1; o_count = h_n1; + setl(b, T_ARG1, h_n2); + setl(b, T_ARG2, h_n3); + if (h_n1 == 0) { o_rc = 1; strcpy(o_msg, "감사 대상 없음"); } + else if (h_n2 > 0) { o_rc = 2; strcpy(o_msg, "취소 이력 포함"); } + else { o_rc = 0; strcpy(o_msg, "감사 정상"); } + + /* --- 섹션3b: 규모 부가판정 --- */ + if (metric < 0) metric = 0; + setl(b, T_ARG5, metric); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_AUDITTRAIL 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1000000000L) { + z_sev = 3; + strcpy(z_msg, "대규모 마감기간"); + } else if (h_zamt >= 100000000L) { + z_sev = 2; + strcpy(z_msg, "중규모 마감기간"); + } else { + z_sev = 1; + strcpy(z_msg, "소규모 마감기간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_AUDITTRAIL 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_AUDITTRAIL 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_AUDITTRAIL 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_AUDITTRAIL 마감 이력 감사: 마감건의 생성-수정 시각 간격(초)을 산출하고, 잠금(L) cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_AUDITTRAIL 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_AUDITTRAIL 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_BALVERIFY.pgc b/app/src/cl/svc/CL_BALVERIFY.pgc index a1f2d83..29d151e 100644 --- a/app/src/cl/svc/CL_BALVERIFY.pgc +++ b/app/src/cl/svc/CL_BALVERIFY.pgc @@ -9,18 +9,168 @@ void CL_BALVERIFY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long dcnt = 0, damt = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid, h_snap_net, h_total; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_hdr_cnt, h_hdr_amt; + long h_real_cnt, h_real_amt; + char h_date[16], h_type[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_BALVERIFY"); + userlog("CL_BALVERIFY 진입: 마감처리 시작 (마감 정합 검증)", svc); + h_cid = getl(b, T_ID1); - EXEC SQL SELECT coalesce(sum(net_amount),0) INTO :h_snap_net - FROM cl_snapshot WHERE close_id = :h_cid; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT total_amount INTO :h_total FROM cl_close_log WHERE close_id = :h_cid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - setl(b, T_AMT2, h_snap_net); - setl(b, T_AMT3, h_total); - setl(b, T_DELTA, h_total - h_snap_net); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0 && h_bizdate[0] == 0) { + setl(b, T_RC, 5401); + Bchg(b, T_MSG, 0, "마감ID/영업일 중 하나는 필수", 0L); FAIL(b); + } + + /* --- 섹션2: 마감이력 헤더 조회부 --- */ + if (h_cid > 0) { + EXEC SQL SELECT close_type, txn_count, total_amount, + to_char(biz_date,'YYYY-MM-DD') + INTO :h_type, :h_hdr_cnt, :h_hdr_amt, :h_date + FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5402); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5403); FAIL(b); } + } else { + strcpy(h_date, h_bizdate); + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(total_amount),0) + INTO :h_hdr_cnt, :h_hdr_amt FROM cl_close_log + WHERE period_key = :h_bizdate AND status <> 'X'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5404); FAIL(b); } + } + + /* --- 섹션4: 실매입 재집계부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_real_cnt, :h_real_amt + FROM purchase WHERE biz_date = :h_date; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5405); FAIL(b); } + + /* --- 섹션3: 대사 분기부 --- */ + dcnt = h_hdr_cnt - h_real_cnt; if (dcnt < 0) dcnt = -dcnt; + damt = h_hdr_amt - h_real_amt; if (damt < 0) damt = -damt; + if (dcnt == 0 && damt == 0) { + o_rc = 0; strcpy(o_msg, "건수·금액 완전 일치"); + } else if (dcnt == 0 && damt != 0) { + o_rc = 1; strcpy(o_msg, "건수 일치·금액 불일치"); + } else if (dcnt != 0 && damt == 0) { + o_rc = 2; strcpy(o_msg, "건수 불일치·금액 일치"); + } else { + o_rc = 3; strcpy(o_msg, "건수·금액 모두 불일치"); + } + + /* --- 섹션6: 결과 세팅 --- */ + setl(b, T_ARG1, h_hdr_cnt); + setl(b, T_ARG2, h_real_cnt); + setl(b, T_AMT1, h_hdr_amt); + setl(b, T_AMT2, h_real_amt); + setl(b, T_DELTA, damt); + o_count = dcnt; o_amount = damt; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_BALVERIFY 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 500000000L) { + z_sev = 3; + strcpy(z_msg, "고액 마감구간"); + } else if (h_zamt >= 50000000L) { + z_sev = 2; + strcpy(z_msg, "중액 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "소액 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_BALVERIFY 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_BALVERIFY 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_BALVERIFY 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_BALVERIFY 정합검증 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_BALVERIFY 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_BALVERIFY 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_CALENDAR.pgc b/app/src/cl/svc/CL_CALENDAR.pgc index 2bfcc54..8f7dfc1 100644 --- a/app/src/cl/svc/CL_CALENDAR.pgc +++ b/app/src/cl/svc/CL_CALENDAR.pgc @@ -9,36 +9,140 @@ void CL_CALENDAR(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char map[33]; - int i; - long done = 0; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long metric = 0; int i = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_day; - char h_month[16], h_st[4]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_n1, h_n2, h_n3, h_cid; + char h_key[32], h_status[4], h_out[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; - gets_(b, T_STR1, h_month, sizeof(h_month)); - if (h_month[0] == 0) { Bchg(b, T_MSG, 0, "월(YYYY-MM) 입력 필요", 0L); FAIL(b); } - for (i = 0; i < 31; i++) map[i] = '.'; - map[31] = 0; - EXEC SQL DECLARE ccal CURSOR FOR - SELECT cast(substr(period_key, 9, 2) as int), max(status) - FROM cl_close_log - WHERE close_type = 'DAILY' AND period_key LIKE :h_month || '-%' - GROUP BY 1 ORDER BY 1; - EXEC SQL OPEN ccal; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH ccal INTO :h_day, :h_st; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE ccal; FAIL(b); } - if (h_day >= 1 && h_day <= 31) { - map[h_day - 1] = h_st[0]; - if (h_st[0] != 'O' && h_st[0] != 'X') done++; - } + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_CALENDAR"); + userlog("CL_CALENDAR 진입: 마감처리 시작 (마감 캘린더: 월(YYYY-MM)의 1~31일 각 일자 마감 상태를 한 글자씩 표현한)", svc); + + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + gets_(b, T_STR1, h_period, sizeof(h_period)); + gets_(b, T_KEY1, h_key, sizeof(h_key)); + h_cid = getl(b, T_ID1); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0 && h_cid <= 0) { + setl(b, T_RC, 6601); + Bchg(b, T_MSG, 0, "기준(영업일/기간/마감ID) 중 하나는 필수", 0L); FAIL(b); } - EXEC SQL CLOSE ccal; - Bchg(b, T_STR1, 0, map, 0L); - setl(b, T_COUNT, done); + + /* --- 섹션2·섹션4: 기간 마감이력 종합 집계 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_n1, :h_n2, :h_n3 FROM cl_close_log + WHERE (:h_period = '' OR period_key LIKE :h_period || '%') + AND status <> 'X'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6625); FAIL(b); } + metric = h_n1; o_count = h_n2; o_amount = h_n3; + if (h_n1 == 0) { o_rc = 1; strcpy(o_msg, "대상 마감이력 없음"); } + else if (h_n3 == 0) { o_rc = 2; strcpy(o_msg, "집계금액 0"); } + else { o_rc = 0; strcpy(o_msg, "마감이력 집계 완료"); } + + /* --- 섹션3b: 규모 부가판정 --- */ + if (metric < 0) metric = 0; + setl(b, T_ARG5, metric); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_CALENDAR 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 500000000L) { + z_sev = 3; + strcpy(z_msg, "고액 마감구간"); + } else if (h_zamt >= 50000000L) { + z_sev = 2; + strcpy(z_msg, "중액 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "소액 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_CALENDAR 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_CALENDAR 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CALENDAR 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_CALENDAR 마감 캘린더: 월(YYYY-MM)의 1~31일 각 일자 마감 상태를 한 글자씩 표현한 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CALENDAR 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_CALENDAR 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_CANCEL.pgc b/app/src/cl/svc/CL_CANCEL.pgc index 6f44644..e8c895e 100644 --- a/app/src/cl/svc/CL_CANCEL.pgc +++ b/app/src/cl/svc/CL_CANCEL.pgc @@ -9,9 +9,162 @@ void CL_CANCEL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long cid = getl(b, T_ID1); - if (cldb_update_close_status(cid, "X") < 0) FAIL(b); - Bchg(b, T_STATUS, 0, "X", 0L); + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + char st = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; + EXEC SQL BEGIN DECLARE SECTION; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_cnt, h_n; + char h_status[4], h_reason[64], h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; + EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_CANCEL"); + userlog("CL_CANCEL 진입: 마감처리 시작 (마감 취소)", svc); + + h_cid = getl(b, T_ID1); + gets_(b, T_REASON, h_reason, sizeof(h_reason)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0) { + setl(b, T_RC, 5711); + Bchg(b, T_MSG, 0, "마감ID(T_ID1) 필수", 0L); FAIL(b); + } + + /* --- 섹션2: 현재 상태/스냅샷 조회부 --- */ + EXEC SQL SELECT status, txn_count INTO :h_status, :h_cnt + FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5712); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5713); FAIL(b); } + st = h_status[0]; + + /* --- 섹션3: 취소/전이 가능 분기부 --- */ + if (st == 'L') { + setl(b, T_RC, 5714); + Bchg(b, T_MSG, 0, "잠금상태 - 취소 불가", 0L); FAIL(b); + } + if (st == 'X') { + o_rc = 0; o_count = 0; + strcpy(o_msg, "이미 취소된 마감"); + setl(b, T_COUNT, 0); Bchg(b, T_STATUS, 0, "X", 0L); + Bchg(b, T_MSG, 0, o_msg, 0L); OK(b); + } + + /* --- 섹션4: 본처리부 --- */ + EXEC SQL UPDATE cl_close_log + SET status = 'X', updated_at = now() + WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5718); + Bchg(b, T_MSG, 0, "취소 처리 실패", 0L); FAIL(b); + } + if (sqlca.sqlerrd[2] != 1) { setl(b, T_RC, 5719); + Bchg(b, T_MSG, 0, "대상행 수 이상", 0L); FAIL(b); } + + /* --- 섹션6: 처리 후 상태 재확인 --- */ + EXEC SQL SELECT status INTO :h_status FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5720); FAIL(b); } + Bchg(b, T_STATUS, 0, h_status, 0L); + o_count = h_cnt; o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "취소 완료 상태=%c", h_status[0]); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_CANCEL 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1500000000L) { + z_sev = 3; + strcpy(z_msg, "광역 마감규모"); + } else if (h_zamt >= 150000000L) { + z_sev = 2; + strcpy(z_msg, "표준 마감규모"); + } else { + z_sev = 1; + strcpy(z_msg, "경량 마감규모"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_CANCEL 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_CANCEL 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CANCEL 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_CANCEL 취소 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CANCEL 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_CANCEL 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_CHKALL.pgc b/app/src/cl/svc/CL_CHKALL.pgc index d412b37..83ef0c1 100644 --- a/app/src/cl/svc/CL_CHKALL.pgc +++ b/app/src/cl/svc/CL_CHKALL.pgc @@ -8,33 +8,160 @@ void CL_CHKALL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long score; - char msg[128]; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long mask = 0; + long blockers = 0; + int over = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; - long h_m, h_u, h_h, h_v; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_n1, h_n2, h_n3, h_n4; + long h_lim; + char h_mer[16]; + char h_status[4]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_CHKALL"); + userlog("CL_CHKALL 진입: 마감처리 시작 (마감 블로커 점검)", svc); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT count(*) INTO :h_m FROM purchase + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0) { + setl(b, T_RC, 5305); + Bchg(b, T_MSG, 0, "영업일 미입력 - 마감 블로커 점검 불가", 0L); + FAIL(b); + } + if (strlen(h_bizdate) < 8) { + setl(b, T_RC, 5405); + Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); FAIL(b); + } + + /* --- 섹션2: 기준/정책 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_n4 FROM cl_close_log + WHERE close_type = 'DAILY' AND period_key = :h_bizdate AND status <> 'X'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5306); FAIL(b); } + + /* --- 섹션3·섹션4: 통합 블로커 스코어 산출 --- */ + EXEC SQL SELECT count(*) INTO :h_n1 FROM purchase WHERE biz_date = :h_bizdate AND status = 'M'; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_u FROM ac_unmatch WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_h FROM purchase + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5307); FAIL(b); } + if (h_n1 > 0) { mask |= 1; blockers += h_n1; } + EXEC SQL SELECT count(*) INTO :h_n2 FROM purchase WHERE biz_date = :h_bizdate AND status = 'H'; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_v FROM purchase + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5308); FAIL(b); } + if (h_n2 > 0) { mask |= 2; blockers += h_n2; } + EXEC SQL SELECT count(*) INTO :h_n3 FROM purchase WHERE biz_date = :h_bizdate AND status = 'V'; - if (sqlca.sqlcode < 0) FAIL(b); - score = h_m + h_u + h_h + h_v; - snprintf(msg, sizeof(msg), "미마감%ld/미매칭%ld/보류%ld/void%ld", h_m, h_u, h_h, h_v); - setl(b, T_ARG1, h_m); - setl(b, T_ARG2, h_u); - setl(b, T_ARG3, h_h); - setl(b, T_ARG4, h_v); - setl(b, T_COUNT, score); - setl(b, T_RC, (score > 0) ? 1 : 0); - Bchg(b, T_MSG, 0, msg, 0L); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5309); FAIL(b); } + if (h_n3 > 0) { mask |= 4; blockers += h_n3; } + EXEC SQL SELECT count(*) INTO :h_lim FROM ac_unmatch WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5310); FAIL(b); } + if (h_lim > 0) { mask |= 8; blockers += h_lim; } + + /* --- 분기: 마스크별 판정 --- */ + o_msg[0] = 0; + if (mask & 1) strcat(o_msg, "[미마감M]"); + if (mask & 2) strcat(o_msg, "[보류H]"); + if (mask & 4) strcat(o_msg, "[void]"); + if (mask & 8) strcat(o_msg, "[미매칭]"); + if (mask == 0) strcpy(o_msg, "전항목 통과 - 마감 가능"); + o_rc = mask; o_count = blockers; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_CHKALL 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 2000000000L) { + z_sev = 3; + strcpy(z_msg, "초과대형 마감구간"); + } else if (h_zamt >= 200000000L) { + z_sev = 2; + strcpy(z_msg, "중대형 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "일반 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_CHKALL 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_CHKALL 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CHKALL 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_CHKALL 마감 블로커점검 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CHKALL 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_CHKALL 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_CHKLIMIT.pgc b/app/src/cl/svc/CL_CHKLIMIT.pgc index ff233fb..f56d7d2 100644 --- a/app/src/cl/svc/CL_CHKLIMIT.pgc +++ b/app/src/cl/svc/CL_CHKLIMIT.pgc @@ -8,21 +8,156 @@ void CL_CHKLIMIT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long mask = 0; + long blockers = 0; + int over = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; - long h_over; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_n1, h_n2, h_n3, h_n4; + long h_lim; + char h_mer[16]; + char h_status[4]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_CHKLIMIT"); + userlog("CL_CHKLIMIT 진입: 마감처리 시작 (일한도 초과 가맹점 점검)", svc); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT count(*) INTO :h_over FROM ( - SELECT p.merchant_id + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0) { + setl(b, T_RC, 5304); + Bchg(b, T_MSG, 0, "영업일 미입력 - 일한도 초과 가맹점 점검 불가", 0L); + FAIL(b); + } + if (strlen(h_bizdate) < 8) { + setl(b, T_RC, 5404); + Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); FAIL(b); + } + + /* --- 섹션2: 기준/정책 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_n4 FROM cl_close_log + WHERE close_type = 'DAILY' AND period_key = :h_bizdate AND status <> 'X'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5305); FAIL(b); } + + /* --- 섹션3·섹션4: 가맹점 일한도 초과 탐지 (커서) --- */ + EXEC SQL DECLARE cchk CURSOR FOR + SELECT p.merchant_id, coalesce(sum(p.amount),0), m.daily_limit FROM purchase p JOIN merchant m ON m.merchant_id = p.merchant_id WHERE p.biz_date = :h_bizdate - GROUP BY p.merchant_id, m.daily_limit - HAVING coalesce(sum(p.amount),0) > m.daily_limit) x; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_over); - setl(b, T_RC, (h_over > 0) ? 1 : 0); - Bchg(b, T_MSG, 0, (h_over > 0) ? "일한도 초과 가맹점 존재" : "한도 내 정상", 0L); + GROUP BY p.merchant_id, m.daily_limit; + EXEC SQL OPEN cchk; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5306); FAIL(b); } + for (;;) { + EXEC SQL FETCH cchk INTO :h_mer, :h_n1, :h_lim; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cchk; setl(b, T_RC, 5307); FAIL(b); } + if (h_n1 > h_lim) { + over++; + if (over <= 20) Bchg(b, T_KEY1, (BFLDOCC)(over-1), h_mer, 0L); + } + } + EXEC SQL CLOSE cchk; + blockers = over; + o_count = over; o_rc = (over > 0) ? 1 : 0; + strcpy(o_msg, over > 0 ? "한도초과 가맹점 존재-마감 보류" : "한도 이내-마감 가능"); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_CHKLIMIT 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1500000000L) { + z_sev = 3; + strcpy(z_msg, "광역 마감규모"); + } else if (h_zamt >= 150000000L) { + z_sev = 2; + strcpy(z_msg, "표준 마감규모"); + } else { + z_sev = 1; + strcpy(z_msg, "경량 마감규모"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_CHKLIMIT 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_CHKLIMIT 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CHKLIMIT 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_CHKLIMIT 일한도 초과 가맹점점검 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CHKLIMIT 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_CHKLIMIT 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_CHKPENDING.pgc b/app/src/cl/svc/CL_CHKPENDING.pgc index 09595dd..8b3adb7 100644 --- a/app/src/cl/svc/CL_CHKPENDING.pgc +++ b/app/src/cl/svc/CL_CHKPENDING.pgc @@ -8,18 +8,149 @@ void CL_CHKPENDING(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long mask = 0; + long blockers = 0; + int over = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; - long h_n, h_amt; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_n1, h_n2, h_n3, h_n4; + long h_lim; + char h_mer[16]; + char h_status[4]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_CHKPENDING"); + userlog("CL_CHKPENDING 진입: 마감처리 시작 (보류(H) 매입 점검)", svc); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_n, :h_amt - FROM purchase WHERE biz_date = :h_bizdate AND status = 'H'; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_n); - setl(b, T_GROSS, h_amt); - setl(b, T_RC, (h_n > 0) ? 1 : 0); - Bchg(b, T_MSG, 0, (h_n > 0) ? "보류(H) 매입 잔존" : "보류 없음", 0L); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0) { + setl(b, T_RC, 5301); + Bchg(b, T_MSG, 0, "영업일 미입력 - 보류(H) 매입 점검 불가", 0L); + FAIL(b); + } + if (strlen(h_bizdate) < 8) { + setl(b, T_RC, 5401); + Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); FAIL(b); + } + + /* --- 섹션2: 기준/정책 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_n4 FROM cl_close_log + WHERE close_type = 'DAILY' AND period_key = :h_bizdate AND status <> 'X'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5302); FAIL(b); } + + /* --- 섹션3·섹션4: 단일조건 건수 판정 --- */ + EXEC SQL SELECT count(*) INTO :h_n1 FROM purchase WHERE biz_date = :h_bizdate AND status = 'H'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5303); FAIL(b); } + o_count = h_n1; + if (h_n1 == 0) { + o_rc = 0; + strcpy(o_msg, "보류(H) 매입 없음 - 마감 가능"); + } else if (h_n1 < 10) { + o_rc = 1; + strcpy(o_msg, "보류(H) 매입 소량 존재 - 확인 필요"); + } else { + o_rc = 2; + strcpy(o_msg, "보류(H) 매입 다수 존재 - 마감 불가"); + } + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_CHKPENDING 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1500000000L) { + z_sev = 3; + strcpy(z_msg, "광역 마감규모"); + } else if (h_zamt >= 150000000L) { + z_sev = 2; + strcpy(z_msg, "표준 마감규모"); + } else { + z_sev = 1; + strcpy(z_msg, "경량 마감규모"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_CHKPENDING 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_CHKPENDING 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CHKPENDING 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_CHKPENDING 보류(H) 매입점검 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CHKPENDING 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_CHKPENDING 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_CHKUNMATCH.pgc b/app/src/cl/svc/CL_CHKUNMATCH.pgc index ce8f331..c8d590a 100644 --- a/app/src/cl/svc/CL_CHKUNMATCH.pgc +++ b/app/src/cl/svc/CL_CHKUNMATCH.pgc @@ -8,16 +8,149 @@ void CL_CHKUNMATCH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long mask = 0; + long blockers = 0; + int over = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; - long h_n; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_n1, h_n2, h_n3, h_n4; + long h_lim; + char h_mer[16]; + char h_status[4]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_CHKUNMATCH"); + userlog("CL_CHKUNMATCH 진입: 마감처리 시작 (미매칭 건 점검)", svc); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT count(*) INTO :h_n FROM ac_unmatch WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_n); - setl(b, T_RC, (h_n > 0) ? 1 : 0); - Bchg(b, T_MSG, 0, (h_n > 0) ? "미매칭 존재-마감 불가" : "미매칭 없음", 0L); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0) { + setl(b, T_RC, 5303); + Bchg(b, T_MSG, 0, "영업일 미입력 - 미매칭 건 점검 불가", 0L); + FAIL(b); + } + if (strlen(h_bizdate) < 8) { + setl(b, T_RC, 5403); + Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); FAIL(b); + } + + /* --- 섹션2: 기준/정책 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_n4 FROM cl_close_log + WHERE close_type = 'DAILY' AND period_key = :h_bizdate AND status <> 'X'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5304); FAIL(b); } + + /* --- 섹션3·섹션4: 단일조건 건수 판정 --- */ + EXEC SQL SELECT count(*) INTO :h_n1 FROM ac_unmatch WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5305); FAIL(b); } + o_count = h_n1; + if (h_n1 == 0) { + o_rc = 0; + strcpy(o_msg, "미매칭 건 없음 - 마감 가능"); + } else if (h_n1 < 10) { + o_rc = 1; + strcpy(o_msg, "미매칭 건 소량 존재 - 확인 필요"); + } else { + o_rc = 2; + strcpy(o_msg, "미매칭 건 다수 존재 - 마감 불가"); + } + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_CHKUNMATCH 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 500000000L) { + z_sev = 3; + strcpy(z_msg, "고액 마감구간"); + } else if (h_zamt >= 50000000L) { + z_sev = 2; + strcpy(z_msg, "중액 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "소액 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_CHKUNMATCH 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_CHKUNMATCH 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CHKUNMATCH 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_CHKUNMATCH 미매칭 건점검 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CHKUNMATCH 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_CHKUNMATCH 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_CHKVOID.pgc b/app/src/cl/svc/CL_CHKVOID.pgc index bbdac93..4b3744d 100644 --- a/app/src/cl/svc/CL_CHKVOID.pgc +++ b/app/src/cl/svc/CL_CHKVOID.pgc @@ -8,17 +8,149 @@ void CL_CHKVOID(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long mask = 0; + long blockers = 0; + int over = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; - long h_n; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_n1, h_n2, h_n3, h_n4; + long h_lim; + char h_mer[16]; + char h_status[4]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_CHKVOID"); + userlog("CL_CHKVOID 진입: 마감처리 시작 (void(V) 매입 점검)", svc); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT count(*) INTO :h_n - FROM purchase WHERE biz_date = :h_bizdate AND status = 'V'; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_n); - setl(b, T_RC, (h_n > 0) ? 1 : 0); - Bchg(b, T_MSG, 0, (h_n > 0) ? "void(V) 매입 잔존" : "void 없음", 0L); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0) { + setl(b, T_RC, 5302); + Bchg(b, T_MSG, 0, "영업일 미입력 - void(V) 매입 점검 불가", 0L); + FAIL(b); + } + if (strlen(h_bizdate) < 8) { + setl(b, T_RC, 5402); + Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); FAIL(b); + } + + /* --- 섹션2: 기준/정책 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_n4 FROM cl_close_log + WHERE close_type = 'DAILY' AND period_key = :h_bizdate AND status <> 'X'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5303); FAIL(b); } + + /* --- 섹션3·섹션4: 단일조건 건수 판정 --- */ + EXEC SQL SELECT count(*) INTO :h_n1 FROM purchase WHERE biz_date = :h_bizdate AND status = 'V'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5304); FAIL(b); } + o_count = h_n1; + if (h_n1 == 0) { + o_rc = 0; + strcpy(o_msg, "void(V) 매입 없음 - 마감 가능"); + } else if (h_n1 < 10) { + o_rc = 1; + strcpy(o_msg, "void(V) 매입 소량 존재 - 확인 필요"); + } else { + o_rc = 2; + strcpy(o_msg, "void(V) 매입 다수 존재 - 마감 불가"); + } + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_CHKVOID 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1000000000L) { + z_sev = 3; + strcpy(z_msg, "대규모 마감기간"); + } else if (h_zamt >= 100000000L) { + z_sev = 2; + strcpy(z_msg, "중규모 마감기간"); + } else { + z_sev = 1; + strcpy(z_msg, "소규모 마감기간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_CHKVOID 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_CHKVOID 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CHKVOID 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_CHKVOID void(V) 매입점검 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CHKVOID 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_CHKVOID 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_CHNCLOSE.pgc b/app/src/cl/svc/CL_CHNCLOSE.pgc index c2024a1..0273ff0 100644 --- a/app/src/cl/svc/CL_CHNCLOSE.pgc +++ b/app/src/cl/svc/CL_CHNCLOSE.pgc @@ -9,26 +9,212 @@ void CL_CHNCLOSE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char memo[48]; - long cid; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long cid = 0; + long fee_calc = 0; + int grade = 0; + char memo[128]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_amt; - char h_bizdate[16], h_chn[16]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cnt, h_amt, h_fee, h_net; + long h_prev, h_bps; + char h_key[32]; + char h_status[4]; + char h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_CHNCLOSE"); + userlog("CL_CHNCLOSE 진입: 마감처리 시작 (채널 마감)", svc); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - gets_(b, T_CHANNEL, h_chn, sizeof(h_chn)); - if (h_chn[0] == 0) strcpy(h_chn, "POS"); - EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_amt - FROM purchase WHERE biz_date = :h_bizdate AND channel = :h_chn; - if (sqlca.sqlcode < 0) FAIL(b); + gets_(b, T_STR1, h_period, sizeof(h_period)); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + gets_(b, T_CHANNEL, h_key, sizeof(h_key)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0) { + setl(b, T_RC, 5001); + Bchg(b, T_MSG, 0, "영업일/기간 미입력 - 채널 마감 불가", 0L); + FAIL(b); + } + if (strlen(h_bizdate) > 0 && strlen(h_bizdate) < 8) { + setl(b, T_RC, 5002); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + FAIL(b); + } + if (h_key[0] == 0) { + setl(b, T_RC, 5003); + Bchg(b, T_MSG, 0, "채널 구분값 미입력", 0L); + FAIL(b); + } + + /* --- 섹션2: 정책/기존마감 상태 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_prev FROM cl_close_log + WHERE close_type = 'CHN' AND period_key = :h_period AND status <> 'X'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5010); + Bchg(b, T_MSG, 0, "기존 마감 조회 실패", 0L); FAIL(b); + } + if (h_prev > 0) { + userlog("CL_CHNCLOSE %s 이미 마감 %ld건 존재 - 재집계 진행", h_period, h_prev); + } + EXEC SQL SELECT count(*) INTO :h_bps FROM cl_period_lock + WHERE period_key = :h_period AND locked = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5011); FAIL(b); } + if (h_bps > 0) { + setl(b, T_RC, 5012); + Bchg(b, T_MSG, 0, "기간이 잠금상태 - 마감 불가", 0L); FAIL(b); + } + + /* --- 섹션4: 본처리부(집계) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) + INTO :h_cnt, :h_amt, :h_fee, :h_net + FROM purchase WHERE biz_date = :h_bizdate AND channel = :h_key; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5020); + Bchg(b, T_MSG, 0, "매입 집계 실패", 0L); FAIL(b); + } + + /* --- 섹션3: 업무규칙 분기부(규모/등급) --- */ + if (h_cnt == 0) { + grade = 0; + strcpy(memo, "CHN:무거래"); + } else if (h_amt >= 1000000000) { + grade = 3; + strcpy(memo, "CHN:대규모"); + } else if (h_amt >= 100000000) { + grade = 2; + strcpy(memo, "CHN:중규모"); + } else { + grade = 1; + strcpy(memo, "CHN:소규모"); + } + fee_calc = acq_fee(h_amt, 25); + if (h_fee == 0 && h_amt > 0) h_fee = fee_calc; + + /* --- 섹션4b: 마감이력 생성 --- */ cid = cldb_next_close_id(); - if (cid < 0) FAIL(b); - snprintf(memo, sizeof(memo), "채널마감:%s", h_chn); - if (cldb_insert_close(cid, "CHN", h_bizdate, h_bizdate, "C", h_cnt, h_amt, memo) < 0) FAIL(b); + if (cid < 0) { setl(b, T_RC, 5030); FAIL(b); } + { char m2[128]; snprintf(m2, sizeof(m2), "%s|%s", memo, h_key); + strncpy(memo, m2, sizeof(memo)-1); memo[sizeof(memo)-1]=0; } + if (cldb_insert_close(cid, "CHN", h_period, + (h_bizdate[0] ? h_bizdate : h_period), + "C", h_cnt, h_amt, memo) < 0) { + setl(b, T_RC, 5031); + Bchg(b, T_MSG, 0, "마감이력 생성 실패", 0L); FAIL(b); + } + + /* --- 섹션6: 집계 확인/요약 재조회부 --- */ + h_kcid = cid; + EXEC SQL SELECT txn_count, total_amount INTO :h_prev, :h_amt + FROM cl_close_log WHERE close_id = :h_kcid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5040); FAIL(b); } + o_count = h_cnt; o_amount = h_amt; + + /* --- 섹션8prep: 상태/결과 세팅 --- */ setl(b, T_ID1, cid); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_amt); - userlog("CL_CHNCLOSE cid=%ld chn=%s cnt=%ld 채널마감", cid, h_chn, h_cnt); + setl(b, T_FEE, h_fee); + setl(b, T_NET, h_net); + Bchg(b, T_STATUS, 0, "C", 0L); + snprintf(o_msg, sizeof(o_msg), "채널 마감완료 등급%d cid=%ld", grade, cid); + o_rc = 0; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_CHNCLOSE 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 500000000L) { + z_sev = 3; + strcpy(z_msg, "고액 마감구간"); + } else if (h_zamt >= 50000000L) { + z_sev = 2; + strcpy(z_msg, "중액 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "소액 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_CHNCLOSE 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_CHNCLOSE 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CHNCLOSE 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_CHNCLOSE 채널마감 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CHNCLOSE 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_CHNCLOSE 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_CHNVERIFY.pgc b/app/src/cl/svc/CL_CHNVERIFY.pgc index 12bcbb2..a791be5 100644 --- a/app/src/cl/svc/CL_CHNVERIFY.pgc +++ b/app/src/cl/svc/CL_CHNVERIFY.pgc @@ -9,25 +9,168 @@ void CL_CHNVERIFY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long dcnt = 0, damt = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_pur, h_led; - char h_bizdate[16], h_chn[16]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_hdr_cnt, h_hdr_amt; + long h_real_cnt, h_real_amt; + char h_date[16], h_type[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_CHNVERIFY"); + userlog("CL_CHNVERIFY 진입: 마감처리 시작 (마감 정합 검증)", svc); + + h_cid = getl(b, T_ID1); gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - gets_(b, T_CHANNEL, h_chn, sizeof(h_chn)); - if (h_chn[0] == 0) strcpy(h_chn, "POS"); - EXEC SQL SELECT count(*) INTO :h_pur FROM purchase - WHERE biz_date = :h_bizdate AND channel = :h_chn AND status <> 'C'; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(DISTINCT l.purchase_id) INTO :h_led - FROM ledger l JOIN purchase pu ON pu.purchase_id = l.purchase_id - WHERE l.entry_type = 'RECON' AND pu.biz_date = :h_bizdate - AND pu.channel = :h_chn; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_ARG1, h_pur); - setl(b, T_ARG2, h_led); - setl(b, T_DELTA, h_pur - h_led); - setl(b, T_RC, (h_pur == h_led) ? 0 : 1); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0 && h_bizdate[0] == 0) { + setl(b, T_RC, 5401); + Bchg(b, T_MSG, 0, "마감ID/영업일 중 하나는 필수", 0L); FAIL(b); + } + + /* --- 섹션2: 마감이력 헤더 조회부 --- */ + if (h_cid > 0) { + EXEC SQL SELECT close_type, txn_count, total_amount, + to_char(biz_date,'YYYY-MM-DD') + INTO :h_type, :h_hdr_cnt, :h_hdr_amt, :h_date + FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5402); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5403); FAIL(b); } + } else { + strcpy(h_date, h_bizdate); + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(total_amount),0) + INTO :h_hdr_cnt, :h_hdr_amt FROM cl_close_log + WHERE period_key = :h_bizdate AND status <> 'X'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5404); FAIL(b); } + } + + /* --- 섹션4: 실매입 재집계부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_real_cnt, :h_real_amt + FROM purchase WHERE biz_date = :h_date; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5405); FAIL(b); } + + /* --- 섹션3: 대사 분기부 --- */ + dcnt = h_hdr_cnt - h_real_cnt; if (dcnt < 0) dcnt = -dcnt; + damt = h_hdr_amt - h_real_amt; if (damt < 0) damt = -damt; + if (dcnt == 0 && damt == 0) { + o_rc = 0; strcpy(o_msg, "건수·금액 완전 일치"); + } else if (dcnt == 0 && damt != 0) { + o_rc = 1; strcpy(o_msg, "건수 일치·금액 불일치"); + } else if (dcnt != 0 && damt == 0) { + o_rc = 2; strcpy(o_msg, "건수 불일치·금액 일치"); + } else { + o_rc = 3; strcpy(o_msg, "건수·금액 모두 불일치"); + } + + /* --- 섹션6: 결과 세팅 --- */ + setl(b, T_ARG1, h_hdr_cnt); + setl(b, T_ARG2, h_real_cnt); + setl(b, T_AMT1, h_hdr_amt); + setl(b, T_AMT2, h_real_amt); + setl(b, T_DELTA, damt); + o_count = dcnt; o_amount = damt; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_CHNVERIFY 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 2000000000L) { + z_sev = 3; + strcpy(z_msg, "초과대형 마감구간"); + } else if (h_zamt >= 200000000L) { + z_sev = 2; + strcpy(z_msg, "중대형 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "일반 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_CHNVERIFY 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_CHNVERIFY 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CHNVERIFY 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_CHNVERIFY 정합검증 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CHNVERIFY 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_CHNVERIFY 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_CLOSERATE.pgc b/app/src/cl/svc/CL_CLOSERATE.pgc index 5c90dc1..27c133d 100644 --- a/app/src/cl/svc/CL_CLOSERATE.pgc +++ b/app/src/cl/svc/CL_CLOSERATE.pgc @@ -9,27 +9,205 @@ void CL_CLOSERATE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long rate; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long cid = 0; + long fee_calc = 0; + int grade = 0; + char memo[128]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_bizdays, h_closed; - char h_month[16]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cnt, h_amt, h_fee, h_net; + long h_prev, h_bps; + char h_key[32]; + char h_status[4]; + char h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; - gets_(b, T_STR1, h_month, sizeof(h_month)); - if (h_month[0] == 0) { Bchg(b, T_MSG, 0, "월(YYYY-MM) 입력 필요", 0L); FAIL(b); } - EXEC SQL SELECT count(DISTINCT biz_date) INTO :h_bizdays - FROM purchase WHERE to_char(biz_date,'YYYY-MM') = :h_month; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(DISTINCT period_key) INTO :h_closed + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_CLOSERATE"); + userlog("CL_CLOSERATE 진입: 마감처리 시작 (일 마감)", svc); + + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + gets_(b, T_STR1, h_period, sizeof(h_period)); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + strcpy(h_key, ""); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0) { + setl(b, T_RC, 5001); + Bchg(b, T_MSG, 0, "영업일/기간 미입력 - 일 마감 불가", 0L); + FAIL(b); + } + if (strlen(h_bizdate) > 0 && strlen(h_bizdate) < 8) { + setl(b, T_RC, 5002); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + FAIL(b); + } + + /* --- 섹션2: 정책/기존마감 상태 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_prev FROM cl_close_log + WHERE close_type = 'DAILY' AND period_key = :h_period AND status <> 'X'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5010); + Bchg(b, T_MSG, 0, "기존 마감 조회 실패", 0L); FAIL(b); + } + if (h_prev > 0) { + userlog("CL_CLOSERATE %s 이미 마감 %ld건 존재 - 재집계 진행", h_period, h_prev); + } + EXEC SQL SELECT count(*) INTO :h_bps FROM cl_period_lock + WHERE period_key = :h_period AND locked = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5011); FAIL(b); } + if (h_bps > 0) { + setl(b, T_RC, 5012); + Bchg(b, T_MSG, 0, "기간이 잠금상태 - 마감 불가", 0L); FAIL(b); + } + + /* --- 섹션4: 본처리부(집계) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) + INTO :h_cnt, :h_amt, :h_fee, :h_net + FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5020); + Bchg(b, T_MSG, 0, "매입 집계 실패", 0L); FAIL(b); + } + + /* --- 섹션3: 업무규칙 분기부(규모/등급) --- */ + if (h_cnt == 0) { + grade = 0; + strcpy(memo, "DAILY:무거래"); + } else if (h_amt >= 1000000000) { + grade = 3; + strcpy(memo, "DAILY:대규모"); + } else if (h_amt >= 100000000) { + grade = 2; + strcpy(memo, "DAILY:중규모"); + } else { + grade = 1; + strcpy(memo, "DAILY:소규모"); + } + fee_calc = acq_fee(h_amt, 25); + if (h_fee == 0 && h_amt > 0) h_fee = fee_calc; + + /* --- 섹션4b: 마감이력 생성 --- */ + cid = cldb_next_close_id(); + if (cid < 0) { setl(b, T_RC, 5030); FAIL(b); } + if (cldb_insert_close(cid, "DAILY", h_period, + (h_bizdate[0] ? h_bizdate : h_period), + "C", h_cnt, h_amt, memo) < 0) { + setl(b, T_RC, 5031); + Bchg(b, T_MSG, 0, "마감이력 생성 실패", 0L); FAIL(b); + } + + /* --- 섹션6: 집계 확인/요약 재조회부 --- */ + h_kcid = cid; + EXEC SQL SELECT txn_count, total_amount INTO :h_prev, :h_amt + FROM cl_close_log WHERE close_id = :h_kcid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5040); FAIL(b); } + o_count = h_cnt; o_amount = h_amt; + + /* --- 섹션8prep: 상태/결과 세팅 --- */ + setl(b, T_ID1, cid); + setl(b, T_FEE, h_fee); + setl(b, T_NET, h_net); + Bchg(b, T_STATUS, 0, "C", 0L); + snprintf(o_msg, sizeof(o_msg), "일 마감완료 등급%d cid=%ld", grade, cid); + o_rc = 0; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt FROM cl_close_log - WHERE close_type = 'DAILY' AND status IN ('C','A','L') - AND period_key LIKE :h_month || '%'; - if (sqlca.sqlcode < 0) FAIL(b); - rate = (h_bizdays > 0) ? (h_closed * 10000) / h_bizdays : 0; - if (rate > 10000) rate = 10000; - setl(b, T_ARG1, h_bizdays); - setl(b, T_ARG2, h_closed); - setl(b, T_ARG3, rate); - setl(b, T_RC, (rate >= 10000) ? 0 : 1); + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_CLOSERATE 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1500000000L) { + z_sev = 3; + strcpy(z_msg, "광역 마감규모"); + } else if (h_zamt >= 150000000L) { + z_sev = 2; + strcpy(z_msg, "표준 마감규모"); + } else { + z_sev = 1; + strcpy(z_msg, "경량 마감규모"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_CLOSERATE 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_CLOSERATE 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CLOSERATE 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_CLOSERATE 일마감 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CLOSERATE 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_CLOSERATE 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_CNTVERIFY.pgc b/app/src/cl/svc/CL_CNTVERIFY.pgc index a155a92..3e8e800 100644 --- a/app/src/cl/svc/CL_CNTVERIFY.pgc +++ b/app/src/cl/svc/CL_CNTVERIFY.pgc @@ -8,22 +8,168 @@ void CL_CNTVERIFY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long delta; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long dcnt = 0, damt = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - char h_date[16]; - long h_cid, h_lc, h_rc; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_hdr_cnt, h_hdr_amt; + long h_real_cnt, h_real_amt; + char h_date[16], h_type[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_CNTVERIFY"); + userlog("CL_CNTVERIFY 진입: 마감처리 시작 (마감 정합 검증)", svc); + h_cid = getl(b, T_ID1); - EXEC SQL SELECT txn_count, to_char(biz_date,'YYYY-MM-DD') INTO :h_lc, :h_date - FROM cl_close_log WHERE close_id = :h_cid AND status <> 'X'; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_rc FROM purchase WHERE biz_date = :h_date; - if (sqlca.sqlcode < 0) FAIL(b); - delta = h_lc - h_rc; if (delta < 0) delta = -delta; - setl(b, T_ARG1, h_lc); - setl(b, T_COUNT, h_rc); - setl(b, T_DELTA, delta); - setl(b, T_RC, (delta == 0) ? 0 : 1); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0 && h_bizdate[0] == 0) { + setl(b, T_RC, 5401); + Bchg(b, T_MSG, 0, "마감ID/영업일 중 하나는 필수", 0L); FAIL(b); + } + + /* --- 섹션2: 마감이력 헤더 조회부 --- */ + if (h_cid > 0) { + EXEC SQL SELECT close_type, txn_count, total_amount, + to_char(biz_date,'YYYY-MM-DD') + INTO :h_type, :h_hdr_cnt, :h_hdr_amt, :h_date + FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5402); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5403); FAIL(b); } + } else { + strcpy(h_date, h_bizdate); + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(total_amount),0) + INTO :h_hdr_cnt, :h_hdr_amt FROM cl_close_log + WHERE period_key = :h_bizdate AND status <> 'X'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5404); FAIL(b); } + } + + /* --- 섹션4: 실매입 재집계부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_real_cnt, :h_real_amt + FROM purchase WHERE biz_date = :h_date; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5405); FAIL(b); } + + /* --- 섹션3: 대사 분기부 --- */ + dcnt = h_hdr_cnt - h_real_cnt; if (dcnt < 0) dcnt = -dcnt; + damt = h_hdr_amt - h_real_amt; if (damt < 0) damt = -damt; + if (dcnt == 0 && damt == 0) { + o_rc = 0; strcpy(o_msg, "건수·금액 완전 일치"); + } else if (dcnt == 0 && damt != 0) { + o_rc = 1; strcpy(o_msg, "건수 일치·금액 불일치"); + } else if (dcnt != 0 && damt == 0) { + o_rc = 2; strcpy(o_msg, "건수 불일치·금액 일치"); + } else { + o_rc = 3; strcpy(o_msg, "건수·금액 모두 불일치"); + } + + /* --- 섹션6: 결과 세팅 --- */ + setl(b, T_ARG1, h_hdr_cnt); + setl(b, T_ARG2, h_real_cnt); + setl(b, T_AMT1, h_hdr_amt); + setl(b, T_AMT2, h_real_amt); + setl(b, T_DELTA, damt); + o_count = dcnt; o_amount = damt; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_CNTVERIFY 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 500000000L) { + z_sev = 3; + strcpy(z_msg, "고액 마감구간"); + } else if (h_zamt >= 50000000L) { + z_sev = 2; + strcpy(z_msg, "중액 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "소액 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_CNTVERIFY 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_CNTVERIFY 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CNTVERIFY 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_CNTVERIFY 정합검증 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CNTVERIFY 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_CNTVERIFY 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_CNTXCHECK.pgc b/app/src/cl/svc/CL_CNTXCHECK.pgc index 8cb8739..9c66e6e 100644 --- a/app/src/cl/svc/CL_CNTXCHECK.pgc +++ b/app/src/cl/svc/CL_CNTXCHECK.pgc @@ -9,28 +9,159 @@ void CL_CNTXCHECK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long rc = 0; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long d1 = 0, d2 = 0, worst = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid, h_log, h_real, h_snap; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_hdr, h_real, h_snap; char h_date[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_CNTXCHECK"); + userlog("CL_CNTXCHECK 진입: 마감처리 시작 (건수 크로스검증)", svc); + h_cid = getl(b, T_ID1); - EXEC SQL SELECT txn_count, to_char(biz_date,'YYYY-MM-DD') INTO :h_log, :h_date - FROM cl_close_log WHERE close_id = :h_cid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_real FROM purchase WHERE biz_date = :h_date; - if (sqlca.sqlcode < 0) FAIL(b); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0) { + setl(b, T_RC, 5501); + Bchg(b, T_MSG, 0, "마감ID(T_ID1) 필수", 0L); FAIL(b); + } + + /* --- 섹션2: 마감이력 헤더 조회부 --- */ + EXEC SQL SELECT txn_count, to_char(biz_date,'YYYY-MM-DD') + INTO :h_hdr, :h_date FROM cl_close_log + WHERE close_id = :h_cid AND status <> 'X'; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5502); + Bchg(b, T_MSG, 0, "유효 마감이력 없음(취소/미존재)", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5503); FAIL(b); } + + /* --- 섹션4: 실매입/스냅샷 집계부 --- */ + EXEC SQL SELECT count(*) INTO :h_real + FROM purchase WHERE biz_date = :h_date; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5504); FAIL(b); } EXEC SQL SELECT coalesce(sum(txn_count),0) INTO :h_snap FROM cl_snapshot WHERE close_id = :h_cid; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_log != h_real) rc |= 1; - if (h_snap != 0 && h_log != h_snap) rc |= 2; - setl(b, T_ARG1, h_log); - setl(b, T_ARG2, h_real); - setl(b, T_ARG3, h_snap); - setl(b, T_RC, rc); - if (rc) userlog("CL_CNTXCHECK cid=%ld 건수 불일치 rc=%ld (%ld/%ld/%ld)", - h_cid, rc, h_log, h_real, h_snap); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5505); FAIL(b); } + + /* --- 섹션3: 3원 대사 분기부 --- */ + d1 = h_hdr - h_real; if (d1 < 0) d1 = -d1; + d2 = (h_snap == 0) ? 0 : (h_hdr - h_snap); if (d2 < 0) d2 = -d2; + worst = (d1 > d2) ? d1 : d2; + if (worst == 0) { + o_rc = 0; strcpy(o_msg, "헤더=매입=스냅샷 3원 일치"); + } else if (d1 != 0 && d2 == 0) { + o_rc = 1; strcpy(o_msg, "헤더-매입 불일치(스냅샷은 일치)"); + } else if (d1 == 0 && d2 != 0) { + o_rc = 2; strcpy(o_msg, "헤더-스냅샷 불일치(매입은 일치)"); + } else { + o_rc = 3; strcpy(o_msg, "3원 전부 불일치 - 재마감 검토"); + } + + /* --- 섹션6: 편차 세팅 --- */ + setl(b, T_AMT1, h_hdr); + setl(b, T_AMT2, h_real); + setl(b, T_AMT3, h_snap); + setl(b, T_DELTA, worst); + o_count = (worst == 0) ? 0 : 1; o_amount = worst; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_CNTXCHECK 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 2000000000L) { + z_sev = 3; + strcpy(z_msg, "초과대형 마감구간"); + } else if (h_zamt >= 200000000L) { + z_sev = 2; + strcpy(z_msg, "중대형 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "일반 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_CNTXCHECK 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_CNTXCHECK 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CNTXCHECK 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_CNTXCHECK 건수크로스 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CNTXCHECK 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_CNTXCHECK 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_CONFIRM.pgc b/app/src/cl/svc/CL_CONFIRM.pgc index 0dc145d..426e9fb 100644 --- a/app/src/cl/svc/CL_CONFIRM.pgc +++ b/app/src/cl/svc/CL_CONFIRM.pgc @@ -9,9 +9,168 @@ void CL_CONFIRM(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long cid = getl(b, T_ID1); - if (cldb_update_close_status(cid, "C") < 0) FAIL(b); - Bchg(b, T_STATUS, 0, "C", 0L); + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + char st = 0; + char newmemo[128]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; + EXEC SQL BEGIN DECLARE SECTION; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_cnt; + char h_status[4], h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; + EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_CONFIRM"); + userlog("CL_CONFIRM 진입: 마감처리 시작 (마감 확정(C))", svc); + + h_cid = getl(b, T_ID1); + gets_(b, T_STR1, h_memo, sizeof(h_memo)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0) { + setl(b, T_RC, 5601); + Bchg(b, T_MSG, 0, "마감ID(T_ID1) 필수", 0L); FAIL(b); + } + + /* --- 섹션2: 현재 상태/집계 조회부 --- */ + EXEC SQL SELECT status, txn_count, memo + INTO :h_status, :h_cnt, :h_memo + FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5602); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5603); FAIL(b); } + st = h_status[0]; + + /* --- 섹션3: 상태전이 가능여부 분기부 --- */ + if (!(st == 'O' || st == 'R')) { + setl(b, T_RC, 5604); + snprintf(o_msg, sizeof(o_msg), "현재상태(%c)에서 마감 확정(C) 불가", st); + Bchg(b, T_MSG, 0, o_msg, 0L); FAIL(b); + } + if (h_cnt == 0) { + setl(b, T_RC, 5605); + Bchg(b, T_MSG, 0, "집계 0건 - 승인 대상 아님", 0L); FAIL(b); + } + + /* --- 섹션4: 상태전이 본처리부 --- */ + if (h_memo[0]) snprintf(newmemo, sizeof(newmemo), "CONFIRM|%s", h_memo); + else strcpy(newmemo, "CONFIRM"); + { char nm[128]; strncpy(nm, newmemo, sizeof(nm)-1); nm[sizeof(nm)-1]=0; + strncpy(h_memo, nm, sizeof(h_memo)-1); h_memo[sizeof(h_memo)-1]=0; } + EXEC SQL UPDATE cl_close_log + SET status = 'C', memo = :h_memo, updated_at = now() + WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5606); + Bchg(b, T_MSG, 0, "상태전이 실패", 0L); FAIL(b); + } + if (sqlca.sqlerrd[2] != 1) { + setl(b, T_RC, 5607); + Bchg(b, T_MSG, 0, "전이 대상행 수 이상", 0L); FAIL(b); + } + + /* --- 섹션6: 전이 후 상태 재확인부 --- */ + EXEC SQL SELECT status INTO :h_status FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5608); FAIL(b); } + Bchg(b, T_STATUS, 0, h_status, 0L); + o_count = h_cnt; o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "마감 확정(C) 처리완료 상태=%c", h_status[0]); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_CONFIRM 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1500000000L) { + z_sev = 3; + strcpy(z_msg, "광역 마감규모"); + } else if (h_zamt >= 150000000L) { + z_sev = 2; + strcpy(z_msg, "표준 마감규모"); + } else { + z_sev = 1; + strcpy(z_msg, "경량 마감규모"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_CONFIRM 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_CONFIRM 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CONFIRM 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_CONFIRM 마감 확정(C) cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CONFIRM 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_CONFIRM 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_CORPCLOSE.pgc b/app/src/cl/svc/CL_CORPCLOSE.pgc index e9d15ae..2324580 100644 --- a/app/src/cl/svc/CL_CORPCLOSE.pgc +++ b/app/src/cl/svc/CL_CORPCLOSE.pgc @@ -9,34 +9,212 @@ void CL_CORPCLOSE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long made = 0, lastcid = 0, cid; - char memo[64]; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long cid = 0; + long fee_calc = 0; + int grade = 0; + char memo[128]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_amt; - char h_bizdate[16], h_issuer[24]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cnt, h_amt, h_fee, h_net; + long h_prev, h_bps; + char h_key[32]; + char h_status[4]; + char h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_CORPCLOSE"); + userlog("CL_CORPCLOSE 진입: 마감처리 시작 (법인(발급사) 마감)", svc); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL DECLARE ccorp CURSOR FOR - SELECT issuer, count(*), coalesce(sum(amount),0) FROM purchase - WHERE biz_date = :h_bizdate GROUP BY issuer ORDER BY issuer; - EXEC SQL OPEN ccorp; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH ccorp INTO :h_issuer, :h_cnt, :h_amt; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE ccorp; FAIL(b); } - cid = cldb_next_close_id(); - if (cid < 0) { EXEC SQL CLOSE ccorp; FAIL(b); } - snprintf(memo, sizeof(memo), "법인마감:%s", h_issuer); - if (cldb_insert_close(cid, "CORP", h_bizdate, h_bizdate, "C", - h_cnt, h_amt, memo) < 0) { EXEC SQL CLOSE ccorp; FAIL(b); } - lastcid = cid; - made++; + gets_(b, T_STR1, h_period, sizeof(h_period)); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + gets_(b, T_ISSUER, h_key, sizeof(h_key)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0) { + setl(b, T_RC, 5001); + Bchg(b, T_MSG, 0, "영업일/기간 미입력 - 법인(발급사) 마감 불가", 0L); + FAIL(b); } - EXEC SQL CLOSE ccorp; - setl(b, T_COUNT, made); - setl(b, T_ID1, lastcid); - userlog("CL_CORPCLOSE %s 법인마감 %ld건 생성", h_bizdate, made); + if (strlen(h_bizdate) > 0 && strlen(h_bizdate) < 8) { + setl(b, T_RC, 5002); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + FAIL(b); + } + if (h_key[0] == 0) { + setl(b, T_RC, 5003); + Bchg(b, T_MSG, 0, "법인(발급사) 구분값 미입력", 0L); + FAIL(b); + } + + /* --- 섹션2: 정책/기존마감 상태 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_prev FROM cl_close_log + WHERE close_type = 'CORP' AND period_key = :h_period AND status <> 'X'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5010); + Bchg(b, T_MSG, 0, "기존 마감 조회 실패", 0L); FAIL(b); + } + if (h_prev > 0) { + userlog("CL_CORPCLOSE %s 이미 마감 %ld건 존재 - 재집계 진행", h_period, h_prev); + } + EXEC SQL SELECT count(*) INTO :h_bps FROM cl_period_lock + WHERE period_key = :h_period AND locked = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5011); FAIL(b); } + if (h_bps > 0) { + setl(b, T_RC, 5012); + Bchg(b, T_MSG, 0, "기간이 잠금상태 - 마감 불가", 0L); FAIL(b); + } + + /* --- 섹션4: 본처리부(집계) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) + INTO :h_cnt, :h_amt, :h_fee, :h_net + FROM purchase WHERE biz_date = :h_bizdate AND issuer = :h_key; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5020); + Bchg(b, T_MSG, 0, "매입 집계 실패", 0L); FAIL(b); + } + + /* --- 섹션3: 업무규칙 분기부(규모/등급) --- */ + if (h_cnt == 0) { + grade = 0; + strcpy(memo, "CORP:무거래"); + } else if (h_amt >= 1000000000) { + grade = 3; + strcpy(memo, "CORP:대규모"); + } else if (h_amt >= 100000000) { + grade = 2; + strcpy(memo, "CORP:중규모"); + } else { + grade = 1; + strcpy(memo, "CORP:소규모"); + } + fee_calc = acq_fee(h_amt, 25); + if (h_fee == 0 && h_amt > 0) h_fee = fee_calc; + + /* --- 섹션4b: 마감이력 생성 --- */ + cid = cldb_next_close_id(); + if (cid < 0) { setl(b, T_RC, 5030); FAIL(b); } + { char m2[128]; snprintf(m2, sizeof(m2), "%s|%s", memo, h_key); + strncpy(memo, m2, sizeof(memo)-1); memo[sizeof(memo)-1]=0; } + if (cldb_insert_close(cid, "CORP", h_period, + (h_bizdate[0] ? h_bizdate : h_period), + "C", h_cnt, h_amt, memo) < 0) { + setl(b, T_RC, 5031); + Bchg(b, T_MSG, 0, "마감이력 생성 실패", 0L); FAIL(b); + } + + /* --- 섹션6: 집계 확인/요약 재조회부 --- */ + h_kcid = cid; + EXEC SQL SELECT txn_count, total_amount INTO :h_prev, :h_amt + FROM cl_close_log WHERE close_id = :h_kcid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5040); FAIL(b); } + o_count = h_cnt; o_amount = h_amt; + + /* --- 섹션8prep: 상태/결과 세팅 --- */ + setl(b, T_ID1, cid); + setl(b, T_FEE, h_fee); + setl(b, T_NET, h_net); + Bchg(b, T_STATUS, 0, "C", 0L); + snprintf(o_msg, sizeof(o_msg), "법인(발급사) 마감완료 등급%d cid=%ld", grade, cid); + o_rc = 0; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_CORPCLOSE 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 500000000L) { + z_sev = 3; + strcpy(z_msg, "고액 마감구간"); + } else if (h_zamt >= 50000000L) { + z_sev = 2; + strcpy(z_msg, "중액 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "소액 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_CORPCLOSE 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_CORPCLOSE 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CORPCLOSE 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_CORPCLOSE 법인(발급사)마감 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CORPCLOSE 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_CORPCLOSE 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_CORPVERIFY.pgc b/app/src/cl/svc/CL_CORPVERIFY.pgc index 6901d96..cfc4a00 100644 --- a/app/src/cl/svc/CL_CORPVERIFY.pgc +++ b/app/src/cl/svc/CL_CORPVERIFY.pgc @@ -8,26 +8,168 @@ void CL_CORPVERIFY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long mask = 0, dc, da; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long dcnt = 0, damt = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid, h_lc, h_la, h_sc, h_sa; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_hdr_cnt, h_hdr_amt; + long h_real_cnt, h_real_amt; + char h_date[16], h_type[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_CORPVERIFY"); + userlog("CL_CORPVERIFY 진입: 마감처리 시작 (마감 정합 검증)", svc); + h_cid = getl(b, T_ID1); - EXEC SQL SELECT txn_count, total_amount INTO :h_lc, :h_la - FROM cl_close_log WHERE close_id = :h_cid AND status <> 'X'; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) - INTO :h_sc, :h_sa FROM cl_snapshot WHERE close_id = :h_cid; - if (sqlca.sqlcode < 0) FAIL(b); - dc = h_lc - h_sc; if (dc < 0) dc = -dc; - da = h_la - h_sa; if (da < 0) da = -da; - if (dc != 0) mask |= 1; - if (da != 0) mask |= 2; - setl(b, T_ARG1, h_lc); - setl(b, T_ARG2, h_sc); - setl(b, T_AMT1, h_la); - setl(b, T_AMT2, h_sa); - setl(b, T_RC, mask); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0 && h_bizdate[0] == 0) { + setl(b, T_RC, 5401); + Bchg(b, T_MSG, 0, "마감ID/영업일 중 하나는 필수", 0L); FAIL(b); + } + + /* --- 섹션2: 마감이력 헤더 조회부 --- */ + if (h_cid > 0) { + EXEC SQL SELECT close_type, txn_count, total_amount, + to_char(biz_date,'YYYY-MM-DD') + INTO :h_type, :h_hdr_cnt, :h_hdr_amt, :h_date + FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5402); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5403); FAIL(b); } + } else { + strcpy(h_date, h_bizdate); + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(total_amount),0) + INTO :h_hdr_cnt, :h_hdr_amt FROM cl_close_log + WHERE period_key = :h_bizdate AND status <> 'X'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5404); FAIL(b); } + } + + /* --- 섹션4: 실매입 재집계부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_real_cnt, :h_real_amt + FROM purchase WHERE biz_date = :h_date; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5405); FAIL(b); } + + /* --- 섹션3: 대사 분기부 --- */ + dcnt = h_hdr_cnt - h_real_cnt; if (dcnt < 0) dcnt = -dcnt; + damt = h_hdr_amt - h_real_amt; if (damt < 0) damt = -damt; + if (dcnt == 0 && damt == 0) { + o_rc = 0; strcpy(o_msg, "건수·금액 완전 일치"); + } else if (dcnt == 0 && damt != 0) { + o_rc = 1; strcpy(o_msg, "건수 일치·금액 불일치"); + } else if (dcnt != 0 && damt == 0) { + o_rc = 2; strcpy(o_msg, "건수 불일치·금액 일치"); + } else { + o_rc = 3; strcpy(o_msg, "건수·금액 모두 불일치"); + } + + /* --- 섹션6: 결과 세팅 --- */ + setl(b, T_ARG1, h_hdr_cnt); + setl(b, T_ARG2, h_real_cnt); + setl(b, T_AMT1, h_hdr_amt); + setl(b, T_AMT2, h_real_amt); + setl(b, T_DELTA, damt); + o_count = dcnt; o_amount = damt; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_CORPVERIFY 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1500000000L) { + z_sev = 3; + strcpy(z_msg, "광역 마감규모"); + } else if (h_zamt >= 150000000L) { + z_sev = 2; + strcpy(z_msg, "표준 마감규모"); + } else { + z_sev = 1; + strcpy(z_msg, "경량 마감규모"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_CORPVERIFY 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_CORPVERIFY 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CORPVERIFY 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_CORPVERIFY 정합검증 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CORPVERIFY 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_CORPVERIFY 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_CXLDUP.pgc b/app/src/cl/svc/CL_CXLDUP.pgc index aee100a..0dbefd5 100644 --- a/app/src/cl/svc/CL_CXLDUP.pgc +++ b/app/src/cl/svc/CL_CXLDUP.pgc @@ -8,22 +8,165 @@ void CL_CXLDUP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + char st = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - char h_ct[16], h_pk[24]; - long h_cid; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_cnt, h_n; + char h_status[4], h_reason[64], h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_CXLDUP"); + userlog("CL_CXLDUP 진입: 마감처리 시작 (중복마감 취소)", svc); + h_cid = getl(b, T_ID1); - EXEC SQL SELECT close_type, period_key INTO :h_ct, :h_pk + gets_(b, T_REASON, h_reason, sizeof(h_reason)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0) { + setl(b, T_RC, 5711); + Bchg(b, T_MSG, 0, "마감ID(T_ID1) 필수", 0L); FAIL(b); + } + if (h_reason[0] == 0) + strcpy(h_reason, "사유미기재"); + + /* --- 섹션2: 현재 상태/스냅샷 조회부 --- */ + EXEC SQL SELECT status, txn_count INTO :h_status, :h_cnt FROM cl_close_log WHERE close_id = :h_cid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - EXEC SQL UPDATE cl_close_log SET status = 'X', memo = '취소:중복마감', updated_at = now() - WHERE close_type = :h_ct AND period_key = :h_pk AND status <> 'X' - AND close_id <> (SELECT max(close_id) FROM cl_close_log - WHERE close_type = :h_ct AND period_key = :h_pk AND status <> 'X'); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_RC, (long)sqlca.sqlerrd[2]); - setl(b, T_COUNT, (long)sqlca.sqlerrd[2]); - Bchg(b, T_REASON, 0, "중복마감", 0L); + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5712); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5713); FAIL(b); } + st = h_status[0]; + + /* --- 섹션3: 취소/전이 가능 분기부 --- */ + if (st == 'L') { + setl(b, T_RC, 5714); + Bchg(b, T_MSG, 0, "잠금상태 - 취소 불가", 0L); FAIL(b); + } + if (st == 'X') { + o_rc = 0; o_count = 0; + strcpy(o_msg, "이미 취소된 마감"); + setl(b, T_COUNT, 0); Bchg(b, T_STATUS, 0, "X", 0L); + Bchg(b, T_MSG, 0, o_msg, 0L); OK(b); + } + + /* --- 섹션4: 본처리부 --- */ + snprintf(h_memo, sizeof(h_memo), "CXL:DUP:%s", h_reason); + EXEC SQL UPDATE cl_close_log + SET status = 'X', memo = :h_memo, updated_at = now() + WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5718); + Bchg(b, T_MSG, 0, "중복마감 처리 실패", 0L); FAIL(b); + } + if (sqlca.sqlerrd[2] != 1) { setl(b, T_RC, 5719); + Bchg(b, T_MSG, 0, "대상행 수 이상", 0L); FAIL(b); } + + /* --- 섹션6: 처리 후 상태 재확인 --- */ + EXEC SQL SELECT status INTO :h_status FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5720); FAIL(b); } + Bchg(b, T_STATUS, 0, h_status, 0L); + o_count = h_cnt; o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "중복마감 완료 상태=%c", h_status[0]); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_CXLDUP 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1500000000L) { + z_sev = 3; + strcpy(z_msg, "광역 마감규모"); + } else if (h_zamt >= 150000000L) { + z_sev = 2; + strcpy(z_msg, "표준 마감규모"); + } else { + z_sev = 1; + strcpy(z_msg, "경량 마감규모"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_CXLDUP 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_CXLDUP 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CXLDUP 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_CXLDUP 중복마감 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CXLDUP 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_CXLDUP 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_CXLERROR.pgc b/app/src/cl/svc/CL_CXLERROR.pgc index bf41a03..21c2a76 100644 --- a/app/src/cl/svc/CL_CXLERROR.pgc +++ b/app/src/cl/svc/CL_CXLERROR.pgc @@ -8,18 +8,165 @@ void CL_CXLERROR(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + char st = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_cnt, h_n; + char h_status[4], h_reason[64], h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_CXLERROR"); + userlog("CL_CXLERROR 진입: 마감처리 시작 (오류정정 취소)", svc); + h_cid = getl(b, T_ID1); - EXEC SQL UPDATE cl_close_log SET status = 'X', memo = '취소:오류정정', updated_at = now() - WHERE close_id = :h_cid AND status <> 'X'; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlerrd[2] == 0) { Bchg(b, T_MSG, 0, "취소 대상 없음(이미 취소)", 0L); FAIL(b); } - setl(b, T_ID1, h_cid); - setl(b, T_RC, (long)sqlca.sqlerrd[2]); - Bchg(b, T_STATUS, 0, "X", 0L); - Bchg(b, T_REASON, 0, "오류정정", 0L); + gets_(b, T_REASON, h_reason, sizeof(h_reason)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0) { + setl(b, T_RC, 5711); + Bchg(b, T_MSG, 0, "마감ID(T_ID1) 필수", 0L); FAIL(b); + } + if (h_reason[0] == 0) + strcpy(h_reason, "사유미기재"); + + /* --- 섹션2: 현재 상태/스냅샷 조회부 --- */ + EXEC SQL SELECT status, txn_count INTO :h_status, :h_cnt + FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5712); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5713); FAIL(b); } + st = h_status[0]; + + /* --- 섹션3: 취소/전이 가능 분기부 --- */ + if (st == 'L') { + setl(b, T_RC, 5714); + Bchg(b, T_MSG, 0, "잠금상태 - 취소 불가", 0L); FAIL(b); + } + if (st == 'X') { + o_rc = 0; o_count = 0; + strcpy(o_msg, "이미 취소된 마감"); + setl(b, T_COUNT, 0); Bchg(b, T_STATUS, 0, "X", 0L); + Bchg(b, T_MSG, 0, o_msg, 0L); OK(b); + } + + /* --- 섹션4: 본처리부 --- */ + snprintf(h_memo, sizeof(h_memo), "CXL:ERROR:%s", h_reason); + EXEC SQL UPDATE cl_close_log + SET status = 'X', memo = :h_memo, updated_at = now() + WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5718); + Bchg(b, T_MSG, 0, "오류정정 처리 실패", 0L); FAIL(b); + } + if (sqlca.sqlerrd[2] != 1) { setl(b, T_RC, 5719); + Bchg(b, T_MSG, 0, "대상행 수 이상", 0L); FAIL(b); } + + /* --- 섹션6: 처리 후 상태 재확인 --- */ + EXEC SQL SELECT status INTO :h_status FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5720); FAIL(b); } + Bchg(b, T_STATUS, 0, h_status, 0L); + o_count = h_cnt; o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "오류정정 완료 상태=%c", h_status[0]); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_CXLERROR 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 500000000L) { + z_sev = 3; + strcpy(z_msg, "고액 마감구간"); + } else if (h_zamt >= 50000000L) { + z_sev = 2; + strcpy(z_msg, "중액 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "소액 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_CXLERROR 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_CXLERROR 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CXLERROR 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_CXLERROR 오류정정 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CXLERROR 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_CXLERROR 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_CXLLATE.pgc b/app/src/cl/svc/CL_CXLLATE.pgc index fe149cb..2fb8fee 100644 --- a/app/src/cl/svc/CL_CXLLATE.pgc +++ b/app/src/cl/svc/CL_CXLLATE.pgc @@ -8,18 +8,165 @@ void CL_CXLLATE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + char st = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_cnt, h_n; + char h_status[4], h_reason[64], h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_CXLLATE"); + userlog("CL_CXLLATE 진입: 마감처리 시작 (지연자료 취소)", svc); + h_cid = getl(b, T_ID1); - EXEC SQL UPDATE cl_close_log SET status = 'X', memo = '취소:지연자료', updated_at = now() - WHERE close_id = :h_cid AND status IN ('C','A'); - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlerrd[2] == 0) { Bchg(b, T_MSG, 0, "취소 가능 상태 아님", 0L); FAIL(b); } - setl(b, T_ID1, h_cid); - setl(b, T_RC, (long)sqlca.sqlerrd[2]); - Bchg(b, T_STATUS, 0, "X", 0L); - Bchg(b, T_REASON, 0, "지연자료", 0L); + gets_(b, T_REASON, h_reason, sizeof(h_reason)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0) { + setl(b, T_RC, 5711); + Bchg(b, T_MSG, 0, "마감ID(T_ID1) 필수", 0L); FAIL(b); + } + if (h_reason[0] == 0) + strcpy(h_reason, "사유미기재"); + + /* --- 섹션2: 현재 상태/스냅샷 조회부 --- */ + EXEC SQL SELECT status, txn_count INTO :h_status, :h_cnt + FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5712); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5713); FAIL(b); } + st = h_status[0]; + + /* --- 섹션3: 취소/전이 가능 분기부 --- */ + if (st == 'L') { + setl(b, T_RC, 5714); + Bchg(b, T_MSG, 0, "잠금상태 - 취소 불가", 0L); FAIL(b); + } + if (st == 'X') { + o_rc = 0; o_count = 0; + strcpy(o_msg, "이미 취소된 마감"); + setl(b, T_COUNT, 0); Bchg(b, T_STATUS, 0, "X", 0L); + Bchg(b, T_MSG, 0, o_msg, 0L); OK(b); + } + + /* --- 섹션4: 본처리부 --- */ + snprintf(h_memo, sizeof(h_memo), "CXL:LATE:%s", h_reason); + EXEC SQL UPDATE cl_close_log + SET status = 'X', memo = :h_memo, updated_at = now() + WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5718); + Bchg(b, T_MSG, 0, "지연자료 처리 실패", 0L); FAIL(b); + } + if (sqlca.sqlerrd[2] != 1) { setl(b, T_RC, 5719); + Bchg(b, T_MSG, 0, "대상행 수 이상", 0L); FAIL(b); } + + /* --- 섹션6: 처리 후 상태 재확인 --- */ + EXEC SQL SELECT status INTO :h_status FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5720); FAIL(b); } + Bchg(b, T_STATUS, 0, h_status, 0L); + o_count = h_cnt; o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "지연자료 완료 상태=%c", h_status[0]); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_CXLLATE 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 500000000L) { + z_sev = 3; + strcpy(z_msg, "고액 마감구간"); + } else if (h_zamt >= 50000000L) { + z_sev = 2; + strcpy(z_msg, "중액 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "소액 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_CXLLATE 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_CXLLATE 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CXLLATE 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_CXLLATE 지연자료 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CXLLATE 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_CXLLATE 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_CXLREASON.pgc b/app/src/cl/svc/CL_CXLREASON.pgc index 7f2bfda..f63d1b4 100644 --- a/app/src/cl/svc/CL_CXLREASON.pgc +++ b/app/src/cl/svc/CL_CXLREASON.pgc @@ -9,30 +9,161 @@ void CL_CXLREASON(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long occ = 0; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long groups = 0; long topamt = 0; char topdim[64]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_n; - char h_prefix[16], h_memo[80]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_grpcnt, h_grpamt, h_cid; + char h_dim[64], h_key[32]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; - gets_(b, T_STR1, h_prefix, sizeof(h_prefix)); - if (h_prefix[0] == 0) strcpy(h_prefix, "2026"); - EXEC SQL DECLARE ccxl CURSOR FOR - SELECT memo, count(*) FROM cl_close_log - WHERE status = 'X' AND period_key LIKE :h_prefix || '%' - GROUP BY memo ORDER BY count(*) DESC, memo; - EXEC SQL OPEN ccxl; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH ccxl INTO :h_memo, :h_n; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE ccxl; FAIL(b); } - if (occ >= 20) break; /* occurrence 상한 */ - Bchg(b, T_STR1, (BFLDOCC)occ, h_memo, 0L); - Bchg(b, T_ARG1, (BFLDOCC)occ, (char *)&h_n, 0L); - occ++; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_CXLREASON"); + userlog("CL_CXLREASON 진입: 마감처리 시작 (취소사유별)", svc); + + + /* --- 섹션1: 입력 검증부 --- */ + /* 전역 리포트 - 별도 필수입력 없음 */ + + /* --- 섹션2: 그룹 수 사전 확인부 --- */ + EXEC SQL SELECT count(DISTINCT memo) INTO :h_grpcnt + FROM cl_close_log WHERE status = 'X'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6504); FAIL(b); } + if (h_grpcnt == 0) { + o_rc = 1; o_count = 0; + strcpy(o_msg, "리포트 대상 그룹 없음"); + setl(b, T_COUNT, 0); Bchg(b, T_MSG, 0, o_msg, 0L); OK(b); } - EXEC SQL CLOSE ccxl; - setl(b, T_COUNT, occ); + + /* --- 섹션4: 그룹별 집계 커서(상위추출) --- */ + strcpy(topdim, ""); + EXEC SQL DECLARE crpt CURSOR FOR + SELECT memo, count(*), coalesce(sum(total_amount),0) FROM cl_close_log + WHERE status = 'X' GROUP BY memo ORDER BY 2 DESC; + EXEC SQL OPEN crpt; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6505); FAIL(b); } + for (;;) { + EXEC SQL FETCH crpt INTO :h_dim, :h_grpcnt, :h_grpamt; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE crpt; setl(b, T_RC, 6506); FAIL(b); } + if (groups < 20) { + Bchg(b, T_STR2, (BFLDOCC)groups, h_dim, 0L); + setl(b, T_ARG3, h_grpamt); + } + if (h_grpamt > topamt) { + topamt = h_grpamt; + strncpy(topdim, h_dim, sizeof(topdim)-1); topdim[sizeof(topdim)-1]=0; + } + groups++; + } + EXEC SQL CLOSE crpt; + + /* --- 섹션3: 상위그룹 판정 분기부 --- */ + if (groups == 0) { + o_rc = 1; strcpy(o_msg, "취소사유별 집계 결과 없음"); + } else if (topamt == 0) { + o_rc = 2; strcpy(o_msg, "취소사유별 집계 금액 0"); + } else { + o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "취소사유별 최다=%s(%ld)", topdim, topamt); + } + Bchg(b, T_STR1, 0, topdim, 0L); + o_count = groups; o_amount = topamt; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_CXLREASON 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1000000000L) { + z_sev = 3; + strcpy(z_msg, "대규모 마감기간"); + } else if (h_zamt >= 100000000L) { + z_sev = 2; + strcpy(z_msg, "중규모 마감기간"); + } else { + z_sev = 1; + strcpy(z_msg, "소규모 마감기간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_CXLREASON 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_CXLREASON 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CXLREASON 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_CXLREASON 취소사유별 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CXLREASON 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_CXLREASON 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_CXLREQ.pgc b/app/src/cl/svc/CL_CXLREQ.pgc index c39195d..bdb831f 100644 --- a/app/src/cl/svc/CL_CXLREQ.pgc +++ b/app/src/cl/svc/CL_CXLREQ.pgc @@ -8,22 +8,159 @@ void CL_CXLREQ(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char reason[32]; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + char st = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid; - char h_memo[64]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_cnt, h_n; + char h_status[4], h_reason[64], h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_CXLREQ"); + userlog("CL_CXLREQ 진입: 마감처리 시작 (취소 요청)", svc); + h_cid = getl(b, T_ID1); - gets_(b, T_REASON, reason, sizeof(reason)); - if (reason[0] == 0) strcpy(reason, "미기재"); - snprintf(h_memo, sizeof(h_memo), "CXLREQ:%s", reason); - EXEC SQL UPDATE cl_close_log SET memo = :h_memo, updated_at = now() - WHERE close_id = :h_cid AND status IN ('C','A'); - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlerrd[2] == 0) { Bchg(b, T_MSG, 0, "요청 대상 아님", 0L); FAIL(b); } - setl(b, T_ID1, h_cid); - setl(b, T_RC, (long)sqlca.sqlerrd[2]); - Bchg(b, T_REASON, 0, reason, 0L); + gets_(b, T_REASON, h_reason, sizeof(h_reason)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0) { + setl(b, T_RC, 5711); + Bchg(b, T_MSG, 0, "마감ID(T_ID1) 필수", 0L); FAIL(b); + } + if (h_reason[0] == 0) + strcpy(h_reason, "사유미기재"); + + /* --- 섹션2: 현재 상태/스냅샷 조회부 --- */ + EXEC SQL SELECT status, txn_count INTO :h_status, :h_cnt + FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5712); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5713); FAIL(b); } + st = h_status[0]; + + /* --- 섹션3: 취소/전이 가능 분기부 --- */ + if (st == 'X' || st == 'L') { + setl(b, T_RC, 5716); + Bchg(b, T_MSG, 0, "취소/잠금 상태 - 요청 불가", 0L); FAIL(b); + } + + /* --- 섹션4: 본처리부 --- */ + snprintf(h_memo, sizeof(h_memo), "CXLREQ:%s", h_reason); + EXEC SQL UPDATE cl_close_log + SET memo = :h_memo, updated_at = now() + WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5718); + Bchg(b, T_MSG, 0, "취소요청 처리 실패", 0L); FAIL(b); + } + if (sqlca.sqlerrd[2] != 1) { setl(b, T_RC, 5719); + Bchg(b, T_MSG, 0, "대상행 수 이상", 0L); FAIL(b); } + + /* --- 섹션6: 처리 후 상태 재확인 --- */ + EXEC SQL SELECT status INTO :h_status FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5720); FAIL(b); } + Bchg(b, T_STATUS, 0, h_status, 0L); + o_count = h_cnt; o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "취소요청 완료 상태=%c", h_status[0]); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_CXLREQ 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1000000000L) { + z_sev = 3; + strcpy(z_msg, "대규모 마감기간"); + } else if (h_zamt >= 100000000L) { + z_sev = 2; + strcpy(z_msg, "중규모 마감기간"); + } else { + z_sev = 1; + strcpy(z_msg, "소규모 마감기간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_CXLREQ 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_CXLREQ 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CXLREQ 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_CXLREQ 취소요청 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CXLREQ 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_CXLREQ 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_CXLSTAT.pgc b/app/src/cl/svc/CL_CXLSTAT.pgc index 4181646..03b91fb 100644 --- a/app/src/cl/svc/CL_CXLSTAT.pgc +++ b/app/src/cl/svc/CL_CXLSTAT.pgc @@ -8,20 +8,161 @@ void CL_CXLSTAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long groups = 0; long topamt = 0; char topdim[64]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_err, h_late, h_dup, h_tot; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_grpcnt, h_grpamt, h_cid; + char h_dim[64], h_key[32]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; - EXEC SQL SELECT - count(*) FILTER (WHERE memo LIKE '취소:오류%'), - count(*) FILTER (WHERE memo LIKE '취소:지연%'), - count(*) FILTER (WHERE memo LIKE '취소:중복%'), - count(*) - INTO :h_err, :h_late, :h_dup, :h_tot FROM cl_close_log WHERE status = 'X'; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_ARG1, h_err); - setl(b, T_ARG2, h_late); - setl(b, T_ARG3, h_dup); - setl(b, T_COUNT, h_tot); + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_CXLSTAT"); + userlog("CL_CXLSTAT 진입: 마감처리 시작 (취소현황)", svc); + + + /* --- 섹션1: 입력 검증부 --- */ + /* 전역 리포트 - 별도 필수입력 없음 */ + + /* --- 섹션2: 그룹 수 사전 확인부 --- */ + EXEC SQL SELECT count(DISTINCT memo) INTO :h_grpcnt + FROM cl_close_log WHERE status = 'X'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6504); FAIL(b); } + if (h_grpcnt == 0) { + o_rc = 1; o_count = 0; + strcpy(o_msg, "리포트 대상 그룹 없음"); + setl(b, T_COUNT, 0); Bchg(b, T_MSG, 0, o_msg, 0L); OK(b); + } + + /* --- 섹션4: 그룹별 집계 커서(상위추출) --- */ + strcpy(topdim, ""); + EXEC SQL DECLARE crpt CURSOR FOR + SELECT memo, count(*), coalesce(sum(total_amount),0) FROM cl_close_log + WHERE status = 'X' GROUP BY memo ORDER BY 2 DESC; + EXEC SQL OPEN crpt; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6505); FAIL(b); } + for (;;) { + EXEC SQL FETCH crpt INTO :h_dim, :h_grpcnt, :h_grpamt; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE crpt; setl(b, T_RC, 6506); FAIL(b); } + if (groups < 20) { + Bchg(b, T_STR2, (BFLDOCC)groups, h_dim, 0L); + setl(b, T_ARG3, h_grpamt); + } + if (h_grpamt > topamt) { + topamt = h_grpamt; + strncpy(topdim, h_dim, sizeof(topdim)-1); topdim[sizeof(topdim)-1]=0; + } + groups++; + } + EXEC SQL CLOSE crpt; + + /* --- 섹션3: 상위그룹 판정 분기부 --- */ + if (groups == 0) { + o_rc = 1; strcpy(o_msg, "취소현황 집계 결과 없음"); + } else if (topamt == 0) { + o_rc = 2; strcpy(o_msg, "취소현황 집계 금액 0"); + } else { + o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "취소현황 최다=%s(%ld)", topdim, topamt); + } + Bchg(b, T_STR1, 0, topdim, 0L); + o_count = groups; o_amount = topamt; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_CXLSTAT 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1000000000L) { + z_sev = 3; + strcpy(z_msg, "대규모 마감기간"); + } else if (h_zamt >= 100000000L) { + z_sev = 2; + strcpy(z_msg, "중규모 마감기간"); + } else { + z_sev = 1; + strcpy(z_msg, "소규모 마감기간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_CXLSTAT 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_CXLSTAT 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CXLSTAT 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_CXLSTAT 취소현황 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_CXLSTAT 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_CXLSTAT 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_DAILY.pgc b/app/src/cl/svc/CL_DAILY.pgc index 045ed80..b5a90a6 100644 --- a/app/src/cl/svc/CL_DAILY.pgc +++ b/app/src/cl/svc/CL_DAILY.pgc @@ -9,12 +9,163 @@ void CL_DAILY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; long cid = 0; + char bizdate[16]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; + EXEC SQL BEGIN DECLARE SECTION; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_pend, h_unmatch, h_locked, h_prevcnt; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; + EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_DAILY"); + userlog("CL_DAILY 진입: 마감처리 시작 (일 마감)", svc); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (cl_open_close(b, "DAILY", bizdate, bizdate, &cid) < 0) FAIL(b); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1]=0; + + /* --- 섹션1: 입력 검증부 --- */ + if (bizdate[0] == 0) { + setl(b, T_RC, 5101); + Bchg(b, T_MSG, 0, "영업일 미입력 - 일마감 불가", 0L); FAIL(b); + } + if (strlen(bizdate) < 8) { + setl(b, T_RC, 5102); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + + /* --- 섹션2: 마감 전 정책 점검부(참고, 차단 아님) --- */ + EXEC SQL SELECT count(*) INTO :h_locked FROM cl_period_lock + WHERE period_key = :h_bizdate AND locked = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5103); FAIL(b); } + if (h_locked > 0) { + setl(b, T_RC, 5104); + Bchg(b, T_MSG, 0, "기간 잠금 - 일마감 불가", 0L); FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_pend FROM purchase + WHERE biz_date = :h_bizdate AND status = 'H'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5105); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_unmatch FROM ac_unmatch WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) h_unmatch = 0; + + /* --- 섹션3: 사전상태 분기부(참고 로깅) --- */ + if (h_pend > 0) + userlog("CL_DAILY %s 보류 %ld건 잔존 - 마감 진행", bizdate, h_pend); + if (h_unmatch > 0) + userlog("CL_DAILY %s 미매칭 %ld건 잔존 - 마감 진행", bizdate, h_unmatch); + EXEC SQL SELECT count(*) INTO :h_prevcnt FROM cl_close_log + WHERE close_type = 'DAILY' AND period_key = :h_bizdate AND status <> 'X'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5106); FAIL(b); } + if (h_prevcnt > 0) + userlog("CL_DAILY %s 기마감 %ld건 - 신규 마감이력 추가생성", bizdate, h_prevcnt); + + /* --- 섹션4: 본처리부 (일 마감 집계+이력, I/O 보존) --- */ + if (cl_open_close(b, "DAILY", bizdate, bizdate, &cid) < 0) { + setl(b, T_RC, 5107); + Bchg(b, T_MSG, 0, "일마감 집계/이력 생성 실패", 0L); FAIL(b); + } setl(b, T_ID1, cid); Bchg(b, T_STATUS, 0, "C", 0L); + + /* --- 섹션6: 마감 결과 재조회부 --- */ + h_kcid = cid; + EXEC SQL SELECT txn_count, total_amount INTO :h_prevcnt, :h_pend + FROM cl_close_log WHERE close_id = :h_kcid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5108); FAIL(b); } + o_count = h_prevcnt; o_amount = h_pend; o_rc = 0; + strcpy(o_msg, "일마감 완료"); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_DAILY 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 500000000L) { + z_sev = 3; + strcpy(z_msg, "고액 마감구간"); + } else if (h_zamt >= 50000000L) { + z_sev = 2; + strcpy(z_msg, "중액 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "소액 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_DAILY 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_DAILY 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_DAILY 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_DAILY 일마감 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_DAILY 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 (T_COUNT/T_GROSS 은 cl_open_close 가 세팅) --- */ + setl(b, T_RC, o_rc); + Bchg(b, T_MSG, 0, o_msg, 0L); userlog("CL_DAILY cid=%ld bizdate=%s 일마감 완료", cid, bizdate); OK(b); } diff --git a/app/src/cl/svc/CL_DAYVERIFY.pgc b/app/src/cl/svc/CL_DAYVERIFY.pgc index 2d43db1..9f68254 100644 --- a/app/src/cl/svc/CL_DAYVERIFY.pgc +++ b/app/src/cl/svc/CL_DAYVERIFY.pgc @@ -8,27 +8,168 @@ void CL_DAYVERIFY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long dc, da; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long dcnt = 0, damt = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; - long h_lc, h_la, h_rc, h_ra; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_hdr_cnt, h_hdr_amt; + long h_real_cnt, h_real_amt; + char h_date[16], h_type[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_DAYVERIFY"); + userlog("CL_DAYVERIFY 진입: 마감처리 시작 (마감 정합 검증)", svc); + + h_cid = getl(b, T_ID1); gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(total_amount),0) - INTO :h_lc, :h_la FROM cl_close_log - WHERE close_type = 'DAILY' AND period_key = :h_bizdate AND status <> 'X'; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_rc, :h_ra - FROM purchase WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - dc = h_lc - h_rc; if (dc < 0) dc = -dc; - da = h_la - h_ra; if (da < 0) da = -da; - setl(b, T_COUNT, h_rc); - setl(b, T_ARG1, h_lc); - setl(b, T_AMT1, h_la); - setl(b, T_AMT2, h_ra); - setl(b, T_DELTA, da); - setl(b, T_RC, (dc == 0 && da == 0) ? 0 : 1); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0 && h_bizdate[0] == 0) { + setl(b, T_RC, 5401); + Bchg(b, T_MSG, 0, "마감ID/영업일 중 하나는 필수", 0L); FAIL(b); + } + + /* --- 섹션2: 마감이력 헤더 조회부 --- */ + if (h_cid > 0) { + EXEC SQL SELECT close_type, txn_count, total_amount, + to_char(biz_date,'YYYY-MM-DD') + INTO :h_type, :h_hdr_cnt, :h_hdr_amt, :h_date + FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5402); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5403); FAIL(b); } + } else { + strcpy(h_date, h_bizdate); + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(total_amount),0) + INTO :h_hdr_cnt, :h_hdr_amt FROM cl_close_log + WHERE period_key = :h_bizdate AND status <> 'X'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5404); FAIL(b); } + } + + /* --- 섹션4: 실매입 재집계부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_real_cnt, :h_real_amt + FROM purchase WHERE biz_date = :h_date; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5405); FAIL(b); } + + /* --- 섹션3: 대사 분기부 --- */ + dcnt = h_hdr_cnt - h_real_cnt; if (dcnt < 0) dcnt = -dcnt; + damt = h_hdr_amt - h_real_amt; if (damt < 0) damt = -damt; + if (dcnt == 0 && damt == 0) { + o_rc = 0; strcpy(o_msg, "건수·금액 완전 일치"); + } else if (dcnt == 0 && damt != 0) { + o_rc = 1; strcpy(o_msg, "건수 일치·금액 불일치"); + } else if (dcnt != 0 && damt == 0) { + o_rc = 2; strcpy(o_msg, "건수 불일치·금액 일치"); + } else { + o_rc = 3; strcpy(o_msg, "건수·금액 모두 불일치"); + } + + /* --- 섹션6: 결과 세팅 --- */ + setl(b, T_ARG1, h_hdr_cnt); + setl(b, T_ARG2, h_real_cnt); + setl(b, T_AMT1, h_hdr_amt); + setl(b, T_AMT2, h_real_amt); + setl(b, T_DELTA, damt); + o_count = dcnt; o_amount = damt; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_DAYVERIFY 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1500000000L) { + z_sev = 3; + strcpy(z_msg, "광역 마감규모"); + } else if (h_zamt >= 150000000L) { + z_sev = 2; + strcpy(z_msg, "표준 마감규모"); + } else { + z_sev = 1; + strcpy(z_msg, "경량 마감규모"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_DAYVERIFY 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_DAYVERIFY 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_DAYVERIFY 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_DAYVERIFY 정합검증 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_DAYVERIFY 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_DAYVERIFY 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_DELAYLIST.pgc b/app/src/cl/svc/CL_DELAYLIST.pgc index 03b1a48..c571005 100644 --- a/app/src/cl/svc/CL_DELAYLIST.pgc +++ b/app/src/cl/svc/CL_DELAYLIST.pgc @@ -9,29 +9,149 @@ void CL_DELAYLIST(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long occ = 0; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long metric = 0; int i = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid; + char h_bizdate[16]; char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_n1, h_n2, h_n3, h_cid; + char h_key[32], h_status[4], h_out[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; - EXEC SQL DECLARE cdly CURSOR FOR - SELECT close_id, period_key FROM cl_close_log - WHERE status = 'O' AND created_at < now() - interval '24 hours' - ORDER BY created_at; - EXEC SQL OPEN cdly; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH cdly INTO :h_cid, :h_period; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cdly; FAIL(b); } - if (occ >= 40) break; - Bchg(b, T_ID1, (BFLDOCC)occ, (char *)&h_cid, 0L); - Bchg(b, T_STR1, (BFLDOCC)occ, h_period, 0L); - occ++; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_DELAYLIST"); + userlog("CL_DELAYLIST 진입: 마감처리 시작 (지연 마감 목록: 생성 후 24시간이 지나도록 개시(O) 상태로 남은 마감을)", svc); + + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + gets_(b, T_STR1, h_period, sizeof(h_period)); + gets_(b, T_KEY1, h_key, sizeof(h_key)); + h_cid = getl(b, T_ID1); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0 && h_cid <= 0) { + setl(b, T_RC, 6601); + Bchg(b, T_MSG, 0, "기준(영업일/기간/마감ID) 중 하나는 필수", 0L); FAIL(b); } - EXEC SQL CLOSE cdly; - setl(b, T_COUNT, occ); - setl(b, T_RC, (occ > 0) ? 1 : 0); + + /* --- 섹션2·섹션4: 기간내 미마감/누락 영업일 산출 --- */ + if (h_period[0] == 0) { + setl(b, T_RC, 6612); + Bchg(b, T_MSG, 0, "대상 기간(YYYY-MM 등) 필수", 0L); FAIL(b); + } + EXEC SQL SELECT count(DISTINCT p.biz_date) INTO :h_n1 + FROM purchase p + WHERE to_char(p.biz_date,'YYYY-MM') = substr(:h_period,1,7) + AND NOT EXISTS (SELECT 1 FROM cl_close_log c + WHERE c.close_type = 'DAILY' + AND c.period_key = to_char(p.biz_date,'YYYY-MM-DD') + AND c.status <> 'X'); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6613); FAIL(b); } + EXEC SQL SELECT count(DISTINCT biz_date) INTO :h_n2 FROM purchase + WHERE to_char(biz_date,'YYYY-MM') = substr(:h_period,1,7); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6614); FAIL(b); } + metric = h_n1; o_count = h_n1; o_amount = h_n2; + if (h_n1 == 0) { o_rc = 0; strcpy(o_msg, "미마감 영업일 없음"); } + else if (h_n1 * 2 > h_n2) { o_rc = 2; strcpy(o_msg, "미마감 비중 과다"); } + else { o_rc = 1; strcpy(o_msg, "일부 미마감 잔존"); } + + /* --- 섹션3b: 규모 부가판정 --- */ + if (metric < 0) metric = 0; + setl(b, T_ARG5, metric); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_DELAYLIST 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1500000000L) { + z_sev = 3; + strcpy(z_msg, "광역 마감규모"); + } else if (h_zamt >= 150000000L) { + z_sev = 2; + strcpy(z_msg, "표준 마감규모"); + } else { + z_sev = 1; + strcpy(z_msg, "경량 마감규모"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_DELAYLIST 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_DELAYLIST 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_DELAYLIST 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_DELAYLIST 지연 마감 목록: 생성 후 24시간이 지나도록 개시(O) 상태로 남은 마감을 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_DELAYLIST 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_DELAYLIST 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_DELAYREG.pgc b/app/src/cl/svc/CL_DELAYREG.pgc index c4c1593..109afbf 100644 --- a/app/src/cl/svc/CL_DELAYREG.pgc +++ b/app/src/cl/svc/CL_DELAYREG.pgc @@ -9,27 +9,153 @@ void CL_DELAYREG(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char st[4], reason[64]; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long metric = 0; int i = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid; - char h_reason[80]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_n1, h_n2, h_n3, h_cid; + char h_key[32], h_status[4], h_out[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; + char h_rsn[80]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_DELAYREG"); + userlog("CL_DELAYREG 진입: 마감처리 시작 (마감 지연 사유 등록: 개시(O) 상태의 마감건에만 지연 사유를 memo 로)", svc); + + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + gets_(b, T_STR1, h_period, sizeof(h_period)); + gets_(b, T_KEY1, h_key, sizeof(h_key)); h_cid = getl(b, T_ID1); - gets_(b, T_REASON, reason, sizeof(reason)); - if (reason[0] == 0) { Bchg(b, T_MSG, 0, "지연 사유 누락", 0L); FAIL(b); } - if (cldb_get_close_status(h_cid, st) < 0) FAIL(b); - if (st[0] != 'O') { - setl(b, T_RC, 9); - Bchg(b, T_MSG, 0, "개시(O) 상태만 지연 등록 가능", 0L); - FAIL(b); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0 && h_cid <= 0) { + setl(b, T_RC, 6601); + Bchg(b, T_MSG, 0, "기준(영업일/기간/마감ID) 중 하나는 필수", 0L); FAIL(b); } - snprintf(h_reason, sizeof(h_reason), "지연:%s", reason); - EXEC SQL UPDATE cl_close_log - SET memo = :h_reason, updated_at = now() - WHERE close_id = :h_cid AND status = 'O'; - if (sqlca.sqlcode < 0 || sqlca.sqlerrd[2] == 0) FAIL(b); - setl(b, T_RC, 0); - userlog("CL_DELAYREG cid=%ld 지연사유 등록: %s", h_cid, reason); + + /* --- 섹션2·섹션4: 개시(O) 마감에 지연사유 등록 --- */ + if (h_cid <= 0) { + setl(b, T_RC, 6615); + Bchg(b, T_MSG, 0, "대상 마감ID 필수", 0L); FAIL(b); + } + EXEC SQL SELECT status INTO :h_status FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 6616); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6617); FAIL(b); } + if (h_status[0] != 'O') { + setl(b, T_RC, 6618); + Bchg(b, T_MSG, 0, "개시(O) 상태만 지연등록 가능", 0L); FAIL(b); + } + { char rsn[64]; + gets_(b, T_REASON, rsn, sizeof(rsn)); + if (rsn[0] == 0) strcpy(rsn, "사유미기재"); + snprintf(h_rsn, sizeof(h_rsn), "DELAY:%s", rsn); + EXEC SQL UPDATE cl_close_log SET memo = :h_rsn, updated_at = now() + WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6619); FAIL(b); } + metric = sqlca.sqlerrd[2]; } + o_count = metric; o_rc = 0; + strcpy(o_msg, "지연 사유 등록 완료"); + + /* --- 섹션3b: 규모 부가판정 --- */ + if (metric < 0) metric = 0; + setl(b, T_ARG5, metric); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_DELAYREG 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1000000000L) { + z_sev = 3; + strcpy(z_msg, "대규모 마감기간"); + } else if (h_zamt >= 100000000L) { + z_sev = 2; + strcpy(z_msg, "중규모 마감기간"); + } else { + z_sev = 1; + strcpy(z_msg, "소규모 마감기간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_DELAYREG 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_DELAYREG 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_DELAYREG 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_DELAYREG 마감 지연 사유 등록: 개시(O) 상태의 마감건에만 지연 사유를 memo 로 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_DELAYREG 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_DELAYREG 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_DIFFCHK.pgc b/app/src/cl/svc/CL_DIFFCHK.pgc index 6443342..76dbbc7 100644 --- a/app/src/cl/svc/CL_DIFFCHK.pgc +++ b/app/src/cl/svc/CL_DIFFCHK.pgc @@ -9,17 +9,147 @@ void CL_DIFFCHK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long mask = 0; + long blockers = 0; + int over = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid, h_total, h_snap; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_n1, h_n2, h_n3, h_n4; + long h_lim; + char h_mer[16]; + char h_status[4]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; - h_cid = getl(b, T_ID1); - EXEC SQL SELECT total_amount INTO :h_total FROM cl_close_log WHERE close_id = :h_cid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :h_snap - FROM cl_snapshot WHERE close_id = :h_cid; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_DELTA, h_total - h_snap); - setl(b, T_RC, (h_total == h_snap) ? 0 : 1); + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_DIFFCHK"); + userlog("CL_DIFFCHK 진입: 마감처리 시작 (총액 차이 점검)", svc); + + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0) { + setl(b, T_RC, 5310); + Bchg(b, T_MSG, 0, "영업일 미입력 - 총액 차이 점검 불가", 0L); + FAIL(b); + } + if (strlen(h_bizdate) < 8) { + setl(b, T_RC, 5410); + Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); FAIL(b); + } + + /* --- 섹션2: 기준/정책 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_n4 FROM cl_close_log + WHERE close_type = 'DAILY' AND period_key = :h_bizdate AND status <> 'X'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5311); FAIL(b); } + + /* --- 섹션3·섹션4: close_log total vs 스냅샷 gross 차이 --- */ + { long cid = getl(b, T_ID1); + h_kcid = cid; + EXEC SQL SELECT total_amount INTO :h_n1 FROM cl_close_log WHERE close_id = :h_kcid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5312); FAIL(b); } + h_kcid = cid; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :h_n2 + FROM cl_snapshot WHERE close_id = :h_kcid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5313); FAIL(b); } + h_n3 = h_n1 - h_n2; if (h_n3 < 0) h_n3 = -h_n3; + o_amount = h_n3; o_rc = (h_n3 == 0) ? 0 : 1; } + strcpy(o_msg, o_rc ? "총액-스냅샷 차이 발생" : "총액-스냅샷 일치"); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_DIFFCHK 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 2000000000L) { + z_sev = 3; + strcpy(z_msg, "초과대형 마감구간"); + } else if (h_zamt >= 200000000L) { + z_sev = 2; + strcpy(z_msg, "중대형 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "일반 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_DIFFCHK 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_DIFFCHK 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_DIFFCHK 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_DIFFCHK 총액 차이점검 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_DIFFCHK 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_DIFFCHK 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_DIVCLOSE.pgc b/app/src/cl/svc/CL_DIVCLOSE.pgc index 2acb3d9..de7bc24 100644 --- a/app/src/cl/svc/CL_DIVCLOSE.pgc +++ b/app/src/cl/svc/CL_DIVCLOSE.pgc @@ -9,27 +9,212 @@ void CL_DIVCLOSE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long cid = 0; + long fee_calc = 0; + int grade = 0; + char memo[128]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid, h_cnt, h_net; - char h_bizdate[16], h_cat[16], h_memo[48]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cnt, h_amt, h_fee, h_net; + long h_prev, h_bps; + char h_key[32]; + char h_status[4]; + char h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_DIVCLOSE"); + userlog("CL_DIVCLOSE 진입: 마감처리 시작 (부문(업종) 마감)", svc); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - gets_(b, T_CATEGORY, h_cat, sizeof(h_cat)); - if (h_cat[0] == 0) strcpy(h_cat, "GEN"); - EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_cnt, :h_net - FROM purchase - WHERE biz_date = :h_bizdate AND category = :h_cat AND status <> 'C'; - if (sqlca.sqlcode < 0) FAIL(b); - snprintf(h_memo, sizeof(h_memo), "부문마감:%s", h_cat); - EXEC SQL SELECT nextval('cl_close_seq') INTO :h_cid; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL INSERT INTO cl_close_log - (close_id, close_type, period_key, biz_date, status, txn_count, total_amount, memo) - VALUES (:h_cid, 'DIV', :h_bizdate, :h_bizdate, 'C', :h_cnt, :h_net, :h_memo); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_ID1, h_cid); - setl(b, T_COUNT, h_cnt); + gets_(b, T_STR1, h_period, sizeof(h_period)); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + gets_(b, T_CATEGORY, h_key, sizeof(h_key)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0) { + setl(b, T_RC, 5001); + Bchg(b, T_MSG, 0, "영업일/기간 미입력 - 부문(업종) 마감 불가", 0L); + FAIL(b); + } + if (strlen(h_bizdate) > 0 && strlen(h_bizdate) < 8) { + setl(b, T_RC, 5002); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + FAIL(b); + } + if (h_key[0] == 0) { + setl(b, T_RC, 5003); + Bchg(b, T_MSG, 0, "부문(업종) 구분값 미입력", 0L); + FAIL(b); + } + + /* --- 섹션2: 정책/기존마감 상태 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_prev FROM cl_close_log + WHERE close_type = 'DIV' AND period_key = :h_period AND status <> 'X'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5010); + Bchg(b, T_MSG, 0, "기존 마감 조회 실패", 0L); FAIL(b); + } + if (h_prev > 0) { + userlog("CL_DIVCLOSE %s 이미 마감 %ld건 존재 - 재집계 진행", h_period, h_prev); + } + EXEC SQL SELECT count(*) INTO :h_bps FROM cl_period_lock + WHERE period_key = :h_period AND locked = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5011); FAIL(b); } + if (h_bps > 0) { + setl(b, T_RC, 5012); + Bchg(b, T_MSG, 0, "기간이 잠금상태 - 마감 불가", 0L); FAIL(b); + } + + /* --- 섹션4: 본처리부(집계) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) + INTO :h_cnt, :h_amt, :h_fee, :h_net + FROM purchase WHERE biz_date = :h_bizdate AND category = :h_key; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5020); + Bchg(b, T_MSG, 0, "매입 집계 실패", 0L); FAIL(b); + } + + /* --- 섹션3: 업무규칙 분기부(규모/등급) --- */ + if (h_cnt == 0) { + grade = 0; + strcpy(memo, "DIV:무거래"); + } else if (h_amt >= 1000000000) { + grade = 3; + strcpy(memo, "DIV:대규모"); + } else if (h_amt >= 100000000) { + grade = 2; + strcpy(memo, "DIV:중규모"); + } else { + grade = 1; + strcpy(memo, "DIV:소규모"); + } + fee_calc = acq_fee(h_amt, 25); + if (h_fee == 0 && h_amt > 0) h_fee = fee_calc; + + /* --- 섹션4b: 마감이력 생성 --- */ + cid = cldb_next_close_id(); + if (cid < 0) { setl(b, T_RC, 5030); FAIL(b); } + { char m2[128]; snprintf(m2, sizeof(m2), "%s|%s", memo, h_key); + strncpy(memo, m2, sizeof(memo)-1); memo[sizeof(memo)-1]=0; } + if (cldb_insert_close(cid, "DIV", h_period, + (h_bizdate[0] ? h_bizdate : h_period), + "C", h_cnt, h_amt, memo) < 0) { + setl(b, T_RC, 5031); + Bchg(b, T_MSG, 0, "마감이력 생성 실패", 0L); FAIL(b); + } + + /* --- 섹션6: 집계 확인/요약 재조회부 --- */ + h_kcid = cid; + EXEC SQL SELECT txn_count, total_amount INTO :h_prev, :h_amt + FROM cl_close_log WHERE close_id = :h_kcid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5040); FAIL(b); } + o_count = h_cnt; o_amount = h_amt; + + /* --- 섹션8prep: 상태/결과 세팅 --- */ + setl(b, T_ID1, cid); + setl(b, T_FEE, h_fee); setl(b, T_NET, h_net); + Bchg(b, T_STATUS, 0, "C", 0L); + snprintf(o_msg, sizeof(o_msg), "부문(업종) 마감완료 등급%d cid=%ld", grade, cid); + o_rc = 0; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_DIVCLOSE 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 2000000000L) { + z_sev = 3; + strcpy(z_msg, "초과대형 마감구간"); + } else if (h_zamt >= 200000000L) { + z_sev = 2; + strcpy(z_msg, "중대형 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "일반 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_DIVCLOSE 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_DIVCLOSE 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_DIVCLOSE 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_DIVCLOSE 부문(업종)마감 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_DIVCLOSE 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_DIVCLOSE 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_DUELIST.pgc b/app/src/cl/svc/CL_DUELIST.pgc index 080965e..e0e2310 100644 --- a/app/src/cl/svc/CL_DUELIST.pgc +++ b/app/src/cl/svc/CL_DUELIST.pgc @@ -8,33 +8,149 @@ void CL_DUELIST(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long occ = 0; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long metric = 0; int i = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16], h_day[16]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_n1, h_n2, h_n3, h_cid; + char h_key[32], h_status[4], h_out[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_DUELIST"); + userlog("CL_DUELIST 진입: 마감처리 시작 (마감 예정 목록: 기준일 이전 14일 내 영업일 중 DAILY 마감이 없는 미마감 대상일을 나열.)", svc); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL DECLARE crdue CURSOR FOR - SELECT to_char(d,'YYYY-MM-DD') FROM ( - SELECT (:h_bizdate::date - s) AS d, - extract(dow from (:h_bizdate::date - s)) AS dw - FROM generate_series(0,13) s) t - WHERE dw NOT IN (0,6) - AND NOT EXISTS (SELECT 1 FROM cl_close_log - WHERE close_type = 'DAILY' - AND period_key = to_char(t.d,'YYYY-MM-DD') AND status <> 'X') - ORDER BY d DESC; - EXEC SQL OPEN crdue; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH crdue INTO :h_day; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE crdue; FAIL(b); } - Bchg(b, T_KEY1, (BFLDOCC)occ, h_day, 0L); - occ++; + gets_(b, T_STR1, h_period, sizeof(h_period)); + gets_(b, T_KEY1, h_key, sizeof(h_key)); + h_cid = getl(b, T_ID1); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0 && h_cid <= 0) { + setl(b, T_RC, 6601); + Bchg(b, T_MSG, 0, "기준(영업일/기간/마감ID) 중 하나는 필수", 0L); FAIL(b); } - EXEC SQL CLOSE crdue; - setl(b, T_COUNT, occ); - setl(b, T_RC, (occ > 0) ? 1 : 0); + + /* --- 섹션2·섹션4: 기간내 미마감/누락 영업일 산출 --- */ + if (h_period[0] == 0) { + setl(b, T_RC, 6612); + Bchg(b, T_MSG, 0, "대상 기간(YYYY-MM 등) 필수", 0L); FAIL(b); + } + EXEC SQL SELECT count(DISTINCT p.biz_date) INTO :h_n1 + FROM purchase p + WHERE to_char(p.biz_date,'YYYY-MM') = substr(:h_period,1,7) + AND NOT EXISTS (SELECT 1 FROM cl_close_log c + WHERE c.close_type = 'DAILY' + AND c.period_key = to_char(p.biz_date,'YYYY-MM-DD') + AND c.status <> 'X'); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6613); FAIL(b); } + EXEC SQL SELECT count(DISTINCT biz_date) INTO :h_n2 FROM purchase + WHERE to_char(biz_date,'YYYY-MM') = substr(:h_period,1,7); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6614); FAIL(b); } + metric = h_n1; o_count = h_n1; o_amount = h_n2; + if (h_n1 == 0) { o_rc = 0; strcpy(o_msg, "미마감 영업일 없음"); } + else if (h_n1 * 2 > h_n2) { o_rc = 2; strcpy(o_msg, "미마감 비중 과다"); } + else { o_rc = 1; strcpy(o_msg, "일부 미마감 잔존"); } + + /* --- 섹션3b: 규모 부가판정 --- */ + if (metric < 0) metric = 0; + setl(b, T_ARG5, metric); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_DUELIST 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 500000000L) { + z_sev = 3; + strcpy(z_msg, "고액 마감구간"); + } else if (h_zamt >= 50000000L) { + z_sev = 2; + strcpy(z_msg, "중액 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "소액 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_DUELIST 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_DUELIST 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_DUELIST 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_DUELIST 마감 예정 목록: 기준일 이전 14일 내 영업일 중 DAILY 마감이 없는 미마감 대상일을 나열. cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_DUELIST 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_DUELIST 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_DUPCHK.pgc b/app/src/cl/svc/CL_DUPCHK.pgc index b04579c..248acbe 100644 --- a/app/src/cl/svc/CL_DUPCHK.pgc +++ b/app/src/cl/svc/CL_DUPCHK.pgc @@ -9,34 +9,154 @@ void CL_DUPCHK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long groups = 0, extras = 0; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long mask = 0; + long blockers = 0; + int over = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_n; - char h_type[16], h_period[24]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_n1, h_n2, h_n3, h_n4; + long h_lim; + char h_mer[16]; + char h_status[4]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; + char h_type[16], h_pk[24]; EXEC SQL END DECLARE SECTION; - EXEC SQL DECLARE cdup CURSOR FOR - SELECT close_type, period_key, count(*) FROM cl_close_log - WHERE status <> 'X' - GROUP BY close_type, period_key HAVING count(*) > 1 - ORDER BY count(*) DESC; - EXEC SQL OPEN cdup; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH cdup INTO :h_type, :h_period, :h_n; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cdup; FAIL(b); } - if (groups < 20) { - Bchg(b, T_STR2, (BFLDOCC)groups, h_type, 0L); - Bchg(b, T_KEY2, (BFLDOCC)groups, h_period, 0L); - } - extras += h_n - 1; - groups++; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_DUPCHK"); + userlog("CL_DUPCHK 진입: 마감처리 시작 (중복 마감 점검)", svc); + + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0) { + setl(b, T_RC, 5313); + Bchg(b, T_MSG, 0, "영업일 미입력 - 중복 마감 점검 불가", 0L); + FAIL(b); } - EXEC SQL CLOSE cdup; - setl(b, T_COUNT, groups); - setl(b, T_ARG1, extras); - setl(b, T_RC, (groups == 0) ? 0 : 1); - if (groups) userlog("CL_DUPCHK 중복그룹 %ld개 잉여 %ld건", groups, extras); + if (strlen(h_bizdate) < 8) { + setl(b, T_RC, 5413); + Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); FAIL(b); + } + + /* --- 섹션2: 기준/정책 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_n4 FROM cl_close_log + WHERE close_type = 'DAILY' AND period_key = :h_bizdate AND status <> 'X'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5314); FAIL(b); } + + /* --- 섹션3·섹션4: 중복 마감 그룹 커서 탐지 --- */ + { long groups = 0, extras = 0; + EXEC SQL DECLARE cdupc CURSOR FOR + SELECT close_type, period_key, count(*) FROM cl_close_log + WHERE status <> 'X' GROUP BY close_type, period_key + HAVING count(*) > 1; + EXEC SQL OPEN cdupc; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5315); FAIL(b); } + for (;;) { + EXEC SQL FETCH cdupc INTO :h_type, :h_pk, :h_n1; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cdupc; setl(b, T_RC, 5316); FAIL(b); } + groups++; extras += h_n1 - 1; + } + EXEC SQL CLOSE cdupc; + o_count = groups; o_amount = extras; + o_rc = (groups == 0) ? 0 : 1; } + strcpy(o_msg, o_rc ? "중복 마감 그룹 존재" : "중복 없음"); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_DUPCHK 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 500000000L) { + z_sev = 3; + strcpy(z_msg, "고액 마감구간"); + } else if (h_zamt >= 50000000L) { + z_sev = 2; + strcpy(z_msg, "중액 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "소액 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_DUPCHK 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_DUPCHK 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_DUPCHK 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_DUPCHK 중복 마감점검 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_DUPCHK 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_DUPCHK 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_ESCALATE.pgc b/app/src/cl/svc/CL_ESCALATE.pgc index 0c2095b..70f4ad3 100644 --- a/app/src/cl/svc/CL_ESCALATE.pgc +++ b/app/src/cl/svc/CL_ESCALATE.pgc @@ -9,26 +9,161 @@ void CL_ESCALATE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long marked; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long groups = 0; long topamt = 0; char topdim[64]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_days, h_left; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_grpcnt, h_grpamt, h_cid; + char h_dim[64], h_key[32]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; - h_days = getl(b, T_ARG1); - if (h_days <= 0) h_days = 3; - EXEC SQL UPDATE cl_close_log - SET memo = memo || '|ESC', updated_at = now() - WHERE status = 'C' AND memo LIKE '%APPR_REQ%' - AND memo NOT LIKE '%|ESC%' - AND created_at < now() - make_interval(days => cast(:h_days as int)); - if (sqlca.sqlcode < 0) FAIL(b); - marked = sqlca.sqlerrd[2]; - EXEC SQL SELECT count(*) INTO :h_left FROM cl_close_log - WHERE status = 'C' AND memo LIKE '%APPR_REQ%' AND memo NOT LIKE '%|ESC%'; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, marked); - setl(b, T_ARG2, h_left); - setl(b, T_RC, (marked > 0) ? 1 : 0); - if (marked) userlog("CL_ESCALATE %ld일 초과 %ld건 에스컬레이션", h_days, marked); + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_ESCALATE"); + userlog("CL_ESCALATE 진입: 마감처리 시작 (에스컬레이션)", svc); + + + /* --- 섹션1: 입력 검증부 --- */ + /* 전역 리포트 - 별도 필수입력 없음 */ + + /* --- 섹션2: 그룹 수 사전 확인부 --- */ + EXEC SQL SELECT count(DISTINCT period_key) INTO :h_grpcnt + FROM cl_close_log WHERE status = 'C'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6504); FAIL(b); } + if (h_grpcnt == 0) { + o_rc = 1; o_count = 0; + strcpy(o_msg, "리포트 대상 그룹 없음"); + setl(b, T_COUNT, 0); Bchg(b, T_MSG, 0, o_msg, 0L); OK(b); + } + + /* --- 섹션4: 그룹별 집계 커서(상위추출) --- */ + strcpy(topdim, ""); + EXEC SQL DECLARE crpt CURSOR FOR + SELECT period_key, count(*), coalesce(sum(total_amount),0) FROM cl_close_log + WHERE status = 'C' GROUP BY period_key ORDER BY 2 DESC; + EXEC SQL OPEN crpt; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6505); FAIL(b); } + for (;;) { + EXEC SQL FETCH crpt INTO :h_dim, :h_grpcnt, :h_grpamt; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE crpt; setl(b, T_RC, 6506); FAIL(b); } + if (groups < 20) { + Bchg(b, T_STR2, (BFLDOCC)groups, h_dim, 0L); + setl(b, T_ARG3, h_grpamt); + } + if (h_grpamt > topamt) { + topamt = h_grpamt; + strncpy(topdim, h_dim, sizeof(topdim)-1); topdim[sizeof(topdim)-1]=0; + } + groups++; + } + EXEC SQL CLOSE crpt; + + /* --- 섹션3: 상위그룹 판정 분기부 --- */ + if (groups == 0) { + o_rc = 1; strcpy(o_msg, "에스컬레이션 집계 결과 없음"); + } else if (topamt == 0) { + o_rc = 2; strcpy(o_msg, "에스컬레이션 집계 금액 0"); + } else { + o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "에스컬레이션 최다=%s(%ld)", topdim, topamt); + } + Bchg(b, T_STR1, 0, topdim, 0L); + o_count = groups; o_amount = topamt; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_ESCALATE 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1000000000L) { + z_sev = 3; + strcpy(z_msg, "대규모 마감기간"); + } else if (h_zamt >= 100000000L) { + z_sev = 2; + strcpy(z_msg, "중규모 마감기간"); + } else { + z_sev = 1; + strcpy(z_msg, "소규모 마감기간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_ESCALATE 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_ESCALATE 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_ESCALATE 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_ESCALATE 에스컬레이션 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_ESCALATE 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_ESCALATE 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_FEECROSS.pgc b/app/src/cl/svc/CL_FEECROSS.pgc index 1060014..79e6a21 100644 --- a/app/src/cl/svc/CL_FEECROSS.pgc +++ b/app/src/cl/svc/CL_FEECROSS.pgc @@ -8,26 +8,159 @@ void CL_FEECROSS(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long delta; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long d1 = 0, d2 = 0, worst = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_hdr, h_real, h_snap; char h_date[16]; - long h_cid, h_sf, h_pf; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_FEECROSS"); + userlog("CL_FEECROSS 진입: 마감처리 시작 (수수료 크로스검증)", svc); + h_cid = getl(b, T_ID1); - EXEC SQL SELECT to_char(biz_date,'YYYY-MM-DD') INTO :h_date - FROM cl_close_log WHERE close_id = :h_cid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - EXEC SQL SELECT coalesce(sum(fee_amount),0) INTO :h_sf - FROM cl_snapshot WHERE close_id = :h_cid; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT coalesce(sum(fee),0) INTO :h_pf + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0) { + setl(b, T_RC, 5501); + Bchg(b, T_MSG, 0, "마감ID(T_ID1) 필수", 0L); FAIL(b); + } + + /* --- 섹션2: 마감이력 헤더 조회부 --- */ + EXEC SQL SELECT total_amount, to_char(biz_date,'YYYY-MM-DD') + INTO :h_hdr, :h_date FROM cl_close_log + WHERE close_id = :h_cid AND status <> 'X'; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5502); + Bchg(b, T_MSG, 0, "유효 마감이력 없음(취소/미존재)", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5503); FAIL(b); } + + /* --- 섹션4: 실매입/스냅샷 집계부 --- */ + EXEC SQL SELECT coalesce(sum(fee),0) INTO :h_real FROM purchase WHERE biz_date = :h_date; - if (sqlca.sqlcode < 0) FAIL(b); - delta = h_sf - h_pf; if (delta < 0) delta = -delta; - setl(b, T_AMT1, h_sf); - setl(b, T_AMT2, h_pf); - setl(b, T_DELTA, delta); - setl(b, T_RC, (delta == 0) ? 0 : 1); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5504); FAIL(b); } + EXEC SQL SELECT coalesce(sum(fee_amount),0) INTO :h_snap + FROM cl_snapshot WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5505); FAIL(b); } + + /* --- 섹션3: 3원 대사 분기부 --- */ + d1 = h_hdr - h_real; if (d1 < 0) d1 = -d1; + d2 = (h_snap == 0) ? 0 : (h_hdr - h_snap); if (d2 < 0) d2 = -d2; + worst = (d1 > d2) ? d1 : d2; + if (worst == 0) { + o_rc = 0; strcpy(o_msg, "헤더=매입=스냅샷 3원 일치"); + } else if (d1 != 0 && d2 == 0) { + o_rc = 1; strcpy(o_msg, "헤더-매입 불일치(스냅샷은 일치)"); + } else if (d1 == 0 && d2 != 0) { + o_rc = 2; strcpy(o_msg, "헤더-스냅샷 불일치(매입은 일치)"); + } else { + o_rc = 3; strcpy(o_msg, "3원 전부 불일치 - 재마감 검토"); + } + + /* --- 섹션6: 편차 세팅 --- */ + setl(b, T_AMT1, h_hdr); + setl(b, T_AMT2, h_real); + setl(b, T_AMT3, h_snap); + setl(b, T_DELTA, worst); + o_count = (worst == 0) ? 0 : 1; o_amount = worst; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_FEECROSS 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 500000000L) { + z_sev = 3; + strcpy(z_msg, "고액 마감구간"); + } else if (h_zamt >= 50000000L) { + z_sev = 2; + strcpy(z_msg, "중액 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "소액 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_FEECROSS 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_FEECROSS 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_FEECROSS 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_FEECROSS 수수료크로스 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_FEECROSS 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_FEECROSS 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_FEEVERIFY.pgc b/app/src/cl/svc/CL_FEEVERIFY.pgc index 12e2f3e..cb141aa 100644 --- a/app/src/cl/svc/CL_FEEVERIFY.pgc +++ b/app/src/cl/svc/CL_FEEVERIFY.pgc @@ -9,30 +9,168 @@ void CL_FEEVERIFY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long expect; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long dcnt = 0, damt = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid, h_snapfee, h_purfee, h_adj; - char h_date[16]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_hdr_cnt, h_hdr_amt; + long h_real_cnt, h_real_amt; + char h_date[16], h_type[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_FEEVERIFY"); + userlog("CL_FEEVERIFY 진입: 마감처리 시작 (마감 정합 검증)", svc); + h_cid = getl(b, T_ID1); - EXEC SQL SELECT to_char(biz_date,'YYYY-MM-DD') INTO :h_date - FROM cl_close_log WHERE close_id = :h_cid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - EXEC SQL SELECT coalesce(sum(fee_amount),0) INTO :h_snapfee - FROM cl_snapshot WHERE close_id = :h_cid; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT coalesce(sum(fee),0) INTO :h_purfee + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0 && h_bizdate[0] == 0) { + setl(b, T_RC, 5401); + Bchg(b, T_MSG, 0, "마감ID/영업일 중 하나는 필수", 0L); FAIL(b); + } + + /* --- 섹션2: 마감이력 헤더 조회부 --- */ + if (h_cid > 0) { + EXEC SQL SELECT close_type, txn_count, total_amount, + to_char(biz_date,'YYYY-MM-DD') + INTO :h_type, :h_hdr_cnt, :h_hdr_amt, :h_date + FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5402); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5403); FAIL(b); } + } else { + strcpy(h_date, h_bizdate); + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(total_amount),0) + INTO :h_hdr_cnt, :h_hdr_amt FROM cl_close_log + WHERE period_key = :h_bizdate AND status <> 'X'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5404); FAIL(b); } + } + + /* --- 섹션4: 실매입 재집계부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_real_cnt, :h_real_amt FROM purchase WHERE biz_date = :h_date; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT coalesce(sum(delta),0) INTO :h_adj - FROM ac_fee_adj WHERE biz_date = :h_date; - if (sqlca.sqlcode < 0) FAIL(b); - expect = h_purfee + h_adj; - setl(b, T_AMT1, h_snapfee); - setl(b, T_AMT2, h_purfee); - setl(b, T_AMT3, h_adj); - setl(b, T_DELTA, h_snapfee - expect); - setl(b, T_RC, (h_snapfee == expect) ? 0 : 1); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5405); FAIL(b); } + + /* --- 섹션3: 대사 분기부 --- */ + dcnt = h_hdr_cnt - h_real_cnt; if (dcnt < 0) dcnt = -dcnt; + damt = h_hdr_amt - h_real_amt; if (damt < 0) damt = -damt; + if (dcnt == 0 && damt == 0) { + o_rc = 0; strcpy(o_msg, "건수·금액 완전 일치"); + } else if (dcnt == 0 && damt != 0) { + o_rc = 1; strcpy(o_msg, "건수 일치·금액 불일치"); + } else if (dcnt != 0 && damt == 0) { + o_rc = 2; strcpy(o_msg, "건수 불일치·금액 일치"); + } else { + o_rc = 3; strcpy(o_msg, "건수·금액 모두 불일치"); + } + + /* --- 섹션6: 결과 세팅 --- */ + setl(b, T_ARG1, h_hdr_cnt); + setl(b, T_ARG2, h_real_cnt); + setl(b, T_AMT1, h_hdr_amt); + setl(b, T_AMT2, h_real_amt); + setl(b, T_DELTA, damt); + o_count = dcnt; o_amount = damt; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_FEEVERIFY 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1500000000L) { + z_sev = 3; + strcpy(z_msg, "광역 마감규모"); + } else if (h_zamt >= 150000000L) { + z_sev = 2; + strcpy(z_msg, "표준 마감규모"); + } else { + z_sev = 1; + strcpy(z_msg, "경량 마감규모"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_FEEVERIFY 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_FEEVERIFY 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_FEEVERIFY 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_FEEVERIFY 정합검증 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_FEEVERIFY 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_FEEVERIFY 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_FINALIZE.pgc b/app/src/cl/svc/CL_FINALIZE.pgc index edb4cb1..e2743c0 100644 --- a/app/src/cl/svc/CL_FINALIZE.pgc +++ b/app/src/cl/svc/CL_FINALIZE.pgc @@ -9,30 +9,205 @@ void CL_FINALIZE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long cid = 0; + long fee_calc = 0; + int grade = 0; + char memo[128]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid, h_oldcnt, h_cnt, h_amt; char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cnt, h_amt, h_fee, h_net; + long h_prev, h_bps; + char h_key[32]; + char h_status[4]; + char h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_FINALIZE"); + userlog("CL_FINALIZE 진입: 마감처리 시작 (본마감(확정) 마감)", svc); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT close_id, txn_count INTO :h_cid, :h_oldcnt FROM cl_close_log - WHERE close_type = 'DAILY' AND period_key = :h_bizdate - AND status = 'O' AND memo = 'PROV' - ORDER BY close_id DESC LIMIT 1; - if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "확정할 가마감 없음", 0L); FAIL(b); } - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_amt - FROM purchase WHERE biz_date = :h_bizdate AND status IN ('M','S'); - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL UPDATE cl_close_log - SET status = 'C', txn_count = :h_cnt, total_amount = :h_amt, - memo = 'FINAL', updated_at = now() - WHERE close_id = :h_cid; - if (sqlca.sqlcode < 0 || sqlca.sqlerrd[2] == 0) FAIL(b); - setl(b, T_ID1, h_cid); - setl(b, T_COUNT, h_cnt); - setl(b, T_DELTA, h_cnt - h_oldcnt); + gets_(b, T_STR1, h_period, sizeof(h_period)); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + strcpy(h_key, ""); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0) { + setl(b, T_RC, 5001); + Bchg(b, T_MSG, 0, "영업일/기간 미입력 - 본마감(확정) 마감 불가", 0L); + FAIL(b); + } + if (strlen(h_bizdate) > 0 && strlen(h_bizdate) < 8) { + setl(b, T_RC, 5002); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + FAIL(b); + } + + /* --- 섹션2: 정책/기존마감 상태 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_prev FROM cl_close_log + WHERE close_type = 'DAILY' AND period_key = :h_period AND status <> 'X'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5010); + Bchg(b, T_MSG, 0, "기존 마감 조회 실패", 0L); FAIL(b); + } + if (h_prev > 0) { + userlog("CL_FINALIZE %s 이미 마감 %ld건 존재 - 재집계 진행", h_period, h_prev); + } + EXEC SQL SELECT count(*) INTO :h_bps FROM cl_period_lock + WHERE period_key = :h_period AND locked = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5011); FAIL(b); } + if (h_bps > 0) { + setl(b, T_RC, 5012); + Bchg(b, T_MSG, 0, "기간이 잠금상태 - 마감 불가", 0L); FAIL(b); + } + + /* --- 섹션4: 본처리부(집계) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) + INTO :h_cnt, :h_amt, :h_fee, :h_net + FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5020); + Bchg(b, T_MSG, 0, "매입 집계 실패", 0L); FAIL(b); + } + + /* --- 섹션3: 업무규칙 분기부(규모/등급) --- */ + if (h_cnt == 0) { + grade = 0; + strcpy(memo, "DAILY:무거래"); + } else if (h_amt >= 1000000000) { + grade = 3; + strcpy(memo, "DAILY:대규모"); + } else if (h_amt >= 100000000) { + grade = 2; + strcpy(memo, "DAILY:중규모"); + } else { + grade = 1; + strcpy(memo, "DAILY:소규모"); + } + fee_calc = acq_fee(h_amt, 25); + if (h_fee == 0 && h_amt > 0) h_fee = fee_calc; + + /* --- 섹션4b: 마감이력 생성 --- */ + cid = cldb_next_close_id(); + if (cid < 0) { setl(b, T_RC, 5030); FAIL(b); } + if (cldb_insert_close(cid, "DAILY", h_period, + (h_bizdate[0] ? h_bizdate : h_period), + "C", h_cnt, h_amt, memo) < 0) { + setl(b, T_RC, 5031); + Bchg(b, T_MSG, 0, "마감이력 생성 실패", 0L); FAIL(b); + } + + /* --- 섹션6: 집계 확인/요약 재조회부 --- */ + h_kcid = cid; + EXEC SQL SELECT txn_count, total_amount INTO :h_prev, :h_amt + FROM cl_close_log WHERE close_id = :h_kcid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5040); FAIL(b); } + o_count = h_cnt; o_amount = h_amt; + + /* --- 섹션8prep: 상태/결과 세팅 --- */ + setl(b, T_ID1, cid); + setl(b, T_FEE, h_fee); + setl(b, T_NET, h_net); Bchg(b, T_STATUS, 0, "C", 0L); - userlog("CL_FINALIZE cid=%ld 본마감 확정 (가마감 대비 %+ld건)", h_cid, h_cnt - h_oldcnt); + snprintf(o_msg, sizeof(o_msg), "본마감(확정) 마감완료 등급%d cid=%ld", grade, cid); + o_rc = 0; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_FINALIZE 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 2000000000L) { + z_sev = 3; + strcpy(z_msg, "초과대형 마감구간"); + } else if (h_zamt >= 200000000L) { + z_sev = 2; + strcpy(z_msg, "중대형 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "일반 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_FINALIZE 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_FINALIZE 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_FINALIZE 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_FINALIZE 본마감(확정)마감 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_FINALIZE 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_FINALIZE 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_FORCECLOSE.pgc b/app/src/cl/svc/CL_FORCECLOSE.pgc index 9f2def4..f5560dd 100644 --- a/app/src/cl/svc/CL_FORCECLOSE.pgc +++ b/app/src/cl/svc/CL_FORCECLOSE.pgc @@ -9,28 +9,205 @@ void CL_FORCECLOSE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long cid, wiped; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long cid = 0; + long fee_calc = 0; + int grade = 0; + char memo[128]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_amt; char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cnt, h_amt, h_fee, h_net; + long h_prev, h_bps; + char h_key[32]; + char h_status[4]; + char h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_FORCECLOSE"); + userlog("CL_FORCECLOSE 진입: 마감처리 시작 (강제마감 마감)", svc); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL DELETE FROM cl_close_log - WHERE close_type = 'DAILY' AND period_key = :h_bizdate AND status = 'O'; - if (sqlca.sqlcode < 0) FAIL(b); - wiped = sqlca.sqlerrd[2]; - EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_amt + gets_(b, T_STR1, h_period, sizeof(h_period)); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + strcpy(h_key, ""); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0) { + setl(b, T_RC, 5001); + Bchg(b, T_MSG, 0, "영업일/기간 미입력 - 강제마감 마감 불가", 0L); + FAIL(b); + } + if (strlen(h_bizdate) > 0 && strlen(h_bizdate) < 8) { + setl(b, T_RC, 5002); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + FAIL(b); + } + + /* --- 섹션2: 정책/기존마감 상태 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_prev FROM cl_close_log + WHERE close_type = 'DAILY' AND period_key = :h_period AND status <> 'X'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5010); + Bchg(b, T_MSG, 0, "기존 마감 조회 실패", 0L); FAIL(b); + } + if (h_prev > 0) { + userlog("CL_FORCECLOSE %s 이미 마감 %ld건 존재 - 재집계 진행", h_period, h_prev); + } + EXEC SQL SELECT count(*) INTO :h_bps FROM cl_period_lock + WHERE period_key = :h_period AND locked = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5011); FAIL(b); } + if (h_bps > 0) { + setl(b, T_RC, 5012); + Bchg(b, T_MSG, 0, "기간이 잠금상태 - 마감 불가", 0L); FAIL(b); + } + + /* --- 섹션4: 본처리부(집계) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) + INTO :h_cnt, :h_amt, :h_fee, :h_net FROM purchase WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5020); + Bchg(b, T_MSG, 0, "매입 집계 실패", 0L); FAIL(b); + } + + /* --- 섹션3: 업무규칙 분기부(규모/등급) --- */ + if (h_cnt == 0) { + grade = 0; + strcpy(memo, "DAILY:무거래"); + } else if (h_amt >= 1000000000) { + grade = 3; + strcpy(memo, "DAILY:대규모"); + } else if (h_amt >= 100000000) { + grade = 2; + strcpy(memo, "DAILY:중규모"); + } else { + grade = 1; + strcpy(memo, "DAILY:소규모"); + } + fee_calc = acq_fee(h_amt, 25); + if (h_fee == 0 && h_amt > 0) h_fee = fee_calc; + + /* --- 섹션4b: 마감이력 생성 --- */ cid = cldb_next_close_id(); - if (cid < 0) FAIL(b); - if (cldb_insert_close(cid, "DAILY", h_bizdate, h_bizdate, "C", - h_cnt, h_amt, "FORCE") < 0) FAIL(b); + if (cid < 0) { setl(b, T_RC, 5030); FAIL(b); } + if (cldb_insert_close(cid, "DAILY", h_period, + (h_bizdate[0] ? h_bizdate : h_period), + "C", h_cnt, h_amt, memo) < 0) { + setl(b, T_RC, 5031); + Bchg(b, T_MSG, 0, "마감이력 생성 실패", 0L); FAIL(b); + } + + /* --- 섹션6: 집계 확인/요약 재조회부 --- */ + h_kcid = cid; + EXEC SQL SELECT txn_count, total_amount INTO :h_prev, :h_amt + FROM cl_close_log WHERE close_id = :h_kcid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5040); FAIL(b); } + o_count = h_cnt; o_amount = h_amt; + + /* --- 섹션8prep: 상태/결과 세팅 --- */ setl(b, T_ID1, cid); - setl(b, T_ARG1, wiped); - setl(b, T_COUNT, h_cnt); + setl(b, T_FEE, h_fee); + setl(b, T_NET, h_net); Bchg(b, T_STATUS, 0, "C", 0L); - userlog("CL_FORCECLOSE %s 강제마감 cid=%ld (개시건 %ld건 제거)", h_bizdate, cid, wiped); + snprintf(o_msg, sizeof(o_msg), "강제마감 마감완료 등급%d cid=%ld", grade, cid); + o_rc = 0; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_FORCECLOSE 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 2000000000L) { + z_sev = 3; + strcpy(z_msg, "초과대형 마감구간"); + } else if (h_zamt >= 200000000L) { + z_sev = 2; + strcpy(z_msg, "중대형 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "일반 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_FORCECLOSE 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_FORCECLOSE 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_FORCECLOSE 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_FORCECLOSE 강제마감마감 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_FORCECLOSE 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_FORCECLOSE 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_FXCLOSE.pgc b/app/src/cl/svc/CL_FXCLOSE.pgc index 593189f..20b71d2 100644 --- a/app/src/cl/svc/CL_FXCLOSE.pgc +++ b/app/src/cl/svc/CL_FXCLOSE.pgc @@ -8,26 +8,205 @@ void CL_FXCLOSE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long cid; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long cid = 0; + long fee_calc = 0; + int grade = 0; + char memo[128]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; - long h_cnt, h_amt, h_krw; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cnt, h_amt, h_fee, h_net; + long h_prev, h_bps; + char h_key[32]; + char h_status[4]; + char h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_FXCLOSE"); + userlog("CL_FXCLOSE 진입: 마감처리 시작 (해외(FOREIGN) 마감)", svc); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_amt + gets_(b, T_STR1, h_period, sizeof(h_period)); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + strcpy(h_key, ""); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0) { + setl(b, T_RC, 5001); + Bchg(b, T_MSG, 0, "영업일/기간 미입력 - 해외(FOREIGN) 마감 불가", 0L); + FAIL(b); + } + if (strlen(h_bizdate) > 0 && strlen(h_bizdate) < 8) { + setl(b, T_RC, 5002); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + FAIL(b); + } + + /* --- 섹션2: 정책/기존마감 상태 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_prev FROM cl_close_log + WHERE close_type = 'FX' AND period_key = :h_period AND status <> 'X'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5010); + Bchg(b, T_MSG, 0, "기존 마감 조회 실패", 0L); FAIL(b); + } + if (h_prev > 0) { + userlog("CL_FXCLOSE %s 이미 마감 %ld건 존재 - 재집계 진행", h_period, h_prev); + } + EXEC SQL SELECT count(*) INTO :h_bps FROM cl_period_lock + WHERE period_key = :h_period AND locked = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5011); FAIL(b); } + if (h_bps > 0) { + setl(b, T_RC, 5012); + Bchg(b, T_MSG, 0, "기간이 잠금상태 - 마감 불가", 0L); FAIL(b); + } + + /* --- 섹션4: 본처리부(집계) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) + INTO :h_cnt, :h_amt, :h_fee, :h_net FROM purchase WHERE biz_date = :h_bizdate AND channel = 'FOREIGN'; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT coalesce(sum(krw_amount),0) INTO :h_krw - FROM ac_fx WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5020); + Bchg(b, T_MSG, 0, "매입 집계 실패", 0L); FAIL(b); + } + + /* --- 섹션3: 업무규칙 분기부(규모/등급) --- */ + if (h_cnt == 0) { + grade = 0; + strcpy(memo, "FX:무거래"); + } else if (h_amt >= 1000000000) { + grade = 3; + strcpy(memo, "FX:대규모"); + } else if (h_amt >= 100000000) { + grade = 2; + strcpy(memo, "FX:중규모"); + } else { + grade = 1; + strcpy(memo, "FX:소규모"); + } + fee_calc = acq_fee(h_amt, 25); + if (h_fee == 0 && h_amt > 0) h_fee = fee_calc; + + /* --- 섹션4b: 마감이력 생성 --- */ cid = cldb_next_close_id(); - if (cid < 0) FAIL(b); - if (cldb_insert_close(cid, "FX", h_bizdate, h_bizdate, "C", h_cnt, h_amt, "해외채널마감") < 0) FAIL(b); + if (cid < 0) { setl(b, T_RC, 5030); FAIL(b); } + if (cldb_insert_close(cid, "FX", h_period, + (h_bizdate[0] ? h_bizdate : h_period), + "C", h_cnt, h_amt, memo) < 0) { + setl(b, T_RC, 5031); + Bchg(b, T_MSG, 0, "마감이력 생성 실패", 0L); FAIL(b); + } + + /* --- 섹션6: 집계 확인/요약 재조회부 --- */ + h_kcid = cid; + EXEC SQL SELECT txn_count, total_amount INTO :h_prev, :h_amt + FROM cl_close_log WHERE close_id = :h_kcid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5040); FAIL(b); } + o_count = h_cnt; o_amount = h_amt; + + /* --- 섹션8prep: 상태/결과 세팅 --- */ setl(b, T_ID1, cid); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_amt); - setl(b, T_AMT1, h_krw); - setl(b, T_RC, (h_amt == h_krw) ? 0 : 1); + setl(b, T_FEE, h_fee); + setl(b, T_NET, h_net); + Bchg(b, T_STATUS, 0, "C", 0L); + snprintf(o_msg, sizeof(o_msg), "해외(FOREIGN) 마감완료 등급%d cid=%ld", grade, cid); + o_rc = 0; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_FXCLOSE 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 500000000L) { + z_sev = 3; + strcpy(z_msg, "고액 마감구간"); + } else if (h_zamt >= 50000000L) { + z_sev = 2; + strcpy(z_msg, "중액 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "소액 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_FXCLOSE 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_FXCLOSE 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_FXCLOSE 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_FXCLOSE 해외(FOREIGN)마감 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_FXCLOSE 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_FXCLOSE 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_GAPDAYS.pgc b/app/src/cl/svc/CL_GAPDAYS.pgc index 9e66898..3a08e63 100644 --- a/app/src/cl/svc/CL_GAPDAYS.pgc +++ b/app/src/cl/svc/CL_GAPDAYS.pgc @@ -9,33 +9,149 @@ void CL_GAPDAYS(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long occ = 0; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long metric = 0; int i = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - char h_month[16], h_day[16]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_n1, h_n2, h_n3, h_cid; + char h_key[32], h_status[4], h_out[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; - gets_(b, T_STR1, h_month, sizeof(h_month)); - if (h_month[0] == 0) { Bchg(b, T_MSG, 0, "월(YYYY-MM) 입력 필요", 0L); FAIL(b); } - EXEC SQL DECLARE cgapd CURSOR FOR - SELECT DISTINCT to_char(p.biz_date,'YYYY-MM-DD') FROM purchase p - WHERE to_char(p.biz_date,'YYYY-MM') = :h_month - AND NOT EXISTS (SELECT 1 FROM cl_close_log c - WHERE c.close_type = 'DAILY' - AND c.period_key = to_char(p.biz_date,'YYYY-MM-DD') - AND c.status <> 'X') - ORDER BY 1; - EXEC SQL OPEN cgapd; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH cgapd INTO :h_day; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cgapd; FAIL(b); } - if (occ >= 31) break; - Bchg(b, T_STR4, (BFLDOCC)occ, h_day, 0L); - occ++; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_GAPDAYS"); + userlog("CL_GAPDAYS 진입: 마감처리 시작 (마감 누락일 탐지: 월 프리픽스 내 매입은 존재하나 일마감이 없는 일자를)", svc); + + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + gets_(b, T_STR1, h_period, sizeof(h_period)); + gets_(b, T_KEY1, h_key, sizeof(h_key)); + h_cid = getl(b, T_ID1); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0 && h_cid <= 0) { + setl(b, T_RC, 6601); + Bchg(b, T_MSG, 0, "기준(영업일/기간/마감ID) 중 하나는 필수", 0L); FAIL(b); } - EXEC SQL CLOSE cgapd; - setl(b, T_COUNT, occ); - setl(b, T_RC, (occ == 0) ? 0 : 1); + + /* --- 섹션2·섹션4: 기간내 미마감/누락 영업일 산출 --- */ + if (h_period[0] == 0) { + setl(b, T_RC, 6612); + Bchg(b, T_MSG, 0, "대상 기간(YYYY-MM 등) 필수", 0L); FAIL(b); + } + EXEC SQL SELECT count(DISTINCT p.biz_date) INTO :h_n1 + FROM purchase p + WHERE to_char(p.biz_date,'YYYY-MM') = substr(:h_period,1,7) + AND NOT EXISTS (SELECT 1 FROM cl_close_log c + WHERE c.close_type = 'DAILY' + AND c.period_key = to_char(p.biz_date,'YYYY-MM-DD') + AND c.status <> 'X'); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6613); FAIL(b); } + EXEC SQL SELECT count(DISTINCT biz_date) INTO :h_n2 FROM purchase + WHERE to_char(biz_date,'YYYY-MM') = substr(:h_period,1,7); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6614); FAIL(b); } + metric = h_n1; o_count = h_n1; o_amount = h_n2; + if (h_n1 == 0) { o_rc = 0; strcpy(o_msg, "미마감 영업일 없음"); } + else if (h_n1 * 2 > h_n2) { o_rc = 2; strcpy(o_msg, "미마감 비중 과다"); } + else { o_rc = 1; strcpy(o_msg, "일부 미마감 잔존"); } + + /* --- 섹션3b: 규모 부가판정 --- */ + if (metric < 0) metric = 0; + setl(b, T_ARG5, metric); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_GAPDAYS 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1500000000L) { + z_sev = 3; + strcpy(z_msg, "광역 마감규모"); + } else if (h_zamt >= 150000000L) { + z_sev = 2; + strcpy(z_msg, "표준 마감규모"); + } else { + z_sev = 1; + strcpy(z_msg, "경량 마감규모"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_GAPDAYS 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_GAPDAYS 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_GAPDAYS 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_GAPDAYS 마감 누락일 탐지: 월 프리픽스 내 매입은 존재하나 일마감이 없는 일자를 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_GAPDAYS 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_GAPDAYS 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_HALFCLOSE.pgc b/app/src/cl/svc/CL_HALFCLOSE.pgc index 0c3eb4b..9c99e3a 100644 --- a/app/src/cl/svc/CL_HALFCLOSE.pgc +++ b/app/src/cl/svc/CL_HALFCLOSE.pgc @@ -9,36 +9,205 @@ void CL_HALFCLOSE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long half, cid; - char year[8], period[16]; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long cid = 0; + long fee_calc = 0; + int grade = 0; + char memo[128]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_amt; - char h_from[16], h_to[16]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cnt, h_amt, h_fee, h_net; + long h_prev, h_bps; + char h_key[32]; + char h_status[4]; + char h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; - gets_(b, T_STR1, year, sizeof(year)); - half = getl(b, T_ARG1); - if (year[0] == 0 || (half != 1 && half != 2)) { - Bchg(b, T_MSG, 0, "연도(YYYY)/반기(1|2) 입력 필요", 0L); + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_HALFCLOSE"); + userlog("CL_HALFCLOSE 진입: 마감처리 시작 (반기 마감)", svc); + + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + gets_(b, T_STR1, h_period, sizeof(h_period)); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + strcpy(h_key, ""); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0) { + setl(b, T_RC, 5001); + Bchg(b, T_MSG, 0, "영업일/기간 미입력 - 반기 마감 불가", 0L); FAIL(b); } - if (half == 1) { - snprintf(h_from, sizeof(h_from), "%s-01-01", year); - snprintf(h_to, sizeof(h_to), "%s-06-30", year); - } else { - snprintf(h_from, sizeof(h_from), "%s-07-01", year); - snprintf(h_to, sizeof(h_to), "%s-12-31", year); + if (strlen(h_bizdate) > 0 && strlen(h_bizdate) < 8) { + setl(b, T_RC, 5002); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + FAIL(b); } - snprintf(period, sizeof(period), "%s-H%ld", year, half); - EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_amt - FROM purchase WHERE biz_date BETWEEN :h_from AND :h_to; - if (sqlca.sqlcode < 0) FAIL(b); + + /* --- 섹션2: 정책/기존마감 상태 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_prev FROM cl_close_log + WHERE close_type = 'HALFYR' AND period_key = :h_period AND status <> 'X'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5010); + Bchg(b, T_MSG, 0, "기존 마감 조회 실패", 0L); FAIL(b); + } + if (h_prev > 0) { + userlog("CL_HALFCLOSE %s 이미 마감 %ld건 존재 - 재집계 진행", h_period, h_prev); + } + EXEC SQL SELECT count(*) INTO :h_bps FROM cl_period_lock + WHERE period_key = :h_period AND locked = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5011); FAIL(b); } + if (h_bps > 0) { + setl(b, T_RC, 5012); + Bchg(b, T_MSG, 0, "기간이 잠금상태 - 마감 불가", 0L); FAIL(b); + } + + /* --- 섹션4: 본처리부(집계) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) + INTO :h_cnt, :h_amt, :h_fee, :h_net + FROM purchase WHERE to_char(biz_date,'YYYY') = :h_period; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5020); + Bchg(b, T_MSG, 0, "매입 집계 실패", 0L); FAIL(b); + } + + /* --- 섹션3: 업무규칙 분기부(규모/등급) --- */ + if (h_cnt == 0) { + grade = 0; + strcpy(memo, "HALFYR:무거래"); + } else if (h_amt >= 1000000000) { + grade = 3; + strcpy(memo, "HALFYR:대규모"); + } else if (h_amt >= 100000000) { + grade = 2; + strcpy(memo, "HALFYR:중규모"); + } else { + grade = 1; + strcpy(memo, "HALFYR:소규모"); + } + fee_calc = acq_fee(h_amt, 25); + if (h_fee == 0 && h_amt > 0) h_fee = fee_calc; + + /* --- 섹션4b: 마감이력 생성 --- */ cid = cldb_next_close_id(); - if (cid < 0) FAIL(b); - if (cldb_insert_close(cid, "HALF", period, h_to, "C", h_cnt, h_amt, "반기결산") < 0) FAIL(b); + if (cid < 0) { setl(b, T_RC, 5030); FAIL(b); } + if (cldb_insert_close(cid, "HALFYR", h_period, + (h_bizdate[0] ? h_bizdate : h_period), + "C", h_cnt, h_amt, memo) < 0) { + setl(b, T_RC, 5031); + Bchg(b, T_MSG, 0, "마감이력 생성 실패", 0L); FAIL(b); + } + + /* --- 섹션6: 집계 확인/요약 재조회부 --- */ + h_kcid = cid; + EXEC SQL SELECT txn_count, total_amount INTO :h_prev, :h_amt + FROM cl_close_log WHERE close_id = :h_kcid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5040); FAIL(b); } + o_count = h_cnt; o_amount = h_amt; + + /* --- 섹션8prep: 상태/결과 세팅 --- */ setl(b, T_ID1, cid); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_amt); - userlog("CL_HALFCLOSE %s 반기결산 cid=%ld cnt=%ld", period, cid, h_cnt); + setl(b, T_FEE, h_fee); + setl(b, T_NET, h_net); + Bchg(b, T_STATUS, 0, "C", 0L); + snprintf(o_msg, sizeof(o_msg), "반기 마감완료 등급%d cid=%ld", grade, cid); + o_rc = 0; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_HALFCLOSE 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1500000000L) { + z_sev = 3; + strcpy(z_msg, "광역 마감규모"); + } else if (h_zamt >= 150000000L) { + z_sev = 2; + strcpy(z_msg, "표준 마감규모"); + } else { + z_sev = 1; + strcpy(z_msg, "경량 마감규모"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_HALFCLOSE 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_HALFCLOSE 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_HALFCLOSE 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_HALFCLOSE 반기마감 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_HALFCLOSE 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_HALFCLOSE 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_HOLICHK.pgc b/app/src/cl/svc/CL_HOLICHK.pgc index 52c3de3..8fa86f4 100644 --- a/app/src/cl/svc/CL_HOLICHK.pgc +++ b/app/src/cl/svc/CL_HOLICHK.pgc @@ -8,18 +8,148 @@ void CL_HOLICHK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - int weekend; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long mask = 0; + long blockers = 0; + int over = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; - long h_dow; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_n1, h_n2, h_n3, h_n4; + long h_lim; + char h_mer[16]; + char h_status[4]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_HOLICHK"); + userlog("CL_HOLICHK 진입: 마감처리 시작 (휴일 마감정책 점검)", svc); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT extract(dow from :h_bizdate::date)::int INTO :h_dow; - if (sqlca.sqlcode < 0) FAIL(b); - weekend = (h_dow == 0 || h_dow == 6) ? 1 : 0; - setl(b, T_ARG1, h_dow); - setl(b, T_RC, weekend); - Bchg(b, T_MSG, 0, weekend ? "휴일-마감 이월" : "영업일-정상 마감", 0L); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0) { + setl(b, T_RC, 5309); + Bchg(b, T_MSG, 0, "영업일 미입력 - 휴일 마감정책 점검 불가", 0L); + FAIL(b); + } + if (strlen(h_bizdate) < 8) { + setl(b, T_RC, 5409); + Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); FAIL(b); + } + + /* --- 섹션2: 기준/정책 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_n4 FROM cl_close_log + WHERE close_type = 'DAILY' AND period_key = :h_bizdate AND status <> 'X'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5310); FAIL(b); } + + /* --- 섹션3·섹션4: 요일 판정으로 휴일 마감정책 결정 --- */ + EXEC SQL SELECT extract(dow from :h_bizdate::date) INTO :h_n1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5311); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_n2 FROM cm_holiday WHERE holi_date = :h_bizdate::date; + if (sqlca.sqlcode < 0) h_n2 = 0; + if (h_n1 == 0 || h_n1 == 6) { + o_rc = 1; strcpy(o_msg, "주말 - 마감 이월정책"); + } else if (h_n2 > 0) { + o_rc = 2; strcpy(o_msg, "공휴일 - 마감 SKIP"); + } else { + o_rc = 0; strcpy(o_msg, "영업일 - 정상 마감"); + } + o_count = h_n2; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_HOLICHK 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 2000000000L) { + z_sev = 3; + strcpy(z_msg, "초과대형 마감구간"); + } else if (h_zamt >= 200000000L) { + z_sev = 2; + strcpy(z_msg, "중대형 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "일반 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_HOLICHK 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_HOLICHK 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_HOLICHK 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_HOLICHK 휴일 마감정책점검 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_HOLICHK 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_HOLICHK 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_HOLIDAY.pgc b/app/src/cl/svc/CL_HOLIDAY.pgc index 6f2894e..241d4ca 100644 --- a/app/src/cl/svc/CL_HOLIDAY.pgc +++ b/app/src/cl/svc/CL_HOLIDAY.pgc @@ -9,29 +9,144 @@ void CL_HOLIDAY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long metric = 0; int i = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_txn; - char h_bizdate[16], h_dow[4]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_n1, h_n2, h_n3, h_cid; + char h_key[32], h_status[4], h_out[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_HOLIDAY"); + userlog("CL_HOLIDAY 진입: 마감처리 시작 (휴일 마감 정책: 영업일의 요일을 판정해 주말이면 마감 SKIP(RC=1) 정책을,)", svc); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT to_char(to_date(:h_bizdate,'YYYY-MM-DD'),'D') INTO :h_dow; - if (sqlca.sqlcode < 0) FAIL(b); - Bchg(b, T_STR3, 0, h_dow, 0L); - if (h_dow[0] == '1' || h_dow[0] == '7') { - setl(b, T_RC, 1); - Bchg(b, T_MSG, 0, "휴일 정책: 주말 마감 SKIP, 익영업일로 이월", 0L); - OK(b); + gets_(b, T_STR1, h_period, sizeof(h_period)); + gets_(b, T_KEY1, h_key, sizeof(h_key)); + h_cid = getl(b, T_ID1); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0 && h_cid <= 0) { + setl(b, T_RC, 6601); + Bchg(b, T_MSG, 0, "기준(영업일/기간/마감ID) 중 하나는 필수", 0L); FAIL(b); } - EXEC SQL SELECT count(*) INTO :h_txn FROM purchase WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_txn); - if (h_txn == 0) { - setl(b, T_RC, 2); - Bchg(b, T_MSG, 0, "평일 무거래: 0건 마감 여부 운영 판단", 0L); + + /* --- 섹션2·섹션4: 마감율/일정/SLA 산출 --- */ + EXEC SQL SELECT count(DISTINCT biz_date) INTO :h_n1 FROM purchase + WHERE to_char(biz_date,'YYYY-MM') = substr(:h_period,1,7); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6623); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_n2 FROM cl_close_log + WHERE close_type = 'DAILY' AND status <> 'X' + AND substr(period_key,1,7) = substr(:h_period,1,7); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6624); FAIL(b); } + metric = (h_n1 == 0) ? 0 : (h_n2 * 100 / h_n1); + o_count = h_n2; o_amount = h_n1; + if (h_n1 == 0) { o_rc = 1; strcpy(o_msg, "대상 영업일 없음"); } + else if (metric >= 100) { o_rc = 0; strcpy(o_msg, "마감 100% 완료"); } + else if (metric >= 50) { o_rc = 2; strcpy(o_msg, "마감 진행중"); } + else { o_rc = 3; strcpy(o_msg, "마감 지연"); } + setl(b, T_ARG1, metric); + + /* --- 섹션3b: 규모 부가판정 --- */ + if (metric < 0) metric = 0; + setl(b, T_ARG5, metric); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_HOLIDAY 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 2000000000L) { + z_sev = 3; + strcpy(z_msg, "초과대형 마감구간"); + } else if (h_zamt >= 200000000L) { + z_sev = 2; + strcpy(z_msg, "중대형 마감구간"); } else { - setl(b, T_RC, 0); - Bchg(b, T_MSG, 0, "평일 정상: 마감 진행 가능", 0L); + z_sev = 1; + strcpy(z_msg, "일반 마감구간"); } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_HOLIDAY 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_HOLIDAY 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_HOLIDAY 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_HOLIDAY 휴일 마감 정책: 영업일의 요일을 판정해 주말이면 마감 SKIP(RC=1) 정책을, cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_HOLIDAY 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_HOLIDAY 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_HYCLOSE.pgc b/app/src/cl/svc/CL_HYCLOSE.pgc index 9bb090a..a4b0d07 100644 --- a/app/src/cl/svc/CL_HYCLOSE.pgc +++ b/app/src/cl/svc/CL_HYCLOSE.pgc @@ -8,32 +8,205 @@ void CL_HYCLOSE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long cid, half; - char period[24], memo[32]; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long cid = 0; + long fee_calc = 0; + int grade = 0; + char memo[128]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16], h_yr[8]; - long h_mon, h_lo, h_hi, h_cnt, h_amt; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cnt, h_amt, h_fee, h_net; + long h_prev, h_bps; + char h_key[32]; + char h_status[4]; + char h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_HYCLOSE"); + userlog("CL_HYCLOSE 진입: 마감처리 시작 (반기 마감)", svc); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT extract(month from :h_bizdate::date)::int, - to_char(:h_bizdate::date, 'YYYY') INTO :h_mon, :h_yr; - if (sqlca.sqlcode < 0) FAIL(b); - half = (h_mon <= 6) ? 1 : 2; - h_lo = (half == 1) ? 1 : 7; - h_hi = (half == 1) ? 6 : 12; - EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_amt - FROM purchase WHERE to_char(biz_date,'YYYY') = :h_yr - AND extract(month from biz_date) BETWEEN :h_lo AND :h_hi; - if (sqlca.sqlcode < 0) FAIL(b); - snprintf(period, sizeof(period), "%s-HY%ld", h_yr, half); - snprintf(memo, sizeof(memo), "반기마감:%s반기", (half == 1) ? "상" : "하"); + gets_(b, T_STR1, h_period, sizeof(h_period)); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + strcpy(h_key, ""); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0) { + setl(b, T_RC, 5001); + Bchg(b, T_MSG, 0, "영업일/기간 미입력 - 반기 마감 불가", 0L); + FAIL(b); + } + if (strlen(h_bizdate) > 0 && strlen(h_bizdate) < 8) { + setl(b, T_RC, 5002); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + FAIL(b); + } + + /* --- 섹션2: 정책/기존마감 상태 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_prev FROM cl_close_log + WHERE close_type = 'HALFYR' AND period_key = :h_period AND status <> 'X'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5010); + Bchg(b, T_MSG, 0, "기존 마감 조회 실패", 0L); FAIL(b); + } + if (h_prev > 0) { + userlog("CL_HYCLOSE %s 이미 마감 %ld건 존재 - 재집계 진행", h_period, h_prev); + } + EXEC SQL SELECT count(*) INTO :h_bps FROM cl_period_lock + WHERE period_key = :h_period AND locked = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5011); FAIL(b); } + if (h_bps > 0) { + setl(b, T_RC, 5012); + Bchg(b, T_MSG, 0, "기간이 잠금상태 - 마감 불가", 0L); FAIL(b); + } + + /* --- 섹션4: 본처리부(집계) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) + INTO :h_cnt, :h_amt, :h_fee, :h_net + FROM purchase WHERE to_char(biz_date,'YYYY') = :h_period; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5020); + Bchg(b, T_MSG, 0, "매입 집계 실패", 0L); FAIL(b); + } + + /* --- 섹션3: 업무규칙 분기부(규모/등급) --- */ + if (h_cnt == 0) { + grade = 0; + strcpy(memo, "HALFYR:무거래"); + } else if (h_amt >= 1000000000) { + grade = 3; + strcpy(memo, "HALFYR:대규모"); + } else if (h_amt >= 100000000) { + grade = 2; + strcpy(memo, "HALFYR:중규모"); + } else { + grade = 1; + strcpy(memo, "HALFYR:소규모"); + } + fee_calc = acq_fee(h_amt, 25); + if (h_fee == 0 && h_amt > 0) h_fee = fee_calc; + + /* --- 섹션4b: 마감이력 생성 --- */ cid = cldb_next_close_id(); - if (cid < 0) FAIL(b); - if (cldb_insert_close(cid, "HALFYR", period, h_bizdate, "C", h_cnt, h_amt, memo) < 0) FAIL(b); + if (cid < 0) { setl(b, T_RC, 5030); FAIL(b); } + if (cldb_insert_close(cid, "HALFYR", h_period, + (h_bizdate[0] ? h_bizdate : h_period), + "C", h_cnt, h_amt, memo) < 0) { + setl(b, T_RC, 5031); + Bchg(b, T_MSG, 0, "마감이력 생성 실패", 0L); FAIL(b); + } + + /* --- 섹션6: 집계 확인/요약 재조회부 --- */ + h_kcid = cid; + EXEC SQL SELECT txn_count, total_amount INTO :h_prev, :h_amt + FROM cl_close_log WHERE close_id = :h_kcid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5040); FAIL(b); } + o_count = h_cnt; o_amount = h_amt; + + /* --- 섹션8prep: 상태/결과 세팅 --- */ setl(b, T_ID1, cid); - setl(b, T_ARG1, half); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_amt); + setl(b, T_FEE, h_fee); + setl(b, T_NET, h_net); + Bchg(b, T_STATUS, 0, "C", 0L); + snprintf(o_msg, sizeof(o_msg), "반기 마감완료 등급%d cid=%ld", grade, cid); + o_rc = 0; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_HYCLOSE 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 500000000L) { + z_sev = 3; + strcpy(z_msg, "고액 마감구간"); + } else if (h_zamt >= 50000000L) { + z_sev = 2; + strcpy(z_msg, "중액 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "소액 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_HYCLOSE 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_HYCLOSE 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_HYCLOSE 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_HYCLOSE 반기마감 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_HYCLOSE 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_HYCLOSE 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_HYVERIFY.pgc b/app/src/cl/svc/CL_HYVERIFY.pgc index 4679465..38b7d75 100644 --- a/app/src/cl/svc/CL_HYVERIFY.pgc +++ b/app/src/cl/svc/CL_HYVERIFY.pgc @@ -8,34 +8,168 @@ void CL_HYVERIFY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long half, da; - char pk[24]; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long dcnt = 0, damt = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16], h_yr[8], h_pk[24]; - long h_mon, h_lo, h_hi, h_la, h_ra; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_hdr_cnt, h_hdr_amt; + long h_real_cnt, h_real_amt; + char h_date[16], h_type[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_HYVERIFY"); + userlog("CL_HYVERIFY 진입: 마감처리 시작 (마감 정합 검증)", svc); + + h_cid = getl(b, T_ID1); gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT extract(month from :h_bizdate::date)::int, - to_char(:h_bizdate::date, 'YYYY') INTO :h_mon, :h_yr; - if (sqlca.sqlcode < 0) FAIL(b); - half = (h_mon <= 6) ? 1 : 2; - h_lo = (half == 1) ? 1 : 7; - h_hi = (half == 1) ? 6 : 12; - snprintf(pk, sizeof(pk), "%s-HY%ld", h_yr, half); - strncpy(h_pk, pk, sizeof(h_pk)-1); h_pk[sizeof(h_pk)-1] = 0; - EXEC SQL SELECT coalesce(sum(total_amount),0) INTO :h_la FROM cl_close_log - WHERE close_type = 'HALFYR' AND period_key = :h_pk AND status <> 'X'; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_ra FROM purchase - WHERE to_char(biz_date,'YYYY') = :h_yr - AND extract(month from biz_date) BETWEEN :h_lo AND :h_hi; - if (sqlca.sqlcode < 0) FAIL(b); - da = h_la - h_ra; if (da < 0) da = -da; - setl(b, T_ARG1, half); - setl(b, T_AMT1, h_la); - setl(b, T_AMT2, h_ra); - setl(b, T_DELTA, da); - setl(b, T_RC, (da == 0) ? 0 : 1); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0 && h_bizdate[0] == 0) { + setl(b, T_RC, 5401); + Bchg(b, T_MSG, 0, "마감ID/영업일 중 하나는 필수", 0L); FAIL(b); + } + + /* --- 섹션2: 마감이력 헤더 조회부 --- */ + if (h_cid > 0) { + EXEC SQL SELECT close_type, txn_count, total_amount, + to_char(biz_date,'YYYY-MM-DD') + INTO :h_type, :h_hdr_cnt, :h_hdr_amt, :h_date + FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5402); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5403); FAIL(b); } + } else { + strcpy(h_date, h_bizdate); + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(total_amount),0) + INTO :h_hdr_cnt, :h_hdr_amt FROM cl_close_log + WHERE period_key = :h_bizdate AND status <> 'X'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5404); FAIL(b); } + } + + /* --- 섹션4: 실매입 재집계부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_real_cnt, :h_real_amt + FROM purchase WHERE biz_date = :h_date; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5405); FAIL(b); } + + /* --- 섹션3: 대사 분기부 --- */ + dcnt = h_hdr_cnt - h_real_cnt; if (dcnt < 0) dcnt = -dcnt; + damt = h_hdr_amt - h_real_amt; if (damt < 0) damt = -damt; + if (dcnt == 0 && damt == 0) { + o_rc = 0; strcpy(o_msg, "건수·금액 완전 일치"); + } else if (dcnt == 0 && damt != 0) { + o_rc = 1; strcpy(o_msg, "건수 일치·금액 불일치"); + } else if (dcnt != 0 && damt == 0) { + o_rc = 2; strcpy(o_msg, "건수 불일치·금액 일치"); + } else { + o_rc = 3; strcpy(o_msg, "건수·금액 모두 불일치"); + } + + /* --- 섹션6: 결과 세팅 --- */ + setl(b, T_ARG1, h_hdr_cnt); + setl(b, T_ARG2, h_real_cnt); + setl(b, T_AMT1, h_hdr_amt); + setl(b, T_AMT2, h_real_amt); + setl(b, T_DELTA, damt); + o_count = dcnt; o_amount = damt; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_HYVERIFY 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 2000000000L) { + z_sev = 3; + strcpy(z_msg, "초과대형 마감구간"); + } else if (h_zamt >= 200000000L) { + z_sev = 2; + strcpy(z_msg, "중대형 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "일반 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_HYVERIFY 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_HYVERIFY 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_HYVERIFY 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_HYVERIFY 정합검증 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_HYVERIFY 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_HYVERIFY 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_INQUIRY.pgc b/app/src/cl/svc/CL_INQUIRY.pgc index dea51b8..4192d99 100644 --- a/app/src/cl/svc/CL_INQUIRY.pgc +++ b/app/src/cl/svc/CL_INQUIRY.pgc @@ -9,19 +9,161 @@ void CL_INQUIRY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long groups = 0; long topamt = 0; char topdim[64]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid, h_cnt, h_amt; - char h_type[16], h_status[8]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_grpcnt, h_grpamt, h_cid; + char h_dim[64], h_key[32]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; - h_cid = getl(b, T_ID1); - EXEC SQL SELECT close_type, status, txn_count, total_amount - INTO :h_type, :h_status, :h_cnt, :h_amt - FROM cl_close_log WHERE close_id = :h_cid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - Bchg(b, T_STR2, 0, h_type, 0L); - Bchg(b, T_STATUS, 0, h_status, 0L); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_amt); + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_INQUIRY"); + userlog("CL_INQUIRY 진입: 마감처리 시작 (마감조회)", svc); + + + /* --- 섹션1: 입력 검증부 --- */ + /* 전역 리포트 - 별도 필수입력 없음 */ + + /* --- 섹션2: 그룹 수 사전 확인부 --- */ + EXEC SQL SELECT count(DISTINCT close_type) INTO :h_grpcnt + FROM cl_close_log WHERE 1=1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6504); FAIL(b); } + if (h_grpcnt == 0) { + o_rc = 1; o_count = 0; + strcpy(o_msg, "리포트 대상 그룹 없음"); + setl(b, T_COUNT, 0); Bchg(b, T_MSG, 0, o_msg, 0L); OK(b); + } + + /* --- 섹션4: 그룹별 집계 커서(상위추출) --- */ + strcpy(topdim, ""); + EXEC SQL DECLARE crpt CURSOR FOR + SELECT close_type, count(*), coalesce(sum(total_amount),0) FROM cl_close_log + WHERE 1=1 GROUP BY close_type ORDER BY 2 DESC; + EXEC SQL OPEN crpt; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6505); FAIL(b); } + for (;;) { + EXEC SQL FETCH crpt INTO :h_dim, :h_grpcnt, :h_grpamt; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE crpt; setl(b, T_RC, 6506); FAIL(b); } + if (groups < 20) { + Bchg(b, T_STR2, (BFLDOCC)groups, h_dim, 0L); + setl(b, T_ARG3, h_grpamt); + } + if (h_grpamt > topamt) { + topamt = h_grpamt; + strncpy(topdim, h_dim, sizeof(topdim)-1); topdim[sizeof(topdim)-1]=0; + } + groups++; + } + EXEC SQL CLOSE crpt; + + /* --- 섹션3: 상위그룹 판정 분기부 --- */ + if (groups == 0) { + o_rc = 1; strcpy(o_msg, "마감조회 집계 결과 없음"); + } else if (topamt == 0) { + o_rc = 2; strcpy(o_msg, "마감조회 집계 금액 0"); + } else { + o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "마감조회 최다=%s(%ld)", topdim, topamt); + } + Bchg(b, T_STR1, 0, topdim, 0L); + o_count = groups; o_amount = topamt; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_INQUIRY 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1500000000L) { + z_sev = 3; + strcpy(z_msg, "광역 마감규모"); + } else if (h_zamt >= 150000000L) { + z_sev = 2; + strcpy(z_msg, "표준 마감규모"); + } else { + z_sev = 1; + strcpy(z_msg, "경량 마감규모"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_INQUIRY 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_INQUIRY 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_INQUIRY 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_INQUIRY 마감조회 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_INQUIRY 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_INQUIRY 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_ISSCLOSE.pgc b/app/src/cl/svc/CL_ISSCLOSE.pgc index 3622e6a..d238d9a 100644 --- a/app/src/cl/svc/CL_ISSCLOSE.pgc +++ b/app/src/cl/svc/CL_ISSCLOSE.pgc @@ -8,28 +8,212 @@ void CL_ISSCLOSE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char memo[48]; - long cid; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long cid = 0; + long fee_calc = 0; + int grade = 0; + char memo[128]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16], h_iss[24]; - long h_cnt, h_amt, h_fee; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cnt, h_amt, h_fee, h_net; + long h_prev, h_bps; + char h_key[32]; + char h_status[4]; + char h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_ISSCLOSE"); + userlog("CL_ISSCLOSE 진입: 마감처리 시작 (발급사 마감)", svc); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - gets_(b, T_ISSUER, h_iss, sizeof(h_iss)); - if (h_iss[0] == 0) strcpy(h_iss, "UNKNOWN"); - EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0) - INTO :h_cnt, :h_amt, :h_fee - FROM purchase WHERE biz_date = :h_bizdate AND issuer = :h_iss; - if (sqlca.sqlcode < 0) FAIL(b); + gets_(b, T_STR1, h_period, sizeof(h_period)); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + gets_(b, T_ISSUER, h_key, sizeof(h_key)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0) { + setl(b, T_RC, 5001); + Bchg(b, T_MSG, 0, "영업일/기간 미입력 - 발급사 마감 불가", 0L); + FAIL(b); + } + if (strlen(h_bizdate) > 0 && strlen(h_bizdate) < 8) { + setl(b, T_RC, 5002); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + FAIL(b); + } + if (h_key[0] == 0) { + setl(b, T_RC, 5003); + Bchg(b, T_MSG, 0, "발급사 구분값 미입력", 0L); + FAIL(b); + } + + /* --- 섹션2: 정책/기존마감 상태 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_prev FROM cl_close_log + WHERE close_type = 'ISS' AND period_key = :h_period AND status <> 'X'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5010); + Bchg(b, T_MSG, 0, "기존 마감 조회 실패", 0L); FAIL(b); + } + if (h_prev > 0) { + userlog("CL_ISSCLOSE %s 이미 마감 %ld건 존재 - 재집계 진행", h_period, h_prev); + } + EXEC SQL SELECT count(*) INTO :h_bps FROM cl_period_lock + WHERE period_key = :h_period AND locked = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5011); FAIL(b); } + if (h_bps > 0) { + setl(b, T_RC, 5012); + Bchg(b, T_MSG, 0, "기간이 잠금상태 - 마감 불가", 0L); FAIL(b); + } + + /* --- 섹션4: 본처리부(집계) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) + INTO :h_cnt, :h_amt, :h_fee, :h_net + FROM purchase WHERE biz_date = :h_bizdate AND issuer = :h_key; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5020); + Bchg(b, T_MSG, 0, "매입 집계 실패", 0L); FAIL(b); + } + + /* --- 섹션3: 업무규칙 분기부(규모/등급) --- */ + if (h_cnt == 0) { + grade = 0; + strcpy(memo, "ISS:무거래"); + } else if (h_amt >= 1000000000) { + grade = 3; + strcpy(memo, "ISS:대규모"); + } else if (h_amt >= 100000000) { + grade = 2; + strcpy(memo, "ISS:중규모"); + } else { + grade = 1; + strcpy(memo, "ISS:소규모"); + } + fee_calc = acq_fee(h_amt, 25); + if (h_fee == 0 && h_amt > 0) h_fee = fee_calc; + + /* --- 섹션4b: 마감이력 생성 --- */ cid = cldb_next_close_id(); - if (cid < 0) FAIL(b); - snprintf(memo, sizeof(memo), "발급사마감:%s", h_iss); - if (cldb_insert_close(cid, "ISS", h_bizdate, h_bizdate, "C", h_cnt, h_amt, memo) < 0) FAIL(b); + if (cid < 0) { setl(b, T_RC, 5030); FAIL(b); } + { char m2[128]; snprintf(m2, sizeof(m2), "%s|%s", memo, h_key); + strncpy(memo, m2, sizeof(memo)-1); memo[sizeof(memo)-1]=0; } + if (cldb_insert_close(cid, "ISS", h_period, + (h_bizdate[0] ? h_bizdate : h_period), + "C", h_cnt, h_amt, memo) < 0) { + setl(b, T_RC, 5031); + Bchg(b, T_MSG, 0, "마감이력 생성 실패", 0L); FAIL(b); + } + + /* --- 섹션6: 집계 확인/요약 재조회부 --- */ + h_kcid = cid; + EXEC SQL SELECT txn_count, total_amount INTO :h_prev, :h_amt + FROM cl_close_log WHERE close_id = :h_kcid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5040); FAIL(b); } + o_count = h_cnt; o_amount = h_amt; + + /* --- 섹션8prep: 상태/결과 세팅 --- */ setl(b, T_ID1, cid); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_amt); setl(b, T_FEE, h_fee); - Bchg(b, T_ISSUER, 0, h_iss, 0L); + setl(b, T_NET, h_net); + Bchg(b, T_STATUS, 0, "C", 0L); + snprintf(o_msg, sizeof(o_msg), "발급사 마감완료 등급%d cid=%ld", grade, cid); + o_rc = 0; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_ISSCLOSE 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1500000000L) { + z_sev = 3; + strcpy(z_msg, "광역 마감규모"); + } else if (h_zamt >= 150000000L) { + z_sev = 2; + strcpy(z_msg, "표준 마감규모"); + } else { + z_sev = 1; + strcpy(z_msg, "경량 마감규모"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_ISSCLOSE 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_ISSCLOSE 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_ISSCLOSE 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_ISSCLOSE 발급사마감 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_ISSCLOSE 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_ISSCLOSE 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_LOCK.pgc b/app/src/cl/svc/CL_LOCK.pgc index 89ee102..66d6e9d 100644 --- a/app/src/cl/svc/CL_LOCK.pgc +++ b/app/src/cl/svc/CL_LOCK.pgc @@ -9,19 +9,152 @@ void CL_LOCK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char period[16]; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long affected = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid; - char h_period[16]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_n, h_cnt; + char h_status[4]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_LOCK"); + userlog("CL_LOCK 진입: 마감처리 시작 (기간 잠금)", svc); + h_cid = getl(b, T_ID1); - gets_(b, T_STR1, period, sizeof(period)); if (period[0] == 0) gets_(b, T_BIZDATE, period, sizeof(period)); - strncpy(h_period, period, sizeof(h_period)-1); h_period[sizeof(h_period)-1] = 0; - EXEC SQL INSERT INTO cl_period_lock (period_key, locked) VALUES (:h_period, true) + gets_(b, T_STR1, h_period, sizeof(h_period)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_period[0] == 0) { + setl(b, T_RC, 5901); + Bchg(b, T_MSG, 0, "기간키(period) 미입력", 0L); FAIL(b); + } + + /* --- 섹션2: 기존 잠금상태 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_n FROM cl_period_lock + WHERE period_key = :h_period AND locked = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5902); FAIL(b); } + if (h_n > 0) { + o_rc = 0; o_count = 1; + strcpy(o_msg, "이미 잠금상태"); + setl(b, T_COUNT, 1); Bchg(b, T_MSG, 0, o_msg, 0L); + Bchg(b, T_STATUS, 0, "L", 0L); OK(b); + } + + /* --- 섹션4: 잠금 upsert 본처리부 --- */ + EXEC SQL INSERT INTO cl_period_lock (period_key, locked, locked_at) + VALUES (:h_period, true, now()) ON CONFLICT (period_key) DO UPDATE SET locked = true, locked_at = now(); - if (sqlca.sqlcode < 0) FAIL(b); - if (h_cid > 0 && cldb_update_close_status(h_cid, "L") < 0) FAIL(b); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5907); + Bchg(b, T_MSG, 0, "기간 잠금 실패", 0L); FAIL(b); + } + + /* --- 섹션4b: 해당 기간 마감 상태 L 표기 --- */ + EXEC SQL UPDATE cl_close_log SET status = 'L', updated_at = now() + WHERE period_key = :h_period AND status IN ('A','C'); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5908); FAIL(b); } + affected = sqlca.sqlerrd[2]; Bchg(b, T_STATUS, 0, "L", 0L); + o_count = affected; o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "기간잠금 완료 마감 %ld건 L표기", affected); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_LOCK 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1000000000L) { + z_sev = 3; + strcpy(z_msg, "대규모 마감기간"); + } else if (h_zamt >= 100000000L) { + z_sev = 2; + strcpy(z_msg, "중규모 마감기간"); + } else { + z_sev = 1; + strcpy(z_msg, "소규모 마감기간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_LOCK 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_LOCK 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_LOCK 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_LOCK 기간 잠금 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_LOCK 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_LOCK 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_LOCKAUDIT.pgc b/app/src/cl/svc/CL_LOCKAUDIT.pgc index 2a4960e..a129a16 100644 --- a/app/src/cl/svc/CL_LOCKAUDIT.pgc +++ b/app/src/cl/svc/CL_LOCKAUDIT.pgc @@ -9,24 +9,161 @@ void CL_LOCKAUDIT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long groups = 0; long topamt = 0; char topdim[64]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_bad; - char h_first[24]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_grpcnt, h_grpamt, h_cid; + char h_dim[64], h_key[32]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; - EXEC SQL SELECT count(*), coalesce(min(l.period_key),'') - INTO :h_bad, :h_first - FROM cl_period_lock l - WHERE l.locked = true - AND NOT EXISTS (SELECT 1 FROM cl_close_log c - WHERE c.period_key = l.period_key - AND c.status IN ('A','L')); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_bad); - setl(b, T_RC, (h_bad == 0) ? 0 : 1); - if (h_bad > 0) { - Bchg(b, T_KEY1, 0, h_first, 0L); - userlog("CL_LOCKAUDIT 승인없는 잠금 %ld건 (최초 %s)", h_bad, h_first); + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_LOCKAUDIT"); + userlog("CL_LOCKAUDIT 진입: 마감처리 시작 (잠금감사)", svc); + + + /* --- 섹션1: 입력 검증부 --- */ + /* 전역 리포트 - 별도 필수입력 없음 */ + + /* --- 섹션2: 그룹 수 사전 확인부 --- */ + EXEC SQL SELECT count(DISTINCT period_key) INTO :h_grpcnt + FROM cl_close_log WHERE status IN ('A','L'); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6504); FAIL(b); } + if (h_grpcnt == 0) { + o_rc = 1; o_count = 0; + strcpy(o_msg, "리포트 대상 그룹 없음"); + setl(b, T_COUNT, 0); Bchg(b, T_MSG, 0, o_msg, 0L); OK(b); } + + /* --- 섹션4: 그룹별 집계 커서(상위추출) --- */ + strcpy(topdim, ""); + EXEC SQL DECLARE crpt CURSOR FOR + SELECT period_key, count(*), coalesce(sum(total_amount),0) FROM cl_close_log + WHERE status IN ('A','L') GROUP BY period_key ORDER BY 2 DESC; + EXEC SQL OPEN crpt; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6505); FAIL(b); } + for (;;) { + EXEC SQL FETCH crpt INTO :h_dim, :h_grpcnt, :h_grpamt; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE crpt; setl(b, T_RC, 6506); FAIL(b); } + if (groups < 20) { + Bchg(b, T_STR2, (BFLDOCC)groups, h_dim, 0L); + setl(b, T_ARG3, h_grpamt); + } + if (h_grpamt > topamt) { + topamt = h_grpamt; + strncpy(topdim, h_dim, sizeof(topdim)-1); topdim[sizeof(topdim)-1]=0; + } + groups++; + } + EXEC SQL CLOSE crpt; + + /* --- 섹션3: 상위그룹 판정 분기부 --- */ + if (groups == 0) { + o_rc = 1; strcpy(o_msg, "잠금감사 집계 결과 없음"); + } else if (topamt == 0) { + o_rc = 2; strcpy(o_msg, "잠금감사 집계 금액 0"); + } else { + o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "잠금감사 최다=%s(%ld)", topdim, topamt); + } + Bchg(b, T_STR1, 0, topdim, 0L); + o_count = groups; o_amount = topamt; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_LOCKAUDIT 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 500000000L) { + z_sev = 3; + strcpy(z_msg, "고액 마감구간"); + } else if (h_zamt >= 50000000L) { + z_sev = 2; + strcpy(z_msg, "중액 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "소액 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_LOCKAUDIT 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_LOCKAUDIT 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_LOCKAUDIT 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_LOCKAUDIT 잠금감사 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_LOCKAUDIT 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_LOCKAUDIT 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_LOCKSWEEP.pgc b/app/src/cl/svc/CL_LOCKSWEEP.pgc index 041f7e7..492093c 100644 --- a/app/src/cl/svc/CL_LOCKSWEEP.pgc +++ b/app/src/cl/svc/CL_LOCKSWEEP.pgc @@ -9,28 +9,154 @@ void CL_LOCKSWEEP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long relocked, added; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long affected = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - char h_prefix[16]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_n, h_cnt; + char h_status[4]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; + char h_pk[24]; EXEC SQL END DECLARE SECTION; - gets_(b, T_STR1, h_prefix, sizeof(h_prefix)); - if (h_prefix[0] == 0) { Bchg(b, T_MSG, 0, "잠금 프리픽스 필요", 0L); FAIL(b); } - EXEC SQL UPDATE cl_period_lock - SET locked = true, locked_at = now() - WHERE period_key LIKE :h_prefix || '%' AND locked = false; - if (sqlca.sqlcode < 0) FAIL(b); - relocked = sqlca.sqlerrd[2]; - EXEC SQL INSERT INTO cl_period_lock (period_key, locked) - SELECT DISTINCT c.period_key, true FROM cl_close_log c - WHERE c.period_key LIKE :h_prefix || '%' AND c.status IN ('A','L') - AND NOT EXISTS (SELECT 1 FROM cl_period_lock l - WHERE l.period_key = c.period_key); - if (sqlca.sqlcode < 0) FAIL(b); - added = sqlca.sqlerrd[2]; - setl(b, T_ARG1, relocked); - setl(b, T_ARG2, added); - setl(b, T_COUNT, relocked + added); - userlog("CL_LOCKSWEEP %s 재잠금=%ld 보충=%ld", h_prefix, relocked, added); + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_LOCKSWEEP"); + userlog("CL_LOCKSWEEP 진입: 마감처리 시작 (기간 잠금 일괄)", svc); + + h_cid = getl(b, T_ID1); + gets_(b, T_STR1, h_period, sizeof(h_period)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_period[0] == 0) { + setl(b, T_RC, 5901); + Bchg(b, T_MSG, 0, "기간키(period) 미입력", 0L); FAIL(b); + } + + /* --- 섹션2: 잠금 대상 마감 확인부 --- */ + EXEC SQL SELECT count(*) INTO :h_n FROM cl_close_log + WHERE period_key LIKE :h_period || '%' AND status IN ('A','C'); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5902); FAIL(b); } + if (h_n == 0) { + setl(b, T_RC, 5903); + Bchg(b, T_MSG, 0, "잠금 대상 마감 없음", 0L); FAIL(b); + } + + /* --- 섹션4: 프리픽스 구간 일괄 잠금 커서 --- */ + { + EXEC SQL DECLARE clsw CURSOR FOR + SELECT DISTINCT period_key FROM cl_close_log + WHERE period_key LIKE :h_period || '%' AND status IN ('A','C'); + EXEC SQL OPEN clsw; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5904); FAIL(b); } + for (;;) { + EXEC SQL FETCH clsw INTO :h_pk; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE clsw; setl(b, T_RC, 5905); FAIL(b); } + EXEC SQL INSERT INTO cl_period_lock (period_key, locked, locked_at) + VALUES (:h_pk, true, now()) + ON CONFLICT (period_key) DO UPDATE SET locked = true, locked_at = now(); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE clsw; setl(b, T_RC, 5906); FAIL(b); } + affected++; + } + EXEC SQL CLOSE clsw; } + o_count = affected; o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "구간 %ld개 기간 일괄 잠금", affected); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_LOCKSWEEP 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1500000000L) { + z_sev = 3; + strcpy(z_msg, "광역 마감규모"); + } else if (h_zamt >= 150000000L) { + z_sev = 2; + strcpy(z_msg, "표준 마감규모"); + } else { + z_sev = 1; + strcpy(z_msg, "경량 마감규모"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_LOCKSWEEP 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_LOCKSWEEP 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_LOCKSWEEP 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_LOCKSWEEP 기간 잠금 일괄 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_LOCKSWEEP 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_LOCKSWEEP 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_MAINCLOSE.pgc b/app/src/cl/svc/CL_MAINCLOSE.pgc index 9ea90b7..8177c29 100644 --- a/app/src/cl/svc/CL_MAINCLOSE.pgc +++ b/app/src/cl/svc/CL_MAINCLOSE.pgc @@ -8,26 +8,205 @@ void CL_MAINCLOSE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char st[2]; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long cid = 0; + long fee_calc = 0; + int grade = 0; + char memo[128]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - char h_date[16]; - long h_cid, h_cnt, h_amt; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cnt, h_amt, h_fee, h_net; + long h_prev, h_bps; + char h_key[32]; + char h_status[4]; + char h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; - h_cid = getl(b, T_ID1); - if (cldb_get_close_status(h_cid, st) < 0) FAIL(b); - if (st[0] != 'O') { Bchg(b, T_MSG, 0, "가마감(O) 상태 아님-본마감 불가", 0L); FAIL(b); } - EXEC SQL SELECT to_char(biz_date,'YYYY-MM-DD') INTO :h_date - FROM cl_close_log WHERE close_id = :h_cid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_amt - FROM purchase WHERE biz_date = :h_date; - if (sqlca.sqlcode < 0) FAIL(b); - if (cldb_update_close_totals(h_cid, h_cnt, h_amt) < 0) FAIL(b); - if (cldb_update_close_status(h_cid, "C") < 0) FAIL(b); - setl(b, T_ID1, h_cid); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_amt); + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_MAINCLOSE"); + userlog("CL_MAINCLOSE 진입: 마감처리 시작 (본마감 마감)", svc); + + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + gets_(b, T_STR1, h_period, sizeof(h_period)); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + strcpy(h_key, ""); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0) { + setl(b, T_RC, 5001); + Bchg(b, T_MSG, 0, "영업일/기간 미입력 - 본마감 마감 불가", 0L); + FAIL(b); + } + if (strlen(h_bizdate) > 0 && strlen(h_bizdate) < 8) { + setl(b, T_RC, 5002); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + FAIL(b); + } + + /* --- 섹션2: 정책/기존마감 상태 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_prev FROM cl_close_log + WHERE close_type = 'DAILY' AND period_key = :h_period AND status <> 'X'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5010); + Bchg(b, T_MSG, 0, "기존 마감 조회 실패", 0L); FAIL(b); + } + if (h_prev > 0) { + userlog("CL_MAINCLOSE %s 이미 마감 %ld건 존재 - 재집계 진행", h_period, h_prev); + } + EXEC SQL SELECT count(*) INTO :h_bps FROM cl_period_lock + WHERE period_key = :h_period AND locked = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5011); FAIL(b); } + if (h_bps > 0) { + setl(b, T_RC, 5012); + Bchg(b, T_MSG, 0, "기간이 잠금상태 - 마감 불가", 0L); FAIL(b); + } + + /* --- 섹션4: 본처리부(집계) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) + INTO :h_cnt, :h_amt, :h_fee, :h_net + FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5020); + Bchg(b, T_MSG, 0, "매입 집계 실패", 0L); FAIL(b); + } + + /* --- 섹션3: 업무규칙 분기부(규모/등급) --- */ + if (h_cnt == 0) { + grade = 0; + strcpy(memo, "DAILY:무거래"); + } else if (h_amt >= 1000000000) { + grade = 3; + strcpy(memo, "DAILY:대규모"); + } else if (h_amt >= 100000000) { + grade = 2; + strcpy(memo, "DAILY:중규모"); + } else { + grade = 1; + strcpy(memo, "DAILY:소규모"); + } + fee_calc = acq_fee(h_amt, 25); + if (h_fee == 0 && h_amt > 0) h_fee = fee_calc; + + /* --- 섹션4b: 마감이력 생성 --- */ + cid = cldb_next_close_id(); + if (cid < 0) { setl(b, T_RC, 5030); FAIL(b); } + if (cldb_insert_close(cid, "DAILY", h_period, + (h_bizdate[0] ? h_bizdate : h_period), + "C", h_cnt, h_amt, memo) < 0) { + setl(b, T_RC, 5031); + Bchg(b, T_MSG, 0, "마감이력 생성 실패", 0L); FAIL(b); + } + + /* --- 섹션6: 집계 확인/요약 재조회부 --- */ + h_kcid = cid; + EXEC SQL SELECT txn_count, total_amount INTO :h_prev, :h_amt + FROM cl_close_log WHERE close_id = :h_kcid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5040); FAIL(b); } + o_count = h_cnt; o_amount = h_amt; + + /* --- 섹션8prep: 상태/결과 세팅 --- */ + setl(b, T_ID1, cid); + setl(b, T_FEE, h_fee); + setl(b, T_NET, h_net); Bchg(b, T_STATUS, 0, "C", 0L); + snprintf(o_msg, sizeof(o_msg), "본마감 마감완료 등급%d cid=%ld", grade, cid); + o_rc = 0; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_MAINCLOSE 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 500000000L) { + z_sev = 3; + strcpy(z_msg, "고액 마감구간"); + } else if (h_zamt >= 50000000L) { + z_sev = 2; + strcpy(z_msg, "중액 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "소액 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_MAINCLOSE 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_MAINCLOSE 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_MAINCLOSE 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_MAINCLOSE 본마감마감 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_MAINCLOSE 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_MAINCLOSE 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_MEMOSEARCH.pgc b/app/src/cl/svc/CL_MEMOSEARCH.pgc index be63c84..795a75c 100644 --- a/app/src/cl/svc/CL_MEMOSEARCH.pgc +++ b/app/src/cl/svc/CL_MEMOSEARCH.pgc @@ -9,26 +9,143 @@ void CL_MEMOSEARCH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long metric = 0; int i = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_hits, h_cid; - char h_kw[40], h_st[4], h_period[24]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_n1, h_n2, h_n3, h_cid; + char h_key[32], h_status[4], h_out[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; + char h_pat[64]; EXEC SQL END DECLARE SECTION; - gets_(b, T_STR1, h_kw, sizeof(h_kw)); - if (h_kw[0] == 0) { Bchg(b, T_MSG, 0, "검색 키워드 필요", 0L); FAIL(b); } - EXEC SQL SELECT count(*) INTO :h_hits FROM cl_close_log - WHERE memo LIKE '%' || :h_kw || '%'; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_hits); - if (h_hits == 0) { setl(b, T_RC, 1); OK(b); } - EXEC SQL SELECT close_id, status, period_key INTO :h_cid, :h_st, :h_period + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_MEMOSEARCH"); + userlog("CL_MEMOSEARCH 진입: 마감처리 시작 (마감 사유 검색: memo 에 키워드(T_STR1)가 포함된 마감을 검색해 총 건수와)", svc); + + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + gets_(b, T_STR1, h_period, sizeof(h_period)); + gets_(b, T_KEY1, h_key, sizeof(h_key)); + h_cid = getl(b, T_ID1); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0 && h_cid <= 0) { + setl(b, T_RC, 6601); + Bchg(b, T_MSG, 0, "기준(영업일/기간/마감ID) 중 하나는 필수", 0L); FAIL(b); + } + + /* --- 섹션2·섹션4: memo 키워드 검색 --- */ + if (h_period[0] == 0) { + setl(b, T_RC, 6605); + Bchg(b, T_MSG, 0, "검색 키워드(T_STR1) 필수", 0L); FAIL(b); + } + { char pat[64]; snprintf(pat, sizeof(pat), "%%%s%%", h_period); + strncpy(h_pat, pat, sizeof(h_pat)-1); h_pat[sizeof(h_pat)-1]=0; + EXEC SQL SELECT count(*), coalesce(sum(total_amount),0) + INTO :h_n1, :h_n2 FROM cl_close_log WHERE memo LIKE :h_pat; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6606); FAIL(b); } + metric = h_n1; o_count = h_n1; o_amount = h_n2; } + if (metric == 0) { o_rc = 1; strcpy(o_msg, "검색 결과 없음"); } + else { o_rc = 0; strcpy(o_msg, "memo 검색 완료"); } + + /* --- 섹션3b: 규모 부가판정 --- */ + if (metric < 0) metric = 0; + setl(b, T_ARG5, metric); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt FROM cl_close_log - WHERE memo LIKE '%' || :h_kw || '%' - ORDER BY created_at DESC, close_id DESC LIMIT 1; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_ID1, h_cid); - Bchg(b, T_STATUS, 0, h_st, 0L); - Bchg(b, T_KEY1, 0, h_period, 0L); - setl(b, T_RC, 0); + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_MEMOSEARCH 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1000000000L) { + z_sev = 3; + strcpy(z_msg, "대규모 마감기간"); + } else if (h_zamt >= 100000000L) { + z_sev = 2; + strcpy(z_msg, "중규모 마감기간"); + } else { + z_sev = 1; + strcpy(z_msg, "소규모 마감기간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_MEMOSEARCH 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_MEMOSEARCH 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_MEMOSEARCH 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_MEMOSEARCH 마감 사유 검색: memo 에 키워드(T_STR1)가 포함된 마감을 검색해 총 건수와 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_MEMOSEARCH 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_MEMOSEARCH 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_MERCLOSE.pgc b/app/src/cl/svc/CL_MERCLOSE.pgc index 59cb66e..5a09a3b 100644 --- a/app/src/cl/svc/CL_MERCLOSE.pgc +++ b/app/src/cl/svc/CL_MERCLOSE.pgc @@ -9,34 +9,212 @@ void CL_MERCLOSE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long sid; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long cid = 0; + long fee_calc = 0; + int grade = 0; + char memo[128]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid, h_gross, h_fee, h_net, h_cnt; - char h_bizdate[16], h_merch[24]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cnt, h_amt, h_fee, h_net; + long h_prev, h_bps; + char h_key[32]; + char h_status[4]; + char h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_MERCLOSE"); + userlog("CL_MERCLOSE 진입: 마감처리 시작 (가맹점 마감)", svc); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); - if (h_merch[0] == 0) { Bchg(b, T_MSG, 0, "가맹점 ID 필요", 0L); FAIL(b); } - EXEC SQL SELECT close_id INTO :h_cid FROM cl_close_log - WHERE close_type = 'DAILY' AND period_key = :h_bizdate AND status <> 'X' - ORDER BY close_id DESC LIMIT 1; - if (sqlca.sqlcode == 100) { Bchg(b, T_MSG, 0, "당일 일마감 선행 필요", 0L); FAIL(b); } - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT coalesce(sum(amount),0), coalesce(sum(fee),0), - coalesce(sum(net),0), count(*) - INTO :h_gross, :h_fee, :h_net, :h_cnt - FROM purchase WHERE biz_date = :h_bizdate AND merchant_id = :h_merch; - if (sqlca.sqlcode < 0) FAIL(b); - sid = cldb_next_snapshot_id(); - if (sid < 0) FAIL(b); - if (cldb_insert_snapshot(sid, h_cid, h_bizdate, h_bizdate, h_merch, - h_gross, h_fee, h_net, h_cnt) < 0) FAIL(b); - setl(b, T_ID1, h_cid); - setl(b, T_ID2, sid); - setl(b, T_GROSS, h_gross); + gets_(b, T_STR1, h_period, sizeof(h_period)); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + gets_(b, T_MERCHANT, h_key, sizeof(h_key)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0) { + setl(b, T_RC, 5001); + Bchg(b, T_MSG, 0, "영업일/기간 미입력 - 가맹점 마감 불가", 0L); + FAIL(b); + } + if (strlen(h_bizdate) > 0 && strlen(h_bizdate) < 8) { + setl(b, T_RC, 5002); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + FAIL(b); + } + if (h_key[0] == 0) { + setl(b, T_RC, 5003); + Bchg(b, T_MSG, 0, "가맹점 구분값 미입력", 0L); + FAIL(b); + } + + /* --- 섹션2: 정책/기존마감 상태 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_prev FROM cl_close_log + WHERE close_type = 'MER' AND period_key = :h_period AND status <> 'X'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5010); + Bchg(b, T_MSG, 0, "기존 마감 조회 실패", 0L); FAIL(b); + } + if (h_prev > 0) { + userlog("CL_MERCLOSE %s 이미 마감 %ld건 존재 - 재집계 진행", h_period, h_prev); + } + EXEC SQL SELECT count(*) INTO :h_bps FROM cl_period_lock + WHERE period_key = :h_period AND locked = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5011); FAIL(b); } + if (h_bps > 0) { + setl(b, T_RC, 5012); + Bchg(b, T_MSG, 0, "기간이 잠금상태 - 마감 불가", 0L); FAIL(b); + } + + /* --- 섹션4: 본처리부(집계) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) + INTO :h_cnt, :h_amt, :h_fee, :h_net + FROM purchase WHERE biz_date = :h_bizdate AND merchant_id = :h_key; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5020); + Bchg(b, T_MSG, 0, "매입 집계 실패", 0L); FAIL(b); + } + + /* --- 섹션3: 업무규칙 분기부(규모/등급) --- */ + if (h_cnt == 0) { + grade = 0; + strcpy(memo, "MER:무거래"); + } else if (h_amt >= 1000000000) { + grade = 3; + strcpy(memo, "MER:대규모"); + } else if (h_amt >= 100000000) { + grade = 2; + strcpy(memo, "MER:중규모"); + } else { + grade = 1; + strcpy(memo, "MER:소규모"); + } + fee_calc = acq_fee(h_amt, 25); + if (h_fee == 0 && h_amt > 0) h_fee = fee_calc; + + /* --- 섹션4b: 마감이력 생성 --- */ + cid = cldb_next_close_id(); + if (cid < 0) { setl(b, T_RC, 5030); FAIL(b); } + { char m2[128]; snprintf(m2, sizeof(m2), "%s|%s", memo, h_key); + strncpy(memo, m2, sizeof(memo)-1); memo[sizeof(memo)-1]=0; } + if (cldb_insert_close(cid, "MER", h_period, + (h_bizdate[0] ? h_bizdate : h_period), + "C", h_cnt, h_amt, memo) < 0) { + setl(b, T_RC, 5031); + Bchg(b, T_MSG, 0, "마감이력 생성 실패", 0L); FAIL(b); + } + + /* --- 섹션6: 집계 확인/요약 재조회부 --- */ + h_kcid = cid; + EXEC SQL SELECT txn_count, total_amount INTO :h_prev, :h_amt + FROM cl_close_log WHERE close_id = :h_kcid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5040); FAIL(b); } + o_count = h_cnt; o_amount = h_amt; + + /* --- 섹션8prep: 상태/결과 세팅 --- */ + setl(b, T_ID1, cid); + setl(b, T_FEE, h_fee); setl(b, T_NET, h_net); - setl(b, T_COUNT, h_cnt); - userlog("CL_MERCLOSE %s/%s sid=%ld 가맹점 마감", h_merch, h_bizdate, sid); + Bchg(b, T_STATUS, 0, "C", 0L); + snprintf(o_msg, sizeof(o_msg), "가맹점 마감완료 등급%d cid=%ld", grade, cid); + o_rc = 0; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_MERCLOSE 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1500000000L) { + z_sev = 3; + strcpy(z_msg, "광역 마감규모"); + } else if (h_zamt >= 150000000L) { + z_sev = 2; + strcpy(z_msg, "표준 마감규모"); + } else { + z_sev = 1; + strcpy(z_msg, "경량 마감규모"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_MERCLOSE 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_MERCLOSE 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_MERCLOSE 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_MERCLOSE 가맹점마감 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_MERCLOSE 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_MERCLOSE 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_MERXCHECK.pgc b/app/src/cl/svc/CL_MERXCHECK.pgc index c4081a2..2320a12 100644 --- a/app/src/cl/svc/CL_MERXCHECK.pgc +++ b/app/src/cl/svc/CL_MERXCHECK.pgc @@ -9,35 +9,159 @@ void CL_MERXCHECK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long total = 0, mism = 0; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long d1 = 0, d2 = 0, worst = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - char h_date[16], h_mer[16]; - long h_cid, h_sg, h_pa; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_hdr, h_real, h_snap; + char h_date[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_MERXCHECK"); + userlog("CL_MERXCHECK 진입: 마감처리 시작 (가맹점금액 크로스검증)", svc); + h_cid = getl(b, T_ID1); - EXEC SQL SELECT to_char(biz_date,'YYYY-MM-DD') INTO :h_date - FROM cl_close_log WHERE close_id = :h_cid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - EXEC SQL DECLARE crmx CURSOR FOR - SELECT s.merchant_id, s.gross_amount, coalesce(pp.amt,0) - FROM cl_snapshot s - LEFT JOIN (SELECT merchant_id, sum(amount) AS amt FROM purchase - WHERE biz_date = :h_date GROUP BY merchant_id) pp - ON pp.merchant_id = s.merchant_id - WHERE s.close_id = :h_cid; - EXEC SQL OPEN crmx; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH crmx INTO :h_mer, :h_sg, :h_pa; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE crmx; FAIL(b); } - total++; - if (h_sg != h_pa) mism++; + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0) { + setl(b, T_RC, 5501); + Bchg(b, T_MSG, 0, "마감ID(T_ID1) 필수", 0L); FAIL(b); } - EXEC SQL CLOSE crmx; - setl(b, T_COUNT, total); - setl(b, T_ARG1, mism); - setl(b, T_RC, (mism == 0) ? 0 : 1); + + /* --- 섹션2: 마감이력 헤더 조회부 --- */ + EXEC SQL SELECT total_amount, to_char(biz_date,'YYYY-MM-DD') + INTO :h_hdr, :h_date FROM cl_close_log + WHERE close_id = :h_cid AND status <> 'X'; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5502); + Bchg(b, T_MSG, 0, "유효 마감이력 없음(취소/미존재)", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5503); FAIL(b); } + + /* --- 섹션4: 실매입/스냅샷 집계부 --- */ + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_real + FROM purchase WHERE biz_date = :h_date; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5504); FAIL(b); } + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :h_snap + FROM cl_snapshot WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5505); FAIL(b); } + + /* --- 섹션3: 3원 대사 분기부 --- */ + d1 = h_hdr - h_real; if (d1 < 0) d1 = -d1; + d2 = (h_snap == 0) ? 0 : (h_hdr - h_snap); if (d2 < 0) d2 = -d2; + worst = (d1 > d2) ? d1 : d2; + if (worst == 0) { + o_rc = 0; strcpy(o_msg, "헤더=매입=스냅샷 3원 일치"); + } else if (d1 != 0 && d2 == 0) { + o_rc = 1; strcpy(o_msg, "헤더-매입 불일치(스냅샷은 일치)"); + } else if (d1 == 0 && d2 != 0) { + o_rc = 2; strcpy(o_msg, "헤더-스냅샷 불일치(매입은 일치)"); + } else { + o_rc = 3; strcpy(o_msg, "3원 전부 불일치 - 재마감 검토"); + } + + /* --- 섹션6: 편차 세팅 --- */ + setl(b, T_AMT1, h_hdr); + setl(b, T_AMT2, h_real); + setl(b, T_AMT3, h_snap); + setl(b, T_DELTA, worst); + o_count = (worst == 0) ? 0 : 1; o_amount = worst; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_MERXCHECK 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 2000000000L) { + z_sev = 3; + strcpy(z_msg, "초과대형 마감구간"); + } else if (h_zamt >= 200000000L) { + z_sev = 2; + strcpy(z_msg, "중대형 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "일반 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_MERXCHECK 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_MERXCHECK 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_MERXCHECK 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_MERXCHECK 가맹점금액크로스 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_MERXCHECK 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_MERXCHECK 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_MIDCLOSE.pgc b/app/src/cl/svc/CL_MIDCLOSE.pgc index a1db8c1..d3fab18 100644 --- a/app/src/cl/svc/CL_MIDCLOSE.pgc +++ b/app/src/cl/svc/CL_MIDCLOSE.pgc @@ -9,21 +9,205 @@ void CL_MIDCLOSE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long cid = 0; + long fee_calc = 0; + int grade = 0; + char memo[128]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_amt, h_cid; char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cnt, h_amt, h_fee, h_net; + long h_prev, h_bps; + char h_key[32]; + char h_status[4]; + char h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_amt - FROM purchase WHERE biz_date = :h_bizdate AND status IN ('A', 'M'); - if (sqlca.sqlcode < 0) FAIL(b); - h_cid = cldb_next_close_id(); - if (h_cid < 0) FAIL(b); - if (cldb_insert_close(h_cid, "MID", bizdate, bizdate, "O", h_cnt, h_amt, "중간마감") < 0) FAIL(b); - setl(b, T_ID1, h_cid); - setl(b, T_COUNT, h_cnt); + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_MIDCLOSE"); + userlog("CL_MIDCLOSE 진입: 마감처리 시작 (중간 마감)", svc); + + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + gets_(b, T_STR1, h_period, sizeof(h_period)); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + strcpy(h_key, ""); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0) { + setl(b, T_RC, 5001); + Bchg(b, T_MSG, 0, "영업일/기간 미입력 - 중간 마감 불가", 0L); + FAIL(b); + } + if (strlen(h_bizdate) > 0 && strlen(h_bizdate) < 8) { + setl(b, T_RC, 5002); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + FAIL(b); + } + + /* --- 섹션2: 정책/기존마감 상태 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_prev FROM cl_close_log + WHERE close_type = 'MID' AND period_key = :h_period AND status <> 'X'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5010); + Bchg(b, T_MSG, 0, "기존 마감 조회 실패", 0L); FAIL(b); + } + if (h_prev > 0) { + userlog("CL_MIDCLOSE %s 이미 마감 %ld건 존재 - 재집계 진행", h_period, h_prev); + } + EXEC SQL SELECT count(*) INTO :h_bps FROM cl_period_lock + WHERE period_key = :h_period AND locked = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5011); FAIL(b); } + if (h_bps > 0) { + setl(b, T_RC, 5012); + Bchg(b, T_MSG, 0, "기간이 잠금상태 - 마감 불가", 0L); FAIL(b); + } + + /* --- 섹션4: 본처리부(집계) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) + INTO :h_cnt, :h_amt, :h_fee, :h_net + FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5020); + Bchg(b, T_MSG, 0, "매입 집계 실패", 0L); FAIL(b); + } + + /* --- 섹션3: 업무규칙 분기부(규모/등급) --- */ + if (h_cnt == 0) { + grade = 0; + strcpy(memo, "MID:무거래"); + } else if (h_amt >= 1000000000) { + grade = 3; + strcpy(memo, "MID:대규모"); + } else if (h_amt >= 100000000) { + grade = 2; + strcpy(memo, "MID:중규모"); + } else { + grade = 1; + strcpy(memo, "MID:소규모"); + } + fee_calc = acq_fee(h_amt, 25); + if (h_fee == 0 && h_amt > 0) h_fee = fee_calc; + + /* --- 섹션4b: 마감이력 생성 --- */ + cid = cldb_next_close_id(); + if (cid < 0) { setl(b, T_RC, 5030); FAIL(b); } + if (cldb_insert_close(cid, "MID", h_period, + (h_bizdate[0] ? h_bizdate : h_period), + "C", h_cnt, h_amt, memo) < 0) { + setl(b, T_RC, 5031); + Bchg(b, T_MSG, 0, "마감이력 생성 실패", 0L); FAIL(b); + } + + /* --- 섹션6: 집계 확인/요약 재조회부 --- */ + h_kcid = cid; + EXEC SQL SELECT txn_count, total_amount INTO :h_prev, :h_amt + FROM cl_close_log WHERE close_id = :h_kcid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5040); FAIL(b); } + o_count = h_cnt; o_amount = h_amt; + + /* --- 섹션8prep: 상태/결과 세팅 --- */ + setl(b, T_ID1, cid); + setl(b, T_FEE, h_fee); + setl(b, T_NET, h_net); + Bchg(b, T_STATUS, 0, "C", 0L); + snprintf(o_msg, sizeof(o_msg), "중간 마감완료 등급%d cid=%ld", grade, cid); + o_rc = 0; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_MIDCLOSE 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1500000000L) { + z_sev = 3; + strcpy(z_msg, "광역 마감규모"); + } else if (h_zamt >= 150000000L) { + z_sev = 2; + strcpy(z_msg, "표준 마감규모"); + } else { + z_sev = 1; + strcpy(z_msg, "경량 마감규모"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_MIDCLOSE 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_MIDCLOSE 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_MIDCLOSE 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_MIDCLOSE 중간마감 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_MIDCLOSE 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_MIDCLOSE 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_MONTHLY.pgc b/app/src/cl/svc/CL_MONTHLY.pgc index 0284156..0152633 100644 --- a/app/src/cl/svc/CL_MONTHLY.pgc +++ b/app/src/cl/svc/CL_MONTHLY.pgc @@ -9,14 +9,205 @@ void CL_MONTHLY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], period[16]; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; long cid = 0; - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - gets_(b, T_STR1, period, sizeof(period)); - if (period[0] == 0) { strncpy(period, bizdate, 7); period[7] = 0; } - if (cl_open_close(b, "MONTHLY", period, bizdate, &cid) < 0) FAIL(b); + long fee_calc = 0; + int grade = 0; + char memo[128]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; + EXEC SQL BEGIN DECLARE SECTION; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cnt, h_amt, h_fee, h_net; + long h_prev, h_bps; + char h_key[32]; + char h_status[4]; + char h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; + EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_MONTHLY"); + userlog("CL_MONTHLY 진입: 마감처리 시작 (월 마감)", svc); + + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + gets_(b, T_STR1, h_period, sizeof(h_period)); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + strcpy(h_key, ""); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0) { + setl(b, T_RC, 5001); + Bchg(b, T_MSG, 0, "영업일/기간 미입력 - 월 마감 불가", 0L); + FAIL(b); + } + if (strlen(h_bizdate) > 0 && strlen(h_bizdate) < 8) { + setl(b, T_RC, 5002); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + FAIL(b); + } + + /* --- 섹션2: 정책/기존마감 상태 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_prev FROM cl_close_log + WHERE close_type = 'MONTHLY' AND period_key = :h_period AND status <> 'X'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5010); + Bchg(b, T_MSG, 0, "기존 마감 조회 실패", 0L); FAIL(b); + } + if (h_prev > 0) { + userlog("CL_MONTHLY %s 이미 마감 %ld건 존재 - 재집계 진행", h_period, h_prev); + } + EXEC SQL SELECT count(*) INTO :h_bps FROM cl_period_lock + WHERE period_key = :h_period AND locked = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5011); FAIL(b); } + if (h_bps > 0) { + setl(b, T_RC, 5012); + Bchg(b, T_MSG, 0, "기간이 잠금상태 - 마감 불가", 0L); FAIL(b); + } + + /* --- 섹션4: 본처리부(집계) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) + INTO :h_cnt, :h_amt, :h_fee, :h_net + FROM purchase WHERE to_char(biz_date,'YYYY-MM') = :h_period; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5020); + Bchg(b, T_MSG, 0, "매입 집계 실패", 0L); FAIL(b); + } + + /* --- 섹션3: 업무규칙 분기부(규모/등급) --- */ + if (h_cnt == 0) { + grade = 0; + strcpy(memo, "MONTHLY:무거래"); + } else if (h_amt >= 1000000000) { + grade = 3; + strcpy(memo, "MONTHLY:대규모"); + } else if (h_amt >= 100000000) { + grade = 2; + strcpy(memo, "MONTHLY:중규모"); + } else { + grade = 1; + strcpy(memo, "MONTHLY:소규모"); + } + fee_calc = acq_fee(h_amt, 25); + if (h_fee == 0 && h_amt > 0) h_fee = fee_calc; + + /* --- 섹션4b: 마감이력 생성 --- */ + cid = cldb_next_close_id(); + if (cid < 0) { setl(b, T_RC, 5030); FAIL(b); } + if (cldb_insert_close(cid, "MONTHLY", h_period, + (h_bizdate[0] ? h_bizdate : h_period), + "C", h_cnt, h_amt, memo) < 0) { + setl(b, T_RC, 5031); + Bchg(b, T_MSG, 0, "마감이력 생성 실패", 0L); FAIL(b); + } + + /* --- 섹션6: 집계 확인/요약 재조회부 --- */ + h_kcid = cid; + EXEC SQL SELECT txn_count, total_amount INTO :h_prev, :h_amt + FROM cl_close_log WHERE close_id = :h_kcid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5040); FAIL(b); } + o_count = h_cnt; o_amount = h_amt; + + /* --- 섹션8prep: 상태/결과 세팅 --- */ setl(b, T_ID1, cid); + setl(b, T_FEE, h_fee); + setl(b, T_NET, h_net); Bchg(b, T_STATUS, 0, "C", 0L); + snprintf(o_msg, sizeof(o_msg), "월 마감완료 등급%d cid=%ld", grade, cid); + o_rc = 0; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_MONTHLY 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1000000000L) { + z_sev = 3; + strcpy(z_msg, "대규모 마감기간"); + } else if (h_zamt >= 100000000L) { + z_sev = 2; + strcpy(z_msg, "중규모 마감기간"); + } else { + z_sev = 1; + strcpy(z_msg, "소규모 마감기간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_MONTHLY 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_MONTHLY 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_MONTHLY 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_MONTHLY 월마감 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_MONTHLY 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_MONTHLY 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_MONVERIFY.pgc b/app/src/cl/svc/CL_MONVERIFY.pgc index 83c8967..a3c61e6 100644 --- a/app/src/cl/svc/CL_MONVERIFY.pgc +++ b/app/src/cl/svc/CL_MONVERIFY.pgc @@ -8,28 +8,168 @@ void CL_MONVERIFY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long dc, da; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long dcnt = 0, damt = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; - long h_lc, h_la, h_rc, h_ra; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_hdr_cnt, h_hdr_amt; + long h_real_cnt, h_real_amt; + char h_date[16], h_type[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_MONVERIFY"); + userlog("CL_MONVERIFY 진입: 마감처리 시작 (마감 정합 검증)", svc); + + h_cid = getl(b, T_ID1); gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(total_amount),0) - INTO :h_lc, :h_la FROM cl_close_log - WHERE close_type = 'MONTHLY' - AND period_key = to_char(:h_bizdate::date, 'YYYY-MM') AND status <> 'X'; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_rc, :h_ra - FROM purchase WHERE to_char(biz_date,'YYYY-MM') = to_char(:h_bizdate::date,'YYYY-MM'); - if (sqlca.sqlcode < 0) FAIL(b); - dc = h_lc - h_rc; if (dc < 0) dc = -dc; - da = h_la - h_ra; if (da < 0) da = -da; - setl(b, T_ARG1, h_lc); - setl(b, T_COUNT, h_rc); - setl(b, T_AMT1, h_la); - setl(b, T_AMT2, h_ra); - setl(b, T_DELTA, da); - setl(b, T_RC, (dc == 0 && da == 0) ? 0 : 1); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0 && h_bizdate[0] == 0) { + setl(b, T_RC, 5401); + Bchg(b, T_MSG, 0, "마감ID/영업일 중 하나는 필수", 0L); FAIL(b); + } + + /* --- 섹션2: 마감이력 헤더 조회부 --- */ + if (h_cid > 0) { + EXEC SQL SELECT close_type, txn_count, total_amount, + to_char(biz_date,'YYYY-MM-DD') + INTO :h_type, :h_hdr_cnt, :h_hdr_amt, :h_date + FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5402); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5403); FAIL(b); } + } else { + strcpy(h_date, h_bizdate); + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(total_amount),0) + INTO :h_hdr_cnt, :h_hdr_amt FROM cl_close_log + WHERE period_key = :h_bizdate AND status <> 'X'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5404); FAIL(b); } + } + + /* --- 섹션4: 실매입 재집계부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_real_cnt, :h_real_amt + FROM purchase WHERE biz_date = :h_date; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5405); FAIL(b); } + + /* --- 섹션3: 대사 분기부 --- */ + dcnt = h_hdr_cnt - h_real_cnt; if (dcnt < 0) dcnt = -dcnt; + damt = h_hdr_amt - h_real_amt; if (damt < 0) damt = -damt; + if (dcnt == 0 && damt == 0) { + o_rc = 0; strcpy(o_msg, "건수·금액 완전 일치"); + } else if (dcnt == 0 && damt != 0) { + o_rc = 1; strcpy(o_msg, "건수 일치·금액 불일치"); + } else if (dcnt != 0 && damt == 0) { + o_rc = 2; strcpy(o_msg, "건수 불일치·금액 일치"); + } else { + o_rc = 3; strcpy(o_msg, "건수·금액 모두 불일치"); + } + + /* --- 섹션6: 결과 세팅 --- */ + setl(b, T_ARG1, h_hdr_cnt); + setl(b, T_ARG2, h_real_cnt); + setl(b, T_AMT1, h_hdr_amt); + setl(b, T_AMT2, h_real_amt); + setl(b, T_DELTA, damt); + o_count = dcnt; o_amount = damt; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_MONVERIFY 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1500000000L) { + z_sev = 3; + strcpy(z_msg, "광역 마감규모"); + } else if (h_zamt >= 150000000L) { + z_sev = 2; + strcpy(z_msg, "표준 마감규모"); + } else { + z_sev = 1; + strcpy(z_msg, "경량 마감규모"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_MONVERIFY 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_MONVERIFY 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_MONVERIFY 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_MONVERIFY 정합검증 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_MONVERIFY 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_MONVERIFY 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_NETCROSS.pgc b/app/src/cl/svc/CL_NETCROSS.pgc index e25ca54..1c57825 100644 --- a/app/src/cl/svc/CL_NETCROSS.pgc +++ b/app/src/cl/svc/CL_NETCROSS.pgc @@ -8,26 +8,159 @@ void CL_NETCROSS(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long delta; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long d1 = 0, d2 = 0, worst = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_hdr, h_real, h_snap; char h_date[16]; - long h_cid, h_sn, h_pn; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_NETCROSS"); + userlog("CL_NETCROSS 진입: 마감처리 시작 (정산액 크로스검증)", svc); + h_cid = getl(b, T_ID1); - EXEC SQL SELECT to_char(biz_date,'YYYY-MM-DD') INTO :h_date - FROM cl_close_log WHERE close_id = :h_cid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - EXEC SQL SELECT coalesce(sum(net_amount),0) INTO :h_sn - FROM cl_snapshot WHERE close_id = :h_cid; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT coalesce(sum(net),0) INTO :h_pn + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0) { + setl(b, T_RC, 5501); + Bchg(b, T_MSG, 0, "마감ID(T_ID1) 필수", 0L); FAIL(b); + } + + /* --- 섹션2: 마감이력 헤더 조회부 --- */ + EXEC SQL SELECT total_amount, to_char(biz_date,'YYYY-MM-DD') + INTO :h_hdr, :h_date FROM cl_close_log + WHERE close_id = :h_cid AND status <> 'X'; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5502); + Bchg(b, T_MSG, 0, "유효 마감이력 없음(취소/미존재)", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5503); FAIL(b); } + + /* --- 섹션4: 실매입/스냅샷 집계부 --- */ + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_real FROM purchase WHERE biz_date = :h_date; - if (sqlca.sqlcode < 0) FAIL(b); - delta = h_sn - h_pn; if (delta < 0) delta = -delta; - setl(b, T_AMT1, h_sn); - setl(b, T_AMT2, h_pn); - setl(b, T_DELTA, delta); - setl(b, T_RC, (delta == 0) ? 0 : 1); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5504); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net_amount),0) INTO :h_snap + FROM cl_snapshot WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5505); FAIL(b); } + + /* --- 섹션3: 3원 대사 분기부 --- */ + d1 = h_hdr - h_real; if (d1 < 0) d1 = -d1; + d2 = (h_snap == 0) ? 0 : (h_hdr - h_snap); if (d2 < 0) d2 = -d2; + worst = (d1 > d2) ? d1 : d2; + if (worst == 0) { + o_rc = 0; strcpy(o_msg, "헤더=매입=스냅샷 3원 일치"); + } else if (d1 != 0 && d2 == 0) { + o_rc = 1; strcpy(o_msg, "헤더-매입 불일치(스냅샷은 일치)"); + } else if (d1 == 0 && d2 != 0) { + o_rc = 2; strcpy(o_msg, "헤더-스냅샷 불일치(매입은 일치)"); + } else { + o_rc = 3; strcpy(o_msg, "3원 전부 불일치 - 재마감 검토"); + } + + /* --- 섹션6: 편차 세팅 --- */ + setl(b, T_AMT1, h_hdr); + setl(b, T_AMT2, h_real); + setl(b, T_AMT3, h_snap); + setl(b, T_DELTA, worst); + o_count = (worst == 0) ? 0 : 1; o_amount = worst; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_NETCROSS 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 2000000000L) { + z_sev = 3; + strcpy(z_msg, "초과대형 마감구간"); + } else if (h_zamt >= 200000000L) { + z_sev = 2; + strcpy(z_msg, "중대형 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "일반 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_NETCROSS 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_NETCROSS 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_NETCROSS 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_NETCROSS 정산액크로스 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_NETCROSS 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_NETCROSS 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_NETVERIFY.pgc b/app/src/cl/svc/CL_NETVERIFY.pgc index c852967..bb15cd1 100644 --- a/app/src/cl/svc/CL_NETVERIFY.pgc +++ b/app/src/cl/svc/CL_NETVERIFY.pgc @@ -9,26 +9,168 @@ void CL_NETVERIFY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long dcnt = 0, damt = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid, h_bad, h_gap; - char h_merch[24]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_hdr_cnt, h_hdr_amt; + long h_real_cnt, h_real_amt; + char h_date[16], h_type[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_NETVERIFY"); + userlog("CL_NETVERIFY 진입: 마감처리 시작 (마감 정합 검증)", svc); + h_cid = getl(b, T_ID1); - EXEC SQL SELECT count(*) INTO :h_bad FROM cl_snapshot - WHERE close_id = :h_cid AND gross_amount - fee_amount <> net_amount; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_bad); - if (h_bad == 0) { setl(b, T_RC, 0); OK(b); } - EXEC SQL SELECT merchant_id, gross_amount - fee_amount - net_amount - INTO :h_merch, :h_gap - FROM cl_snapshot - WHERE close_id = :h_cid AND gross_amount - fee_amount <> net_amount - ORDER BY abs(gross_amount - fee_amount - net_amount) DESC LIMIT 1; - if (sqlca.sqlcode < 0) FAIL(b); - Bchg(b, T_MERCHANT, 0, h_merch, 0L); - setl(b, T_DELTA, h_gap); - setl(b, T_RC, 1); - userlog("CL_NETVERIFY cid=%ld net 모순 %ld행 (최대 %s %+ld)", h_cid, h_bad, h_merch, h_gap); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0 && h_bizdate[0] == 0) { + setl(b, T_RC, 5401); + Bchg(b, T_MSG, 0, "마감ID/영업일 중 하나는 필수", 0L); FAIL(b); + } + + /* --- 섹션2: 마감이력 헤더 조회부 --- */ + if (h_cid > 0) { + EXEC SQL SELECT close_type, txn_count, total_amount, + to_char(biz_date,'YYYY-MM-DD') + INTO :h_type, :h_hdr_cnt, :h_hdr_amt, :h_date + FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5402); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5403); FAIL(b); } + } else { + strcpy(h_date, h_bizdate); + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(total_amount),0) + INTO :h_hdr_cnt, :h_hdr_amt FROM cl_close_log + WHERE period_key = :h_bizdate AND status <> 'X'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5404); FAIL(b); } + } + + /* --- 섹션4: 실매입 재집계부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_real_cnt, :h_real_amt + FROM purchase WHERE biz_date = :h_date; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5405); FAIL(b); } + + /* --- 섹션3: 대사 분기부 --- */ + dcnt = h_hdr_cnt - h_real_cnt; if (dcnt < 0) dcnt = -dcnt; + damt = h_hdr_amt - h_real_amt; if (damt < 0) damt = -damt; + if (dcnt == 0 && damt == 0) { + o_rc = 0; strcpy(o_msg, "건수·금액 완전 일치"); + } else if (dcnt == 0 && damt != 0) { + o_rc = 1; strcpy(o_msg, "건수 일치·금액 불일치"); + } else if (dcnt != 0 && damt == 0) { + o_rc = 2; strcpy(o_msg, "건수 불일치·금액 일치"); + } else { + o_rc = 3; strcpy(o_msg, "건수·금액 모두 불일치"); + } + + /* --- 섹션6: 결과 세팅 --- */ + setl(b, T_ARG1, h_hdr_cnt); + setl(b, T_ARG2, h_real_cnt); + setl(b, T_AMT1, h_hdr_amt); + setl(b, T_AMT2, h_real_amt); + setl(b, T_DELTA, damt); + o_count = dcnt; o_amount = damt; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_NETVERIFY 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 2000000000L) { + z_sev = 3; + strcpy(z_msg, "초과대형 마감구간"); + } else if (h_zamt >= 200000000L) { + z_sev = 2; + strcpy(z_msg, "중대형 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "일반 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_NETVERIFY 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_NETVERIFY 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_NETVERIFY 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_NETVERIFY 정합검증 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_NETVERIFY 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_NETVERIFY 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_NEXTBIZ.pgc b/app/src/cl/svc/CL_NEXTBIZ.pgc index 5f3af9a..5959a31 100644 --- a/app/src/cl/svc/CL_NEXTBIZ.pgc +++ b/app/src/cl/svc/CL_NEXTBIZ.pgc @@ -8,21 +8,149 @@ void CL_NEXTBIZ(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long metric = 0; int i = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16], h_next[16]; - long h_skip; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_n1, h_n2, h_n3, h_cid; + char h_key[32], h_status[4], h_out[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_NEXTBIZ"); + userlog("CL_NEXTBIZ 진입: 마감처리 시작 (마감 처리)", svc); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT to_char(d,'YYYY-MM-DD') INTO :h_next FROM ( - SELECT (:h_bizdate::date + s) AS d, - extract(dow from (:h_bizdate::date + s)) AS dw - FROM generate_series(0,7) s) t - WHERE dw NOT IN (0,6) ORDER BY d LIMIT 1; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - EXEC SQL SELECT (:h_next::date - :h_bizdate::date) INTO :h_skip; - if (sqlca.sqlcode < 0) FAIL(b); - Bchg(b, T_KEY1, 0, h_next, 0L); - setl(b, T_ARG1, h_skip); + gets_(b, T_STR1, h_period, sizeof(h_period)); + gets_(b, T_KEY1, h_key, sizeof(h_key)); + h_cid = getl(b, T_ID1); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0 && h_cid <= 0) { + setl(b, T_RC, 6601); + Bchg(b, T_MSG, 0, "기준(영업일/기간/마감ID) 중 하나는 필수", 0L); FAIL(b); + } + + /* --- 섹션2·섹션4: 익영업일 산출(공통유틸) --- */ + if (h_bizdate[0] == 0) { + setl(b, T_RC, 6602); + Bchg(b, T_MSG, 0, "기준 영업일 필수", 0L); FAIL(b); + } + { char cur[16], nxt[16]; int skipped = 0; + strncpy(cur, h_bizdate, sizeof(cur)-1); cur[sizeof(cur)-1]=0; + for (i = 0; i < 10; i++) { + if (acq_next_bizday(cur, nxt) < 0) { + setl(b, T_RC, 6603); + Bchg(b, T_MSG, 0, "영업일 계산 오류", 0L); FAIL(b); + } + skipped++; + strncpy(cur, nxt, sizeof(cur)-1); cur[sizeof(cur)-1]=0; + break; + } + Bchg(b, T_STR1, 0, cur, 0L); + metric = skipped; o_count = skipped; } + strcpy(o_msg, "익영업일 산출 완료"); + o_rc = 0; + + /* --- 섹션3b: 규모 부가판정 --- */ + if (metric < 0) metric = 0; + setl(b, T_ARG5, metric); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_NEXTBIZ 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 2000000000L) { + z_sev = 3; + strcpy(z_msg, "초과대형 마감구간"); + } else if (h_zamt >= 200000000L) { + z_sev = 2; + strcpy(z_msg, "중대형 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "일반 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_NEXTBIZ 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_NEXTBIZ 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_NEXTBIZ 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_NEXTBIZ 마감 처리 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_NEXTBIZ 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_NEXTBIZ 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_PENDLIST.pgc b/app/src/cl/svc/CL_PENDLIST.pgc index f04832b..23b3816 100644 --- a/app/src/cl/svc/CL_PENDLIST.pgc +++ b/app/src/cl/svc/CL_PENDLIST.pgc @@ -9,27 +9,161 @@ void CL_PENDLIST(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long groups = 0; long topamt = 0; char topdim[64]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_pend, h_oldest, h_waitdays; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_grpcnt, h_grpamt, h_cid; + char h_dim[64], h_key[32]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; - EXEC SQL SELECT count(*) INTO :h_pend FROM cl_close_log - WHERE status = 'C' AND memo LIKE '%APPR_REQ%'; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_pend); - if (h_pend == 0) { - setl(b, T_RC, 0); - Bchg(b, T_MSG, 0, "승인 대기 없음", 0L); - OK(b); + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_PENDLIST"); + userlog("CL_PENDLIST 진입: 마감처리 시작 (승인대기)", svc); + + + /* --- 섹션1: 입력 검증부 --- */ + /* 전역 리포트 - 별도 필수입력 없음 */ + + /* --- 섹션2: 그룹 수 사전 확인부 --- */ + EXEC SQL SELECT count(DISTINCT close_type) INTO :h_grpcnt + FROM cl_close_log WHERE status = 'C'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6504); FAIL(b); } + if (h_grpcnt == 0) { + o_rc = 1; o_count = 0; + strcpy(o_msg, "리포트 대상 그룹 없음"); + setl(b, T_COUNT, 0); Bchg(b, T_MSG, 0, o_msg, 0L); OK(b); } - EXEC SQL SELECT close_id, current_date - cast(updated_at as date) - INTO :h_oldest, :h_waitdays + + /* --- 섹션4: 그룹별 집계 커서(상위추출) --- */ + strcpy(topdim, ""); + EXEC SQL DECLARE crpt CURSOR FOR + SELECT close_type, count(*), coalesce(sum(total_amount),0) FROM cl_close_log + WHERE status = 'C' GROUP BY close_type ORDER BY 2 DESC; + EXEC SQL OPEN crpt; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6505); FAIL(b); } + for (;;) { + EXEC SQL FETCH crpt INTO :h_dim, :h_grpcnt, :h_grpamt; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE crpt; setl(b, T_RC, 6506); FAIL(b); } + if (groups < 20) { + Bchg(b, T_STR2, (BFLDOCC)groups, h_dim, 0L); + setl(b, T_ARG3, h_grpamt); + } + if (h_grpamt > topamt) { + topamt = h_grpamt; + strncpy(topdim, h_dim, sizeof(topdim)-1); topdim[sizeof(topdim)-1]=0; + } + groups++; + } + EXEC SQL CLOSE crpt; + + /* --- 섹션3: 상위그룹 판정 분기부 --- */ + if (groups == 0) { + o_rc = 1; strcpy(o_msg, "승인대기 집계 결과 없음"); + } else if (topamt == 0) { + o_rc = 2; strcpy(o_msg, "승인대기 집계 금액 0"); + } else { + o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "승인대기 최다=%s(%ld)", topdim, topamt); + } + Bchg(b, T_STR1, 0, topdim, 0L); + o_count = groups; o_amount = topamt; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt FROM cl_close_log - WHERE status = 'C' AND memo LIKE '%APPR_REQ%' - ORDER BY updated_at ASC LIMIT 1; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_ID1, h_oldest); - setl(b, T_ARG1, h_waitdays); - setl(b, T_RC, (h_waitdays >= 3) ? 2 : 1); + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_PENDLIST 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 2000000000L) { + z_sev = 3; + strcpy(z_msg, "초과대형 마감구간"); + } else if (h_zamt >= 200000000L) { + z_sev = 2; + strcpy(z_msg, "중대형 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "일반 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_PENDLIST 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_PENDLIST 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_PENDLIST 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_PENDLIST 승인대기 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_PENDLIST 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_PENDLIST 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_PERIOD.pgc b/app/src/cl/svc/CL_PERIOD.pgc index 6885702..2e49018 100644 --- a/app/src/cl/svc/CL_PERIOD.pgc +++ b/app/src/cl/svc/CL_PERIOD.pgc @@ -9,16 +9,205 @@ void CL_PERIOD(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], period[16], ctype[16]; - long cid; - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - gets_(b, T_STR1, period, sizeof(period)); if (period[0] == 0) strcpy(period, bizdate); - gets_(b, T_STR2, ctype, sizeof(ctype)); if (ctype[0] == 0) strcpy(ctype, "DAILY"); + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long cid = 0; + long fee_calc = 0; + int grade = 0; + char memo[128]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; + EXEC SQL BEGIN DECLARE SECTION; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cnt, h_amt, h_fee, h_net; + long h_prev, h_bps; + char h_key[32]; + char h_status[4]; + char h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; + EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_PERIOD"); + userlog("CL_PERIOD 진입: 마감처리 시작 (기간 마감 개시 마감)", svc); + + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + gets_(b, T_STR1, h_period, sizeof(h_period)); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + strcpy(h_key, ""); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0) { + setl(b, T_RC, 5001); + Bchg(b, T_MSG, 0, "영업일/기간 미입력 - 기간 마감 개시 마감 불가", 0L); + FAIL(b); + } + if (strlen(h_bizdate) > 0 && strlen(h_bizdate) < 8) { + setl(b, T_RC, 5002); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + FAIL(b); + } + + /* --- 섹션2: 정책/기존마감 상태 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_prev FROM cl_close_log + WHERE close_type = 'PERIOD' AND period_key = :h_period AND status <> 'X'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5010); + Bchg(b, T_MSG, 0, "기존 마감 조회 실패", 0L); FAIL(b); + } + if (h_prev > 0) { + userlog("CL_PERIOD %s 이미 마감 %ld건 존재 - 재집계 진행", h_period, h_prev); + } + EXEC SQL SELECT count(*) INTO :h_bps FROM cl_period_lock + WHERE period_key = :h_period AND locked = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5011); FAIL(b); } + if (h_bps > 0) { + setl(b, T_RC, 5012); + Bchg(b, T_MSG, 0, "기간이 잠금상태 - 마감 불가", 0L); FAIL(b); + } + + /* --- 섹션4: 본처리부(집계) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) + INTO :h_cnt, :h_amt, :h_fee, :h_net + FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5020); + Bchg(b, T_MSG, 0, "매입 집계 실패", 0L); FAIL(b); + } + + /* --- 섹션3: 업무규칙 분기부(규모/등급) --- */ + if (h_cnt == 0) { + grade = 0; + strcpy(memo, "PERIOD:무거래"); + } else if (h_amt >= 1000000000) { + grade = 3; + strcpy(memo, "PERIOD:대규모"); + } else if (h_amt >= 100000000) { + grade = 2; + strcpy(memo, "PERIOD:중규모"); + } else { + grade = 1; + strcpy(memo, "PERIOD:소규모"); + } + fee_calc = acq_fee(h_amt, 25); + if (h_fee == 0 && h_amt > 0) h_fee = fee_calc; + + /* --- 섹션4b: 마감이력 생성 --- */ cid = cldb_next_close_id(); - if (cid < 0) FAIL(b); - if (cldb_insert_close(cid, ctype, period, bizdate, "O", 0, 0, "기간개시") < 0) FAIL(b); + if (cid < 0) { setl(b, T_RC, 5030); FAIL(b); } + if (cldb_insert_close(cid, "PERIOD", h_period, + (h_bizdate[0] ? h_bizdate : h_period), + "C", h_cnt, h_amt, memo) < 0) { + setl(b, T_RC, 5031); + Bchg(b, T_MSG, 0, "마감이력 생성 실패", 0L); FAIL(b); + } + + /* --- 섹션6: 집계 확인/요약 재조회부 --- */ + h_kcid = cid; + EXEC SQL SELECT txn_count, total_amount INTO :h_prev, :h_amt + FROM cl_close_log WHERE close_id = :h_kcid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5040); FAIL(b); } + o_count = h_cnt; o_amount = h_amt; + + /* --- 섹션8prep: 상태/결과 세팅 --- */ setl(b, T_ID1, cid); - Bchg(b, T_STATUS, 0, "O", 0L); + setl(b, T_FEE, h_fee); + setl(b, T_NET, h_net); + Bchg(b, T_STATUS, 0, "C", 0L); + snprintf(o_msg, sizeof(o_msg), "기간 마감 개시 마감완료 등급%d cid=%ld", grade, cid); + o_rc = 0; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_PERIOD 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1000000000L) { + z_sev = 3; + strcpy(z_msg, "대규모 마감기간"); + } else if (h_zamt >= 100000000L) { + z_sev = 2; + strcpy(z_msg, "중규모 마감기간"); + } else { + z_sev = 1; + strcpy(z_msg, "소규모 마감기간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_PERIOD 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_PERIOD 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_PERIOD 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_PERIOD 기간 마감 개시마감 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_PERIOD 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_PERIOD 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_POSCLOSE.pgc b/app/src/cl/svc/CL_POSCLOSE.pgc index 3c31e3d..a47cacd 100644 --- a/app/src/cl/svc/CL_POSCLOSE.pgc +++ b/app/src/cl/svc/CL_POSCLOSE.pgc @@ -8,24 +8,205 @@ void CL_POSCLOSE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long cid; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long cid = 0; + long fee_calc = 0; + int grade = 0; + char memo[128]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; long h_cnt, h_amt, h_fee, h_net; + long h_prev, h_bps; + char h_key[32]; + char h_status[4]; + char h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_POSCLOSE"); + userlog("CL_POSCLOSE 진입: 마감처리 시작 (POS 채널 마감)", svc); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) + gets_(b, T_STR1, h_period, sizeof(h_period)); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + strcpy(h_key, ""); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0) { + setl(b, T_RC, 5001); + Bchg(b, T_MSG, 0, "영업일/기간 미입력 - POS 채널 마감 불가", 0L); + FAIL(b); + } + if (strlen(h_bizdate) > 0 && strlen(h_bizdate) < 8) { + setl(b, T_RC, 5002); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + FAIL(b); + } + + /* --- 섹션2: 정책/기존마감 상태 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_prev FROM cl_close_log + WHERE close_type = 'POS' AND period_key = :h_period AND status <> 'X'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5010); + Bchg(b, T_MSG, 0, "기존 마감 조회 실패", 0L); FAIL(b); + } + if (h_prev > 0) { + userlog("CL_POSCLOSE %s 이미 마감 %ld건 존재 - 재집계 진행", h_period, h_prev); + } + EXEC SQL SELECT count(*) INTO :h_bps FROM cl_period_lock + WHERE period_key = :h_period AND locked = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5011); FAIL(b); } + if (h_bps > 0) { + setl(b, T_RC, 5012); + Bchg(b, T_MSG, 0, "기간이 잠금상태 - 마감 불가", 0L); FAIL(b); + } + + /* --- 섹션4: 본처리부(집계) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) INTO :h_cnt, :h_amt, :h_fee, :h_net FROM purchase WHERE biz_date = :h_bizdate AND channel = 'POS'; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5020); + Bchg(b, T_MSG, 0, "매입 집계 실패", 0L); FAIL(b); + } + + /* --- 섹션3: 업무규칙 분기부(규모/등급) --- */ + if (h_cnt == 0) { + grade = 0; + strcpy(memo, "POS:무거래"); + } else if (h_amt >= 1000000000) { + grade = 3; + strcpy(memo, "POS:대규모"); + } else if (h_amt >= 100000000) { + grade = 2; + strcpy(memo, "POS:중규모"); + } else { + grade = 1; + strcpy(memo, "POS:소규모"); + } + fee_calc = acq_fee(h_amt, 25); + if (h_fee == 0 && h_amt > 0) h_fee = fee_calc; + + /* --- 섹션4b: 마감이력 생성 --- */ cid = cldb_next_close_id(); - if (cid < 0) FAIL(b); - if (cldb_insert_close(cid, "POS", h_bizdate, h_bizdate, "C", h_cnt, h_amt, "POS채널마감") < 0) FAIL(b); + if (cid < 0) { setl(b, T_RC, 5030); FAIL(b); } + if (cldb_insert_close(cid, "POS", h_period, + (h_bizdate[0] ? h_bizdate : h_period), + "C", h_cnt, h_amt, memo) < 0) { + setl(b, T_RC, 5031); + Bchg(b, T_MSG, 0, "마감이력 생성 실패", 0L); FAIL(b); + } + + /* --- 섹션6: 집계 확인/요약 재조회부 --- */ + h_kcid = cid; + EXEC SQL SELECT txn_count, total_amount INTO :h_prev, :h_amt + FROM cl_close_log WHERE close_id = :h_kcid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5040); FAIL(b); } + o_count = h_cnt; o_amount = h_amt; + + /* --- 섹션8prep: 상태/결과 세팅 --- */ setl(b, T_ID1, cid); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_amt); setl(b, T_FEE, h_fee); setl(b, T_NET, h_net); + Bchg(b, T_STATUS, 0, "C", 0L); + snprintf(o_msg, sizeof(o_msg), "POS 채널 마감완료 등급%d cid=%ld", grade, cid); + o_rc = 0; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_POSCLOSE 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 2000000000L) { + z_sev = 3; + strcpy(z_msg, "초과대형 마감구간"); + } else if (h_zamt >= 200000000L) { + z_sev = 2; + strcpy(z_msg, "중대형 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "일반 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_POSCLOSE 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_POSCLOSE 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_POSCLOSE 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_POSCLOSE POS 채널마감 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_POSCLOSE 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_POSCLOSE 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_POSTTRIG.pgc b/app/src/cl/svc/CL_POSTTRIG.pgc index 7eebf80..050a2ae 100644 --- a/app/src/cl/svc/CL_POSTTRIG.pgc +++ b/app/src/cl/svc/CL_POSTTRIG.pgc @@ -9,29 +9,145 @@ void CL_POSTTRIG(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char st[4]; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long metric = 0; int i = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid; - char h_date[16]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_n1, h_n2, h_n3, h_cid; + char h_key[32], h_status[4], h_out[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_POSTTRIG"); + userlog("CL_POSTTRIG 진입: 마감처리 시작 (마감 후속작업 트리거: 마감(C/A) 확인 후 해당 영업일의 정산건을 ledger 에)", svc); + + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + gets_(b, T_STR1, h_period, sizeof(h_period)); + gets_(b, T_KEY1, h_key, sizeof(h_key)); h_cid = getl(b, T_ID1); - if (cldb_get_close_status(h_cid, st) < 0) FAIL(b); - if (st[0] != 'C' && st[0] != 'A') { - Bchg(b, T_MSG, 0, "마감완료(C)/승인(A) 후에만 후속작업 가능", 0L); - FAIL(b); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0 && h_cid <= 0) { + setl(b, T_RC, 6601); + Bchg(b, T_MSG, 0, "기준(영업일/기간/마감ID) 중 하나는 필수", 0L); FAIL(b); } - EXEC SQL SELECT to_char(biz_date,'YYYY-MM-DD') INTO :h_date - FROM cl_close_log WHERE close_id = :h_cid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) - SELECT nextval('ledger_seq'), s.purchase_id, 'XFER', s.net, s.biz_date - FROM settlement s - WHERE s.biz_date = :h_date - AND NOT EXISTS (SELECT 1 FROM ledger l - WHERE l.purchase_id = s.purchase_id AND l.entry_type = 'XFER'); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, sqlca.sqlerrd[2]); - userlog("CL_POSTTRIG cid=%ld XFER %ld건 전기", h_cid, (long)sqlca.sqlerrd[2]); + + /* --- 섹션2·섹션4: 마감 후속 트리거 대상 산출 --- */ + if (h_bizdate[0] == 0) { + setl(b, T_RC, 6620); + Bchg(b, T_MSG, 0, "기준 영업일 필수", 0L); FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_n1, :h_n2 + FROM purchase WHERE biz_date = :h_bizdate AND status = 'S'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6621); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_n3 FROM ledger + WHERE biz_date = :h_bizdate AND entry_type = 'SETTLE'; + if (sqlca.sqlcode < 0) h_n3 = 0; + metric = h_n1; o_count = h_n1; o_amount = h_n2; + if (h_n1 == 0) { o_rc = 1; strcpy(o_msg, "후속 대상 없음"); } + else if (h_n3 >= h_n1) { o_rc = 0; strcpy(o_msg, "후속 전기 완료상태"); } + else { o_rc = 2; strcpy(o_msg, "후속 전기 대기 존재"); } + setl(b, T_ARG1, h_n3); + + /* --- 섹션3b: 규모 부가판정 --- */ + if (metric < 0) metric = 0; + setl(b, T_ARG5, metric); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_POSTTRIG 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1000000000L) { + z_sev = 3; + strcpy(z_msg, "대규모 마감기간"); + } else if (h_zamt >= 100000000L) { + z_sev = 2; + strcpy(z_msg, "중규모 마감기간"); + } else { + z_sev = 1; + strcpy(z_msg, "소규모 마감기간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_POSTTRIG 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_POSTTRIG 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_POSTTRIG 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_POSTTRIG 마감 후속작업 트리거: 마감(C/A) 확인 후 해당 영업일의 정산건을 ledger 에 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_POSTTRIG 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_POSTTRIG 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_PRECHECK.pgc b/app/src/cl/svc/CL_PRECHECK.pgc index 889f2f9..c2cbf98 100644 --- a/app/src/cl/svc/CL_PRECHECK.pgc +++ b/app/src/cl/svc/CL_PRECHECK.pgc @@ -10,37 +10,155 @@ void CL_PRECHECK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; long mask = 0; - char msg[128]; + long blockers = 0; + int over = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_m, h_um, h_lk, h_dc; char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_n1, h_n2, h_n3, h_n4; + long h_lim; + char h_mer[16]; + char h_status[4]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_PRECHECK"); + userlog("CL_PRECHECK 진입: 마감처리 시작 (체크리스트 점검)", svc); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT count(*) INTO :h_m FROM purchase - WHERE biz_date = :h_bizdate AND status = 'M'; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_m > 0) mask |= 1; - EXEC SQL SELECT count(*) INTO :h_um FROM ac_unmatch WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_um > 0) mask |= 2; - EXEC SQL SELECT count(*) INTO :h_lk FROM cl_period_lock - WHERE period_key = :h_bizdate AND locked = true; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_lk > 0) mask |= 4; - EXEC SQL SELECT count(*) INTO :h_dc FROM cl_close_log + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0) { + setl(b, T_RC, 5311); + Bchg(b, T_MSG, 0, "영업일 미입력 - 체크리스트 점검 불가", 0L); + FAIL(b); + } + if (strlen(h_bizdate) < 8) { + setl(b, T_RC, 5411); + Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); FAIL(b); + } + + /* --- 섹션2: 기준/정책 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_n4 FROM cl_close_log WHERE close_type = 'DAILY' AND period_key = :h_bizdate AND status <> 'X'; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_dc > 0) mask |= 8; - msg[0] = 0; - if (mask & 1) strcat(msg, "[미마감M]"); - if (mask & 2) strcat(msg, "[미매칭]"); - if (mask & 4) strcat(msg, "[기간잠금]"); - if (mask & 8) strcat(msg, "[기마감]"); - if (mask == 0) strcpy(msg, "체크리스트 전항목 통과"); - setl(b, T_RC, mask); - setl(b, T_COUNT, h_m + h_um); - Bchg(b, T_MSG, 0, msg, 0L); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5312); FAIL(b); } + + /* --- 섹션3·섹션4: 체크리스트 4항목 비트마스크 --- */ + EXEC SQL SELECT count(*) INTO :h_n1 FROM purchase + WHERE biz_date = :h_bizdate AND status = 'M'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5313); FAIL(b); } + if (h_n1 > 0) mask |= 1; + EXEC SQL SELECT count(*) INTO :h_n2 FROM ac_unmatch WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5314); FAIL(b); } + if (h_n2 > 0) mask |= 2; + EXEC SQL SELECT count(*) INTO :h_n3 FROM cl_period_lock + WHERE period_key = :h_bizdate AND locked = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5315); FAIL(b); } + if (h_n3 > 0) mask |= 4; + if (h_n4 > 0) mask |= 8; + o_msg[0] = 0; + if (mask & 1) strcat(o_msg, "[미마감M]"); + if (mask & 2) strcat(o_msg, "[미매칭]"); + if (mask & 4) strcat(o_msg, "[기간잠금]"); + if (mask & 8) strcat(o_msg, "[기마감]"); + if (mask == 0) strcpy(o_msg, "체크리스트 전항목 통과"); + o_rc = mask; o_count = h_n1 + h_n2; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_PRECHECK 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 500000000L) { + z_sev = 3; + strcpy(z_msg, "고액 마감구간"); + } else if (h_zamt >= 50000000L) { + z_sev = 2; + strcpy(z_msg, "중액 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "소액 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_PRECHECK 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_PRECHECK 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_PRECHECK 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_PRECHECK 체크리스트점검 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_PRECHECK 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_PRECHECK 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_PROVCLOSE.pgc b/app/src/cl/svc/CL_PROVCLOSE.pgc index 1596f1a..d4e187f 100644 --- a/app/src/cl/svc/CL_PROVCLOSE.pgc +++ b/app/src/cl/svc/CL_PROVCLOSE.pgc @@ -9,33 +9,205 @@ void CL_PROVCLOSE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long cid; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long cid = 0; + long fee_calc = 0; + int grade = 0; + char memo[128]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_dup, h_cnt, h_amt; char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cnt, h_amt, h_fee, h_net; + long h_prev, h_bps; + char h_key[32]; + char h_status[4]; + char h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_PROVCLOSE"); + userlog("CL_PROVCLOSE 진입: 마감처리 시작 (가마감 마감)", svc); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT count(*) INTO :h_dup FROM cl_close_log - WHERE close_type = 'DAILY' AND period_key = :h_bizdate - AND status = 'O' AND memo = 'PROV'; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_dup > 0) { - setl(b, T_RC, 1); - Bchg(b, T_MSG, 0, "가마감 중복: 미확정 가마감 존재", 0L); + gets_(b, T_STR1, h_period, sizeof(h_period)); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + strcpy(h_key, ""); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0) { + setl(b, T_RC, 5001); + Bchg(b, T_MSG, 0, "영업일/기간 미입력 - 가마감 마감 불가", 0L); FAIL(b); } - EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_amt - FROM purchase WHERE biz_date = :h_bizdate AND status IN ('M','S'); - if (sqlca.sqlcode < 0) FAIL(b); + if (strlen(h_bizdate) > 0 && strlen(h_bizdate) < 8) { + setl(b, T_RC, 5002); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + FAIL(b); + } + + /* --- 섹션2: 정책/기존마감 상태 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_prev FROM cl_close_log + WHERE close_type = 'PROV' AND period_key = :h_period AND status <> 'X'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5010); + Bchg(b, T_MSG, 0, "기존 마감 조회 실패", 0L); FAIL(b); + } + if (h_prev > 0) { + userlog("CL_PROVCLOSE %s 이미 마감 %ld건 존재 - 재집계 진행", h_period, h_prev); + } + EXEC SQL SELECT count(*) INTO :h_bps FROM cl_period_lock + WHERE period_key = :h_period AND locked = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5011); FAIL(b); } + if (h_bps > 0) { + setl(b, T_RC, 5012); + Bchg(b, T_MSG, 0, "기간이 잠금상태 - 마감 불가", 0L); FAIL(b); + } + + /* --- 섹션4: 본처리부(집계) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) + INTO :h_cnt, :h_amt, :h_fee, :h_net + FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5020); + Bchg(b, T_MSG, 0, "매입 집계 실패", 0L); FAIL(b); + } + + /* --- 섹션3: 업무규칙 분기부(규모/등급) --- */ + if (h_cnt == 0) { + grade = 0; + strcpy(memo, "PROV:무거래"); + } else if (h_amt >= 1000000000) { + grade = 3; + strcpy(memo, "PROV:대규모"); + } else if (h_amt >= 100000000) { + grade = 2; + strcpy(memo, "PROV:중규모"); + } else { + grade = 1; + strcpy(memo, "PROV:소규모"); + } + fee_calc = acq_fee(h_amt, 25); + if (h_fee == 0 && h_amt > 0) h_fee = fee_calc; + + /* --- 섹션4b: 마감이력 생성 --- */ cid = cldb_next_close_id(); - if (cid < 0) FAIL(b); - if (cldb_insert_close(cid, "DAILY", h_bizdate, h_bizdate, "O", - h_cnt, h_amt, "PROV") < 0) FAIL(b); + if (cid < 0) { setl(b, T_RC, 5030); FAIL(b); } + if (cldb_insert_close(cid, "PROV", h_period, + (h_bizdate[0] ? h_bizdate : h_period), + "C", h_cnt, h_amt, memo) < 0) { + setl(b, T_RC, 5031); + Bchg(b, T_MSG, 0, "마감이력 생성 실패", 0L); FAIL(b); + } + + /* --- 섹션6: 집계 확인/요약 재조회부 --- */ + h_kcid = cid; + EXEC SQL SELECT txn_count, total_amount INTO :h_prev, :h_amt + FROM cl_close_log WHERE close_id = :h_kcid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5040); FAIL(b); } + o_count = h_cnt; o_amount = h_amt; + + /* --- 섹션8prep: 상태/결과 세팅 --- */ setl(b, T_ID1, cid); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_amt); - Bchg(b, T_STATUS, 0, "O", 0L); - userlog("CL_PROVCLOSE cid=%ld %s 가마감 생성", cid, h_bizdate); + setl(b, T_FEE, h_fee); + setl(b, T_NET, h_net); + Bchg(b, T_STATUS, 0, "C", 0L); + snprintf(o_msg, sizeof(o_msg), "가마감 마감완료 등급%d cid=%ld", grade, cid); + o_rc = 0; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_PROVCLOSE 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 2000000000L) { + z_sev = 3; + strcpy(z_msg, "초과대형 마감구간"); + } else if (h_zamt >= 200000000L) { + z_sev = 2; + strcpy(z_msg, "중대형 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "일반 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_PROVCLOSE 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_PROVCLOSE 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_PROVCLOSE 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_PROVCLOSE 가마감마감 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_PROVCLOSE 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_PROVCLOSE 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_PROVCMP.pgc b/app/src/cl/svc/CL_PROVCMP.pgc index e3abb37..bbca62f 100644 --- a/app/src/cl/svc/CL_PROVCMP.pgc +++ b/app/src/cl/svc/CL_PROVCMP.pgc @@ -8,23 +8,144 @@ void CL_PROVCMP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long delta; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long metric = 0; int i = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; - long h_prov, h_main; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_n1, h_n2, h_n3, h_cid; + char h_key[32], h_status[4], h_out[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_PROVCMP"); + userlog("CL_PROVCMP 진입: 마감처리 시작 (가마감/본마감 비교: 동일 영업일의 가마감(TENT) 잠정 집계와 확정(DAILY C) 차이 산출.)", svc); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT coalesce(sum(total_amount),0) INTO :h_prov FROM cl_close_log - WHERE close_type = 'TENT' AND period_key = :h_bizdate AND status <> 'X'; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT coalesce(sum(total_amount),0) INTO :h_main FROM cl_close_log - WHERE close_type = 'DAILY' AND period_key = :h_bizdate AND status = 'C'; - if (sqlca.sqlcode < 0) FAIL(b); - delta = h_main - h_prov; if (delta < 0) delta = -delta; - setl(b, T_AMT1, h_prov); - setl(b, T_AMT2, h_main); - setl(b, T_DELTA, delta); - setl(b, T_RC, (delta == 0) ? 0 : 1); + gets_(b, T_STR1, h_period, sizeof(h_period)); + gets_(b, T_KEY1, h_key, sizeof(h_key)); + h_cid = getl(b, T_ID1); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0 && h_cid <= 0) { + setl(b, T_RC, 6601); + Bchg(b, T_MSG, 0, "기준(영업일/기간/마감ID) 중 하나는 필수", 0L); FAIL(b); + } + + /* --- 섹션2·섹션4: 마감율/일정/SLA 산출 --- */ + EXEC SQL SELECT count(DISTINCT biz_date) INTO :h_n1 FROM purchase + WHERE to_char(biz_date,'YYYY-MM') = substr(:h_period,1,7); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6623); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_n2 FROM cl_close_log + WHERE close_type = 'DAILY' AND status <> 'X' + AND substr(period_key,1,7) = substr(:h_period,1,7); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6624); FAIL(b); } + metric = (h_n1 == 0) ? 0 : (h_n2 * 100 / h_n1); + o_count = h_n2; o_amount = h_n1; + if (h_n1 == 0) { o_rc = 1; strcpy(o_msg, "대상 영업일 없음"); } + else if (metric >= 100) { o_rc = 0; strcpy(o_msg, "마감 100% 완료"); } + else if (metric >= 50) { o_rc = 2; strcpy(o_msg, "마감 진행중"); } + else { o_rc = 3; strcpy(o_msg, "마감 지연"); } + setl(b, T_ARG1, metric); + + /* --- 섹션3b: 규모 부가판정 --- */ + if (metric < 0) metric = 0; + setl(b, T_ARG5, metric); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_PROVCMP 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1000000000L) { + z_sev = 3; + strcpy(z_msg, "대규모 마감기간"); + } else if (h_zamt >= 100000000L) { + z_sev = 2; + strcpy(z_msg, "중규모 마감기간"); + } else { + z_sev = 1; + strcpy(z_msg, "소규모 마감기간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_PROVCMP 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_PROVCMP 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_PROVCMP 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_PROVCMP 가마감/본마감 비교: 동일 영업일의 가마감(TENT) 잠정 집계와 확정(DAILY C) 차이 산출. cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_PROVCMP 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_PROVCMP 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_PURGE.pgc b/app/src/cl/svc/CL_PURGE.pgc index 3b2edd5..8ec283b 100644 --- a/app/src/cl/svc/CL_PURGE.pgc +++ b/app/src/cl/svc/CL_PURGE.pgc @@ -9,21 +9,143 @@ void CL_PURGE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + char st = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; - long h_cnt; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_cnt, h_n; + char h_status[4], h_reason[64], h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL DELETE FROM cl_snapshot WHERE close_id IN - (SELECT close_id FROM cl_close_log WHERE status = 'X' AND biz_date < :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL DELETE FROM cl_close_log WHERE status = 'X' AND biz_date < :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - h_cnt = sqlca.sqlerrd[2]; - setl(b, T_COUNT, h_cnt); - userlog("CL_PURGE %s 이전 취소마감 %ld건 삭제", bizdate, h_cnt); + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_PURGE"); + userlog("CL_PURGE 진입: 마감처리 시작 (취소마감 정리)", svc); + + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0) { + setl(b, T_RC, 5701); + Bchg(b, T_MSG, 0, "정리 기준일 미입력", 0L); FAIL(b); + } + + /* --- 섹션2: 정리 대상 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_n FROM cl_close_log + WHERE status = 'X' AND period_key < :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5702); FAIL(b); } + if (h_n == 0) { + o_rc = 0; o_count = 0; + strcpy(o_msg, "정리 대상 취소마감 없음"); + setl(b, T_COUNT, 0); Bchg(b, T_MSG, 0, o_msg, 0L); OK(b); + } + + /* --- 섹션4: 스냅샷 선삭제 후 헤더 삭제 --- */ + EXEC SQL DELETE FROM cl_snapshot WHERE close_id IN ( + SELECT close_id FROM cl_close_log + WHERE status = 'X' AND period_key < :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5703); + Bchg(b, T_MSG, 0, "스냅샷 정리 실패", 0L); FAIL(b); } + EXEC SQL DELETE FROM cl_close_log + WHERE status = 'X' AND period_key < :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5704); + Bchg(b, T_MSG, 0, "취소마감 정리 실패", 0L); FAIL(b); } + o_count = sqlca.sqlerrd[2]; o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "취소마감 %ld건 정리 완료", o_count); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_PURGE 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 500000000L) { + z_sev = 3; + strcpy(z_msg, "고액 마감구간"); + } else if (h_zamt >= 50000000L) { + z_sev = 2; + strcpy(z_msg, "중액 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "소액 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_PURGE 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_PURGE 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_PURGE 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_PURGE 정리 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_PURGE 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_PURGE 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_QTRVERIFY.pgc b/app/src/cl/svc/CL_QTRVERIFY.pgc index a7d9ef1..e20d97b 100644 --- a/app/src/cl/svc/CL_QTRVERIFY.pgc +++ b/app/src/cl/svc/CL_QTRVERIFY.pgc @@ -8,24 +8,168 @@ void CL_QTRVERIFY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long da; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long dcnt = 0, damt = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; - long h_la, h_ra; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_hdr_cnt, h_hdr_amt; + long h_real_cnt, h_real_amt; + char h_date[16], h_type[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_QTRVERIFY"); + userlog("CL_QTRVERIFY 진입: 마감처리 시작 (마감 정합 검증)", svc); + + h_cid = getl(b, T_ID1); gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT coalesce(sum(total_amount),0) INTO :h_la FROM cl_close_log - WHERE close_type = 'QUARTER' - AND period_key = to_char(:h_bizdate::date, 'YYYY"Q"Q') AND status <> 'X'; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_ra FROM purchase - WHERE to_char(biz_date,'YYYY"Q"Q') = to_char(:h_bizdate::date,'YYYY"Q"Q'); - if (sqlca.sqlcode < 0) FAIL(b); - da = h_la - h_ra; if (da < 0) da = -da; - setl(b, T_AMT1, h_la); - setl(b, T_AMT2, h_ra); - setl(b, T_DELTA, da); - setl(b, T_RC, (da == 0) ? 0 : 1); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0 && h_bizdate[0] == 0) { + setl(b, T_RC, 5401); + Bchg(b, T_MSG, 0, "마감ID/영업일 중 하나는 필수", 0L); FAIL(b); + } + + /* --- 섹션2: 마감이력 헤더 조회부 --- */ + if (h_cid > 0) { + EXEC SQL SELECT close_type, txn_count, total_amount, + to_char(biz_date,'YYYY-MM-DD') + INTO :h_type, :h_hdr_cnt, :h_hdr_amt, :h_date + FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5402); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5403); FAIL(b); } + } else { + strcpy(h_date, h_bizdate); + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(total_amount),0) + INTO :h_hdr_cnt, :h_hdr_amt FROM cl_close_log + WHERE period_key = :h_bizdate AND status <> 'X'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5404); FAIL(b); } + } + + /* --- 섹션4: 실매입 재집계부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_real_cnt, :h_real_amt + FROM purchase WHERE biz_date = :h_date; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5405); FAIL(b); } + + /* --- 섹션3: 대사 분기부 --- */ + dcnt = h_hdr_cnt - h_real_cnt; if (dcnt < 0) dcnt = -dcnt; + damt = h_hdr_amt - h_real_amt; if (damt < 0) damt = -damt; + if (dcnt == 0 && damt == 0) { + o_rc = 0; strcpy(o_msg, "건수·금액 완전 일치"); + } else if (dcnt == 0 && damt != 0) { + o_rc = 1; strcpy(o_msg, "건수 일치·금액 불일치"); + } else if (dcnt != 0 && damt == 0) { + o_rc = 2; strcpy(o_msg, "건수 불일치·금액 일치"); + } else { + o_rc = 3; strcpy(o_msg, "건수·금액 모두 불일치"); + } + + /* --- 섹션6: 결과 세팅 --- */ + setl(b, T_ARG1, h_hdr_cnt); + setl(b, T_ARG2, h_real_cnt); + setl(b, T_AMT1, h_hdr_amt); + setl(b, T_AMT2, h_real_amt); + setl(b, T_DELTA, damt); + o_count = dcnt; o_amount = damt; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_QTRVERIFY 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1000000000L) { + z_sev = 3; + strcpy(z_msg, "대규모 마감기간"); + } else if (h_zamt >= 100000000L) { + z_sev = 2; + strcpy(z_msg, "중규모 마감기간"); + } else { + z_sev = 1; + strcpy(z_msg, "소규모 마감기간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_QTRVERIFY 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_QTRVERIFY 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_QTRVERIFY 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_QTRVERIFY 정합검증 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_QTRVERIFY 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_QTRVERIFY 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_QUARTER.pgc b/app/src/cl/svc/CL_QUARTER.pgc index 6b2f86e..b6d8116 100644 --- a/app/src/cl/svc/CL_QUARTER.pgc +++ b/app/src/cl/svc/CL_QUARTER.pgc @@ -9,13 +9,205 @@ void CL_QUARTER(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], period[16]; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; long cid = 0; - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - gets_(b, T_STR1, period, sizeof(period)); - if (period[0] == 0) { strncpy(period, bizdate, 4); period[4] = 0; } - if (cl_open_close(b, "QUARTER", period, bizdate, &cid) < 0) FAIL(b); + long fee_calc = 0; + int grade = 0; + char memo[128]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; + EXEC SQL BEGIN DECLARE SECTION; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cnt, h_amt, h_fee, h_net; + long h_prev, h_bps; + char h_key[32]; + char h_status[4]; + char h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; + EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_QUARTER"); + userlog("CL_QUARTER 진입: 마감처리 시작 (분기 마감)", svc); + + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + gets_(b, T_STR1, h_period, sizeof(h_period)); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + strcpy(h_key, ""); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0) { + setl(b, T_RC, 5001); + Bchg(b, T_MSG, 0, "영업일/기간 미입력 - 분기 마감 불가", 0L); + FAIL(b); + } + if (strlen(h_bizdate) > 0 && strlen(h_bizdate) < 8) { + setl(b, T_RC, 5002); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + FAIL(b); + } + + /* --- 섹션2: 정책/기존마감 상태 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_prev FROM cl_close_log + WHERE close_type = 'QUARTER' AND period_key = :h_period AND status <> 'X'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5010); + Bchg(b, T_MSG, 0, "기존 마감 조회 실패", 0L); FAIL(b); + } + if (h_prev > 0) { + userlog("CL_QUARTER %s 이미 마감 %ld건 존재 - 재집계 진행", h_period, h_prev); + } + EXEC SQL SELECT count(*) INTO :h_bps FROM cl_period_lock + WHERE period_key = :h_period AND locked = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5011); FAIL(b); } + if (h_bps > 0) { + setl(b, T_RC, 5012); + Bchg(b, T_MSG, 0, "기간이 잠금상태 - 마감 불가", 0L); FAIL(b); + } + + /* --- 섹션4: 본처리부(집계) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) + INTO :h_cnt, :h_amt, :h_fee, :h_net + FROM purchase WHERE to_char(biz_date,'YYYY') = substr(:h_period,1,4); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5020); + Bchg(b, T_MSG, 0, "매입 집계 실패", 0L); FAIL(b); + } + + /* --- 섹션3: 업무규칙 분기부(규모/등급) --- */ + if (h_cnt == 0) { + grade = 0; + strcpy(memo, "QUARTER:무거래"); + } else if (h_amt >= 1000000000) { + grade = 3; + strcpy(memo, "QUARTER:대규모"); + } else if (h_amt >= 100000000) { + grade = 2; + strcpy(memo, "QUARTER:중규모"); + } else { + grade = 1; + strcpy(memo, "QUARTER:소규모"); + } + fee_calc = acq_fee(h_amt, 25); + if (h_fee == 0 && h_amt > 0) h_fee = fee_calc; + + /* --- 섹션4b: 마감이력 생성 --- */ + cid = cldb_next_close_id(); + if (cid < 0) { setl(b, T_RC, 5030); FAIL(b); } + if (cldb_insert_close(cid, "QUARTER", h_period, + (h_bizdate[0] ? h_bizdate : h_period), + "C", h_cnt, h_amt, memo) < 0) { + setl(b, T_RC, 5031); + Bchg(b, T_MSG, 0, "마감이력 생성 실패", 0L); FAIL(b); + } + + /* --- 섹션6: 집계 확인/요약 재조회부 --- */ + h_kcid = cid; + EXEC SQL SELECT txn_count, total_amount INTO :h_prev, :h_amt + FROM cl_close_log WHERE close_id = :h_kcid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5040); FAIL(b); } + o_count = h_cnt; o_amount = h_amt; + + /* --- 섹션8prep: 상태/결과 세팅 --- */ setl(b, T_ID1, cid); + setl(b, T_FEE, h_fee); + setl(b, T_NET, h_net); + Bchg(b, T_STATUS, 0, "C", 0L); + snprintf(o_msg, sizeof(o_msg), "분기 마감완료 등급%d cid=%ld", grade, cid); + o_rc = 0; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_QUARTER 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 500000000L) { + z_sev = 3; + strcpy(z_msg, "고액 마감구간"); + } else if (h_zamt >= 50000000L) { + z_sev = 2; + strcpy(z_msg, "중액 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "소액 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_QUARTER 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_QUARTER 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_QUARTER 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_QUARTER 분기마감 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_QUARTER 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_QUARTER 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_RECLOSE.pgc b/app/src/cl/svc/CL_RECLOSE.pgc index 2fbe126..2369dc4 100644 --- a/app/src/cl/svc/CL_RECLOSE.pgc +++ b/app/src/cl/svc/CL_RECLOSE.pgc @@ -9,21 +9,169 @@ void CL_RECLOSE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + char st = 0; + long newcid = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid, h_cnt, h_amt; char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_cnt, h_amt, h_newcid; + char h_status[4], h_type[16], h_pdate[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_RECLOSE"); + userlog("CL_RECLOSE 진입: 마감처리 시작 (재마감)", svc); + h_cid = getl(b, T_ID1); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_amt - FROM purchase WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - if (cldb_update_close_totals(h_cid, h_cnt, h_amt) < 0) FAIL(b); - if (cldb_update_close_status(h_cid, "R") < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0) { + setl(b, T_RC, 5801); + Bchg(b, T_MSG, 0, "재마감 대상 마감ID 필수", 0L); FAIL(b); + } + + /* --- 섹션2: 대상 마감 조회부 --- */ + EXEC SQL SELECT status, close_type, period_key, + to_char(biz_date,'YYYY-MM-DD') + INTO :h_status, :h_type, :h_period, :h_pdate + FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5802); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5803); FAIL(b); } + st = h_status[0]; + + /* --- 섹션3: 재마감 가능여부 분기부 --- */ + if (st == 'A' || st == 'L') { + setl(b, T_RC, 5804); + Bchg(b, T_MSG, 0, "승인/잠금 상태 - 재마감 불가", 0L); FAIL(b); + } + if (st == 'X') { + setl(b, T_RC, 5805); + Bchg(b, T_MSG, 0, "취소 상태 - 복원 선행 필요", 0L); FAIL(b); + } + + /* --- 섹션4: 매입 재집계 --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_cnt, :h_amt + FROM purchase WHERE biz_date = :h_pdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5806); FAIL(b); } + if (cldb_update_close_totals(h_cid, h_cnt, h_amt) < 0) { + setl(b, T_RC, 5807); + Bchg(b, T_MSG, 0, "총계 갱신 실패", 0L); FAIL(b); + } + + /* --- 섹션4b: 상태 R 표기 --- */ + if (cldb_update_close_status(h_cid, "R") < 0) { + setl(b, T_RC, 5808); + Bchg(b, T_MSG, 0, "재마감 상태전이 실패", 0L); FAIL(b); + } Bchg(b, T_STATUS, 0, "R", 0L); + o_count = h_cnt; o_amount = h_amt; o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "재마감 완료 cnt=%ld amt=%ld", h_cnt, h_amt); + + /* --- 섹션6: 재마감 후 헤더 재확인 --- */ + EXEC SQL SELECT txn_count, total_amount INTO :h_cnt, :h_amt + FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5811); FAIL(b); } + setl(b, T_ID1, h_cid); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_RECLOSE 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1500000000L) { + z_sev = 3; + strcpy(z_msg, "광역 마감규모"); + } else if (h_zamt >= 150000000L) { + z_sev = 2; + strcpy(z_msg, "표준 마감규모"); + } else { + z_sev = 1; + strcpy(z_msg, "경량 마감규모"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_RECLOSE 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_RECLOSE 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_RECLOSE 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_RECLOSE 재마감 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_RECLOSE 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_RECLOSE 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_RECLOSECHK.pgc b/app/src/cl/svc/CL_RECLOSECHK.pgc index 7c4cda1..495ac7c 100644 --- a/app/src/cl/svc/CL_RECLOSECHK.pgc +++ b/app/src/cl/svc/CL_RECLOSECHK.pgc @@ -8,14 +8,150 @@ void CL_RECLOSECHK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char st[2]; - int allowed; - long cid = getl(b, T_ID1); - if (cldb_get_close_status(cid, st) < 0) FAIL(b); - allowed = (st[0] == 'C' || st[0] == 'R') ? 1 : 0; - setl(b, T_RC, allowed ? 0 : 1); - Bchg(b, T_STATUS, 0, st, 0L); - Bchg(b, T_MSG, 0, allowed ? "재마감 가능" : "승인/잠금-재마감 불가", 0L); + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long mask = 0; + long blockers = 0; + int over = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; + EXEC SQL BEGIN DECLARE SECTION; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_n1, h_n2, h_n3, h_n4; + long h_lim; + char h_mer[16]; + char h_status[4]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; + EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_RECLOSECHK"); + userlog("CL_RECLOSECHK 진입: 마감처리 시작 (재마감 가능여부 점검)", svc); + + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0) { + setl(b, T_RC, 5312); + Bchg(b, T_MSG, 0, "영업일 미입력 - 재마감 가능여부 점검 불가", 0L); + FAIL(b); + } + if (strlen(h_bizdate) < 8) { + setl(b, T_RC, 5412); + Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); FAIL(b); + } + + /* --- 섹션2: 기준/정책 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_n4 FROM cl_close_log + WHERE close_type = 'DAILY' AND period_key = :h_bizdate AND status <> 'X'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5313); FAIL(b); } + + /* --- 섹션3·섹션4: 재마감 가능여부 판정 --- */ + { long cid = getl(b, T_ID1); char st[4]; + h_kcid = cid; + EXEC SQL SELECT status INTO :h_status FROM cl_close_log WHERE close_id = :h_kcid; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 5314); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5315); FAIL(b); } + st[0] = h_status[0]; st[1] = 0; + if (st[0] == 'A' || st[0] == 'L') { + o_rc = 1; strcpy(o_msg, "승인/잠금 상태 - 재마감 불가"); + } else if (st[0] == 'X') { + o_rc = 2; strcpy(o_msg, "취소 상태 - 복원 후 재마감"); + } else { + o_rc = 0; strcpy(o_msg, "재마감 가능"); + } } + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_RECLOSECHK 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 500000000L) { + z_sev = 3; + strcpy(z_msg, "고액 마감구간"); + } else if (h_zamt >= 50000000L) { + z_sev = 2; + strcpy(z_msg, "중액 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "소액 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_RECLOSECHK 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_RECLOSECHK 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_RECLOSECHK 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_RECLOSECHK 재마감 가능여부점검 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_RECLOSECHK 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_RECLOSECHK 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_RECLOSEREG.pgc b/app/src/cl/svc/CL_RECLOSEREG.pgc index 285a81e..0307396 100644 --- a/app/src/cl/svc/CL_RECLOSEREG.pgc +++ b/app/src/cl/svc/CL_RECLOSEREG.pgc @@ -8,26 +8,176 @@ void CL_RECLOSEREG(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long newid; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + char st = 0; + long newcid = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - char h_ct[16], h_pk[24], h_date[16]; - long h_old, h_cnt, h_amt; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_cnt, h_amt, h_newcid; + char h_status[4], h_type[16], h_pdate[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; - h_old = getl(b, T_ID1); - EXEC SQL SELECT close_type, period_key, to_char(biz_date,'YYYY-MM-DD') - INTO :h_ct, :h_pk, :h_date FROM cl_close_log WHERE close_id = :h_old; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - if (cldb_update_close_status(h_old, "R") < 0) FAIL(b); - EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_amt - FROM purchase WHERE biz_date = :h_date; - if (sqlca.sqlcode < 0) FAIL(b); - newid = cldb_next_close_id(); - if (newid < 0) FAIL(b); - if (cldb_insert_close(newid, h_ct, h_pk, h_date, "C", h_cnt, h_amt, "재마감") < 0) FAIL(b); - setl(b, T_ID1, newid); - setl(b, T_ID2, h_old); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_amt); + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_RECLOSEREG"); + userlog("CL_RECLOSEREG 진입: 마감처리 시작 (재마감)", svc); + + h_cid = getl(b, T_ID1); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0) { + setl(b, T_RC, 5801); + Bchg(b, T_MSG, 0, "재마감 대상 마감ID 필수", 0L); FAIL(b); + } + + /* --- 섹션2: 대상 마감 조회부 --- */ + EXEC SQL SELECT status, close_type, period_key, + to_char(biz_date,'YYYY-MM-DD') + INTO :h_status, :h_type, :h_period, :h_pdate + FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5802); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5803); FAIL(b); } + st = h_status[0]; + + /* --- 섹션3: 재마감 가능여부 분기부 --- */ + if (st == 'A' || st == 'L') { + setl(b, T_RC, 5804); + Bchg(b, T_MSG, 0, "승인/잠금 상태 - 재마감 불가", 0L); FAIL(b); + } + if (st == 'X') { + setl(b, T_RC, 5805); + Bchg(b, T_MSG, 0, "취소 상태 - 복원 선행 필요", 0L); FAIL(b); + } + + /* --- 섹션4: 매입 재집계 --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_cnt, :h_amt + FROM purchase WHERE biz_date = :h_pdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5806); FAIL(b); } + if (cldb_update_close_totals(h_cid, h_cnt, h_amt) < 0) { + setl(b, T_RC, 5807); + Bchg(b, T_MSG, 0, "총계 갱신 실패", 0L); FAIL(b); + } + + /* --- 섹션4b: 기존 R 표기 + 신규 마감 개시 --- */ + if (cldb_update_close_status(h_cid, "R") < 0) { + setl(b, T_RC, 5808); FAIL(b); + } + newcid = cldb_next_close_id(); + if (newcid < 0) { setl(b, T_RC, 5809); FAIL(b); } + if (cldb_insert_close(newcid, h_type, h_period, h_pdate, + "O", h_cnt, h_amt, "RECLOSE:NEW") < 0) { + setl(b, T_RC, 5810); + Bchg(b, T_MSG, 0, "신규 마감 개시 실패", 0L); FAIL(b); + } + setl(b, T_ID2, newcid); + Bchg(b, T_STATUS, 0, "R", 0L); + o_count = h_cnt; o_amount = h_amt; o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "재마감 등록 신규cid=%ld", newcid); + + /* --- 섹션6: 재마감 후 헤더 재확인 --- */ + EXEC SQL SELECT txn_count, total_amount INTO :h_cnt, :h_amt + FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5811); FAIL(b); } + setl(b, T_ID1, h_cid); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_RECLOSEREG 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1500000000L) { + z_sev = 3; + strcpy(z_msg, "광역 마감규모"); + } else if (h_zamt >= 150000000L) { + z_sev = 2; + strcpy(z_msg, "표준 마감규모"); + } else { + z_sev = 1; + strcpy(z_msg, "경량 마감규모"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_RECLOSEREG 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_RECLOSEREG 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_RECLOSEREG 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_RECLOSEREG 재마감 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_RECLOSEREG 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_RECLOSEREG 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_RECON.pgc b/app/src/cl/svc/CL_RECON.pgc index a063be8..da141c1 100644 --- a/app/src/cl/svc/CL_RECON.pgc +++ b/app/src/cl/svc/CL_RECON.pgc @@ -9,23 +9,168 @@ void CL_RECON(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long dcnt = 0, damt = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid, h_stored, h_actual, h_asum, h_tsum; char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_hdr_cnt, h_hdr_amt; + long h_real_cnt, h_real_amt; + char h_date[16], h_type[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_RECON"); + userlog("CL_RECON 진입: 마감처리 시작 (마감 정합 검증)", svc); + h_cid = getl(b, T_ID1); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT txn_count, total_amount INTO :h_stored, :h_tsum - FROM cl_close_log WHERE close_id = :h_cid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_actual, :h_asum - FROM purchase WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_actual); - setl(b, T_DELTA, h_tsum - h_asum); - setl(b, T_RC, (h_stored == h_actual && h_tsum == h_asum) ? 0 : 1); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0 && h_bizdate[0] == 0) { + setl(b, T_RC, 5401); + Bchg(b, T_MSG, 0, "마감ID/영업일 중 하나는 필수", 0L); FAIL(b); + } + + /* --- 섹션2: 마감이력 헤더 조회부 --- */ + if (h_cid > 0) { + EXEC SQL SELECT close_type, txn_count, total_amount, + to_char(biz_date,'YYYY-MM-DD') + INTO :h_type, :h_hdr_cnt, :h_hdr_amt, :h_date + FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5402); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5403); FAIL(b); } + } else { + strcpy(h_date, h_bizdate); + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(total_amount),0) + INTO :h_hdr_cnt, :h_hdr_amt FROM cl_close_log + WHERE period_key = :h_bizdate AND status <> 'X'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5404); FAIL(b); } + } + + /* --- 섹션4: 실매입 재집계부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_real_cnt, :h_real_amt + FROM purchase WHERE biz_date = :h_date; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5405); FAIL(b); } + + /* --- 섹션3: 대사 분기부 --- */ + dcnt = h_hdr_cnt - h_real_cnt; if (dcnt < 0) dcnt = -dcnt; + damt = h_hdr_amt - h_real_amt; if (damt < 0) damt = -damt; + if (dcnt == 0 && damt == 0) { + o_rc = 0; strcpy(o_msg, "건수·금액 완전 일치"); + } else if (dcnt == 0 && damt != 0) { + o_rc = 1; strcpy(o_msg, "건수 일치·금액 불일치"); + } else if (dcnt != 0 && damt == 0) { + o_rc = 2; strcpy(o_msg, "건수 불일치·금액 일치"); + } else { + o_rc = 3; strcpy(o_msg, "건수·금액 모두 불일치"); + } + + /* --- 섹션6: 결과 세팅 --- */ + setl(b, T_ARG1, h_hdr_cnt); + setl(b, T_ARG2, h_real_cnt); + setl(b, T_AMT1, h_hdr_amt); + setl(b, T_AMT2, h_real_amt); + setl(b, T_DELTA, damt); + o_count = dcnt; o_amount = damt; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_RECON 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 2000000000L) { + z_sev = 3; + strcpy(z_msg, "초과대형 마감구간"); + } else if (h_zamt >= 200000000L) { + z_sev = 2; + strcpy(z_msg, "중대형 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "일반 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_RECON 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_RECON 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_RECON 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_RECON 정합검증 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_RECON 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_RECON 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_REOPENREQ.pgc b/app/src/cl/svc/CL_REOPENREQ.pgc index ccb96e1..d088b54 100644 --- a/app/src/cl/svc/CL_REOPENREQ.pgc +++ b/app/src/cl/svc/CL_REOPENREQ.pgc @@ -9,35 +9,168 @@ void CL_REOPENREQ(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char reason[48]; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + char st = 0; + char newmemo[128]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid, h_locked; - char h_memo[64]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_cnt; + char h_status[4], h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_REOPENREQ"); + userlog("CL_REOPENREQ 진입: 마감처리 시작 (마감 재개시)", svc); + h_cid = getl(b, T_ID1); - gets_(b, T_REASON, reason, sizeof(reason)); - EXEC SQL SELECT count(*) INTO :h_locked - FROM cl_period_lock l, cl_close_log c - WHERE c.close_id = :h_cid AND l.period_key = c.period_key AND l.locked = true; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_locked > 0) { - setl(b, T_RC, 3); - Bchg(b, T_MSG, 0, "잠금 기간: 재개시 불가 (선 잠금해제 필요)", 0L); - FAIL(b); + gets_(b, T_STR1, h_memo, sizeof(h_memo)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0) { + setl(b, T_RC, 5601); + Bchg(b, T_MSG, 0, "마감ID(T_ID1) 필수", 0L); FAIL(b); } - snprintf(h_memo, sizeof(h_memo), "REOPEN:%s", reason[0] ? reason : "무사유"); + + /* --- 섹션2: 현재 상태/집계 조회부 --- */ + EXEC SQL SELECT status, txn_count, memo + INTO :h_status, :h_cnt, :h_memo + FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5602); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5603); FAIL(b); } + st = h_status[0]; + + /* --- 섹션3: 상태전이 가능여부 분기부 --- */ + if (!(st == 'C')) { + setl(b, T_RC, 5604); + snprintf(o_msg, sizeof(o_msg), "현재상태(%c)에서 마감 재개시 불가", st); + Bchg(b, T_MSG, 0, o_msg, 0L); FAIL(b); + } + if (h_cnt == 0) { + setl(b, T_RC, 5605); + Bchg(b, T_MSG, 0, "집계 0건 - 승인 대상 아님", 0L); FAIL(b); + } + + /* --- 섹션4: 상태전이 본처리부 --- */ + if (h_memo[0]) snprintf(newmemo, sizeof(newmemo), "REOPEN|%s", h_memo); + else strcpy(newmemo, "REOPEN"); + { char nm[128]; strncpy(nm, newmemo, sizeof(nm)-1); nm[sizeof(nm)-1]=0; + strncpy(h_memo, nm, sizeof(h_memo)-1); h_memo[sizeof(h_memo)-1]=0; } EXEC SQL UPDATE cl_close_log SET status = 'O', memo = :h_memo, updated_at = now() - WHERE close_id = :h_cid AND status = 'C'; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlerrd[2] != 1) { - setl(b, T_RC, 4); - Bchg(b, T_MSG, 0, "완료(C) 상태가 아니어서 재개시 실패", 0L); - FAIL(b); + WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5606); + Bchg(b, T_MSG, 0, "상태전이 실패", 0L); FAIL(b); } - setl(b, T_RC, 0); - Bchg(b, T_STATUS, 0, "O", 0L); - userlog("CL_REOPENREQ cid=%ld 재개시", h_cid); + if (sqlca.sqlerrd[2] != 1) { + setl(b, T_RC, 5607); + Bchg(b, T_MSG, 0, "전이 대상행 수 이상", 0L); FAIL(b); + } + + /* --- 섹션6: 전이 후 상태 재확인부 --- */ + EXEC SQL SELECT status INTO :h_status FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5608); FAIL(b); } + Bchg(b, T_STATUS, 0, h_status, 0L); + o_count = h_cnt; o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "마감 재개시 처리완료 상태=%c", h_status[0]); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_REOPENREQ 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 2000000000L) { + z_sev = 3; + strcpy(z_msg, "초과대형 마감구간"); + } else if (h_zamt >= 200000000L) { + z_sev = 2; + strcpy(z_msg, "중대형 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "일반 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_REOPENREQ 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_REOPENREQ 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_REOPENREQ 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_REOPENREQ 마감 재개시 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_REOPENREQ 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_REOPENREQ 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_REPORT.pgc b/app/src/cl/svc/CL_REPORT.pgc index eff23eb..7d2d6f3 100644 --- a/app/src/cl/svc/CL_REPORT.pgc +++ b/app/src/cl/svc/CL_REPORT.pgc @@ -9,15 +9,147 @@ void CL_REPORT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid, h_cnt, h_gross; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_cnt, h_g, h_f, h_nt; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_REPORT"); + userlog("CL_REPORT 진입: 마감처리 시작 (리포트)", svc); + h_cid = getl(b, T_ID1); - EXEC SQL SELECT count(*), coalesce(sum(gross_amount),0) INTO :h_cnt, :h_gross + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0) { + setl(b, T_RC, 6301); + Bchg(b, T_MSG, 0, "대상 마감ID 필수", 0L); FAIL(b); + } + + /* --- 섹션2: 마감 존재 확인부 --- */ + EXEC SQL SELECT count(*) INTO :h_cnt FROM cl_close_log + WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6302); FAIL(b); } + if (h_cnt == 0) { + setl(b, T_RC, 6303); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); + } + + /* --- 섹션4: 스냅샷 gross/fee/net 합계부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(gross_amount),0), + coalesce(sum(fee_amount),0), coalesce(sum(net_amount),0) + INTO :h_cnt, :h_g, :h_f, :h_nt FROM cl_snapshot WHERE close_id = :h_cid; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_gross); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6304); FAIL(b); } + + /* --- 섹션3: 요약 규모 분기부 --- */ + if (h_cnt == 0) { + o_rc = 1; strcpy(o_msg, "스냅샷 없음"); + } else if (h_g == h_f + h_nt) { + o_rc = 0; strcpy(o_msg, "gross=fee+net 정합"); + } else { + o_rc = 2; strcpy(o_msg, "gross<>fee+net 불일치"); + } + setl(b, T_GROSS, h_g); + setl(b, T_FEE, h_f); + setl(b, T_NET, h_nt); + o_count = h_cnt; o_amount = h_g; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_REPORT 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 2000000000L) { + z_sev = 3; + strcpy(z_msg, "초과대형 마감구간"); + } else if (h_zamt >= 200000000L) { + z_sev = 2; + strcpy(z_msg, "중대형 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "일반 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_REPORT 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_REPORT 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_REPORT 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_REPORT 리포트 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_REPORT 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_REPORT 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_RESTORE.pgc b/app/src/cl/svc/CL_RESTORE.pgc index 86eb0e1..67d96aa 100644 --- a/app/src/cl/svc/CL_RESTORE.pgc +++ b/app/src/cl/svc/CL_RESTORE.pgc @@ -9,14 +9,159 @@ void CL_RESTORE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + char st = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_cnt, h_n; + char h_status[4], h_reason[64], h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_RESTORE"); + userlog("CL_RESTORE 진입: 마감처리 시작 (마감 복원)", svc); + h_cid = getl(b, T_ID1); - EXEC SQL UPDATE cl_close_log SET status = 'O', memo = 'RESTORED', updated_at = now() - WHERE close_id = :h_cid AND status = 'X'; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - Bchg(b, T_STATUS, 0, "O", 0L); + gets_(b, T_REASON, h_reason, sizeof(h_reason)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0) { + setl(b, T_RC, 5711); + Bchg(b, T_MSG, 0, "마감ID(T_ID1) 필수", 0L); FAIL(b); + } + + /* --- 섹션2: 현재 상태/스냅샷 조회부 --- */ + EXEC SQL SELECT status, txn_count INTO :h_status, :h_cnt + FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5712); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5713); FAIL(b); } + st = h_status[0]; + + /* --- 섹션3: 취소/전이 가능 분기부 --- */ + if (st != 'X' && st != 'C' && st != 'R') { + setl(b, T_RC, 5715); + Bchg(b, T_MSG, 0, "복원/롤백 불가 상태", 0L); FAIL(b); + } + + /* --- 섹션4: 본처리부 --- */ + EXEC SQL DELETE FROM cl_snapshot WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5717); + Bchg(b, T_MSG, 0, "스냅샷 삭제 실패", 0L); FAIL(b); } + EXEC SQL UPDATE cl_close_log + SET status = 'O', updated_at = now() + WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5718); + Bchg(b, T_MSG, 0, "복원 처리 실패", 0L); FAIL(b); + } + if (sqlca.sqlerrd[2] != 1) { setl(b, T_RC, 5719); + Bchg(b, T_MSG, 0, "대상행 수 이상", 0L); FAIL(b); } + + /* --- 섹션6: 처리 후 상태 재확인 --- */ + EXEC SQL SELECT status INTO :h_status FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5720); FAIL(b); } + Bchg(b, T_STATUS, 0, h_status, 0L); + o_count = h_cnt; o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "복원 완료 상태=%c", h_status[0]); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_RESTORE 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 500000000L) { + z_sev = 3; + strcpy(z_msg, "고액 마감구간"); + } else if (h_zamt >= 50000000L) { + z_sev = 2; + strcpy(z_msg, "중액 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "소액 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_RESTORE 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_RESTORE 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_RESTORE 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_RESTORE 복원 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_RESTORE 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_RESTORE 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_ROLLBACK.pgc b/app/src/cl/svc/CL_ROLLBACK.pgc index 926b7b9..9b68c75 100644 --- a/app/src/cl/svc/CL_ROLLBACK.pgc +++ b/app/src/cl/svc/CL_ROLLBACK.pgc @@ -9,15 +9,159 @@ void CL_ROLLBACK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + char st = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_cnt, h_n; + char h_status[4], h_reason[64], h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_ROLLBACK"); + userlog("CL_ROLLBACK 진입: 마감처리 시작 (마감 롤백)", svc); + h_cid = getl(b, T_ID1); + gets_(b, T_REASON, h_reason, sizeof(h_reason)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0) { + setl(b, T_RC, 5711); + Bchg(b, T_MSG, 0, "마감ID(T_ID1) 필수", 0L); FAIL(b); + } + + /* --- 섹션2: 현재 상태/스냅샷 조회부 --- */ + EXEC SQL SELECT status, txn_count INTO :h_status, :h_cnt + FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5712); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5713); FAIL(b); } + st = h_status[0]; + + /* --- 섹션3: 취소/전이 가능 분기부 --- */ + if (st != 'X' && st != 'C' && st != 'R') { + setl(b, T_RC, 5715); + Bchg(b, T_MSG, 0, "복원/롤백 불가 상태", 0L); FAIL(b); + } + + /* --- 섹션4: 본처리부 --- */ EXEC SQL DELETE FROM cl_snapshot WHERE close_id = :h_cid; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, sqlca.sqlerrd[2]); - if (cldb_update_close_status(h_cid, "O") < 0) FAIL(b); - Bchg(b, T_STATUS, 0, "O", 0L); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5717); + Bchg(b, T_MSG, 0, "스냅샷 삭제 실패", 0L); FAIL(b); } + EXEC SQL UPDATE cl_close_log + SET status = 'O', updated_at = now() + WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5718); + Bchg(b, T_MSG, 0, "롤백 처리 실패", 0L); FAIL(b); + } + if (sqlca.sqlerrd[2] != 1) { setl(b, T_RC, 5719); + Bchg(b, T_MSG, 0, "대상행 수 이상", 0L); FAIL(b); } + + /* --- 섹션6: 처리 후 상태 재확인 --- */ + EXEC SQL SELECT status INTO :h_status FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5720); FAIL(b); } + Bchg(b, T_STATUS, 0, h_status, 0L); + o_count = h_cnt; o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "롤백 완료 상태=%c", h_status[0]); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_ROLLBACK 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 2000000000L) { + z_sev = 3; + strcpy(z_msg, "초과대형 마감구간"); + } else if (h_zamt >= 200000000L) { + z_sev = 2; + strcpy(z_msg, "중대형 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "일반 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_ROLLBACK 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_ROLLBACK 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_ROLLBACK 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_ROLLBACK 롤백 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_ROLLBACK 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_ROLLBACK 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_RPTAPPR.pgc b/app/src/cl/svc/CL_RPTAPPR.pgc index efe14cb..6fc4bd3 100644 --- a/app/src/cl/svc/CL_RPTAPPR.pgc +++ b/app/src/cl/svc/CL_RPTAPPR.pgc @@ -8,30 +8,161 @@ void CL_RPTAPPR(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long occ = 0, totamt = 0; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long groups = 0; long topamt = 0; char topdim[64]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - char h_pk[24]; - long h_id, h_amt; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_grpcnt, h_grpamt, h_cid; + char h_dim[64], h_key[32]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; - EXEC SQL DECLARE crappr CURSOR FOR - SELECT close_id, period_key, total_amount - FROM cl_close_log WHERE status = 'A' ORDER BY close_id DESC; - EXEC SQL OPEN crappr; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH crappr INTO :h_id, :h_pk, :h_amt; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE crappr; FAIL(b); } - if (occ >= 30) break; - Bchg(b, T_ARG1, (BFLDOCC)occ, (char *)&h_id, 0L); - Bchg(b, T_KEY1, (BFLDOCC)occ, h_pk, 0L); - Bchg(b, T_AMT1, (BFLDOCC)occ, (char *)&h_amt, 0L); - totamt += h_amt; - occ++; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_RPTAPPR"); + userlog("CL_RPTAPPR 진입: 마감처리 시작 (승인마감)", svc); + + + /* --- 섹션1: 입력 검증부 --- */ + /* 전역 리포트 - 별도 필수입력 없음 */ + + /* --- 섹션2: 그룹 수 사전 확인부 --- */ + EXEC SQL SELECT count(DISTINCT period_key) INTO :h_grpcnt + FROM cl_close_log WHERE status = 'A'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6504); FAIL(b); } + if (h_grpcnt == 0) { + o_rc = 1; o_count = 0; + strcpy(o_msg, "리포트 대상 그룹 없음"); + setl(b, T_COUNT, 0); Bchg(b, T_MSG, 0, o_msg, 0L); OK(b); } - EXEC SQL CLOSE crappr; - setl(b, T_COUNT, occ); - setl(b, T_GROSS, totamt); + + /* --- 섹션4: 그룹별 집계 커서(상위추출) --- */ + strcpy(topdim, ""); + EXEC SQL DECLARE crpt CURSOR FOR + SELECT period_key, count(*), coalesce(sum(total_amount),0) FROM cl_close_log + WHERE status = 'A' GROUP BY period_key ORDER BY 2 DESC; + EXEC SQL OPEN crpt; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6505); FAIL(b); } + for (;;) { + EXEC SQL FETCH crpt INTO :h_dim, :h_grpcnt, :h_grpamt; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE crpt; setl(b, T_RC, 6506); FAIL(b); } + if (groups < 20) { + Bchg(b, T_STR2, (BFLDOCC)groups, h_dim, 0L); + setl(b, T_ARG3, h_grpamt); + } + if (h_grpamt > topamt) { + topamt = h_grpamt; + strncpy(topdim, h_dim, sizeof(topdim)-1); topdim[sizeof(topdim)-1]=0; + } + groups++; + } + EXEC SQL CLOSE crpt; + + /* --- 섹션3: 상위그룹 판정 분기부 --- */ + if (groups == 0) { + o_rc = 1; strcpy(o_msg, "승인마감 집계 결과 없음"); + } else if (topamt == 0) { + o_rc = 2; strcpy(o_msg, "승인마감 집계 금액 0"); + } else { + o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "승인마감 최다=%s(%ld)", topdim, topamt); + } + Bchg(b, T_STR1, 0, topdim, 0L); + o_count = groups; o_amount = topamt; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_RPTAPPR 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1500000000L) { + z_sev = 3; + strcpy(z_msg, "광역 마감규모"); + } else if (h_zamt >= 150000000L) { + z_sev = 2; + strcpy(z_msg, "표준 마감규모"); + } else { + z_sev = 1; + strcpy(z_msg, "경량 마감규모"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_RPTAPPR 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_RPTAPPR 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_RPTAPPR 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_RPTAPPR 승인마감 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_RPTAPPR 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_RPTAPPR 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_RPTCAT.pgc b/app/src/cl/svc/CL_RPTCAT.pgc index 1eec36f..152b001 100644 --- a/app/src/cl/svc/CL_RPTCAT.pgc +++ b/app/src/cl/svc/CL_RPTCAT.pgc @@ -9,32 +9,165 @@ void CL_RPTCAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long occ = 0, maxbps = 0; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long groups = 0; long topamt = 0; char topdim[64]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_amt, h_bps; - char h_bizdate[16], h_cat[16]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_grpcnt, h_grpamt, h_cid; + char h_dim[64], h_key[32]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_RPTCAT"); + userlog("CL_RPTCAT 진입: 마감처리 시작 (업종별)", svc); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL DECLARE crcat CURSOR FOR - SELECT category, coalesce(sum(amount),0), - coalesce(sum(fee) * 10000 / nullif(sum(amount),0), 0) - FROM purchase WHERE biz_date = :h_bizdate - GROUP BY category ORDER BY category; - EXEC SQL OPEN crcat; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH crcat INTO :h_cat, :h_amt, :h_bps; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE crcat; FAIL(b); } - Bchg(b, T_CATEGORY, (BFLDOCC)occ, h_cat, 0L); - Bchg(b, T_AMT3, (BFLDOCC)occ, (char *)&h_amt, 0L); - Bchg(b, T_ARG3, (BFLDOCC)occ, (char *)&h_bps, 0L); - if (h_bps > maxbps) maxbps = h_bps; - occ++; + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0) { + setl(b, T_RC, 6501); + Bchg(b, T_MSG, 0, "리포트 기준 영업일 필수", 0L); FAIL(b); } - EXEC SQL CLOSE crcat; - setl(b, T_COUNT, occ); - setl(b, T_ARG4, maxbps); + + /* --- 섹션2: 그룹 수 사전 확인부 --- */ + EXEC SQL SELECT count(DISTINCT category) INTO :h_grpcnt + FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6504); FAIL(b); } + if (h_grpcnt == 0) { + o_rc = 1; o_count = 0; + strcpy(o_msg, "리포트 대상 그룹 없음"); + setl(b, T_COUNT, 0); Bchg(b, T_MSG, 0, o_msg, 0L); OK(b); + } + + /* --- 섹션4: 그룹별 집계 커서(상위추출) --- */ + strcpy(topdim, ""); + EXEC SQL DECLARE crpt CURSOR FOR + SELECT category, count(*), coalesce(sum(amount),0) FROM purchase + WHERE biz_date = :h_bizdate GROUP BY category ORDER BY 2 DESC; + EXEC SQL OPEN crpt; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6505); FAIL(b); } + for (;;) { + EXEC SQL FETCH crpt INTO :h_dim, :h_grpcnt, :h_grpamt; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE crpt; setl(b, T_RC, 6506); FAIL(b); } + if (groups < 20) { + Bchg(b, T_STR2, (BFLDOCC)groups, h_dim, 0L); + setl(b, T_ARG3, h_grpamt); + } + if (h_grpamt > topamt) { + topamt = h_grpamt; + strncpy(topdim, h_dim, sizeof(topdim)-1); topdim[sizeof(topdim)-1]=0; + } + groups++; + } + EXEC SQL CLOSE crpt; + + /* --- 섹션3: 상위그룹 판정 분기부 --- */ + if (groups == 0) { + o_rc = 1; strcpy(o_msg, "업종별 집계 결과 없음"); + } else if (topamt == 0) { + o_rc = 2; strcpy(o_msg, "업종별 집계 금액 0"); + } else { + o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "업종별 최다=%s(%ld)", topdim, topamt); + } + Bchg(b, T_STR1, 0, topdim, 0L); + o_count = groups; o_amount = topamt; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_RPTCAT 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 500000000L) { + z_sev = 3; + strcpy(z_msg, "고액 마감구간"); + } else if (h_zamt >= 50000000L) { + z_sev = 2; + strcpy(z_msg, "중액 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "소액 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_RPTCAT 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_RPTCAT 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_RPTCAT 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_RPTCAT 업종별 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_RPTCAT 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_RPTCAT 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_RPTCHN.pgc b/app/src/cl/svc/CL_RPTCHN.pgc index 7b7b007..cc54acd 100644 --- a/app/src/cl/svc/CL_RPTCHN.pgc +++ b/app/src/cl/svc/CL_RPTCHN.pgc @@ -9,31 +9,165 @@ void CL_RPTCHN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long occ = 0, totamt = 0; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long groups = 0; long topamt = 0; char topdim[64]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_amt; - char h_bizdate[16], h_chn[16]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_grpcnt, h_grpamt, h_cid; + char h_dim[64], h_key[32]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_RPTCHN"); + userlog("CL_RPTCHN 진입: 마감처리 시작 (채널별)", svc); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL DECLARE crchn CURSOR FOR - SELECT channel, count(*), coalesce(sum(amount),0) - FROM purchase WHERE biz_date = :h_bizdate AND status <> 'C' - GROUP BY channel ORDER BY 3 DESC; - EXEC SQL OPEN crchn; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH crchn INTO :h_chn, :h_cnt, :h_amt; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE crchn; FAIL(b); } - Bchg(b, T_CHANNEL, (BFLDOCC)occ, h_chn, 0L); - Bchg(b, T_ARG1, (BFLDOCC)occ, (char *)&h_cnt, 0L); - Bchg(b, T_AMT1, (BFLDOCC)occ, (char *)&h_amt, 0L); - totamt += h_amt; - occ++; + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0) { + setl(b, T_RC, 6501); + Bchg(b, T_MSG, 0, "리포트 기준 영업일 필수", 0L); FAIL(b); } - EXEC SQL CLOSE crchn; - setl(b, T_COUNT, occ); - setl(b, T_GROSS, totamt); + + /* --- 섹션2: 그룹 수 사전 확인부 --- */ + EXEC SQL SELECT count(DISTINCT channel) INTO :h_grpcnt + FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6504); FAIL(b); } + if (h_grpcnt == 0) { + o_rc = 1; o_count = 0; + strcpy(o_msg, "리포트 대상 그룹 없음"); + setl(b, T_COUNT, 0); Bchg(b, T_MSG, 0, o_msg, 0L); OK(b); + } + + /* --- 섹션4: 그룹별 집계 커서(상위추출) --- */ + strcpy(topdim, ""); + EXEC SQL DECLARE crpt CURSOR FOR + SELECT channel, count(*), coalesce(sum(amount),0) FROM purchase + WHERE biz_date = :h_bizdate GROUP BY channel ORDER BY 2 DESC; + EXEC SQL OPEN crpt; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6505); FAIL(b); } + for (;;) { + EXEC SQL FETCH crpt INTO :h_dim, :h_grpcnt, :h_grpamt; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE crpt; setl(b, T_RC, 6506); FAIL(b); } + if (groups < 20) { + Bchg(b, T_STR2, (BFLDOCC)groups, h_dim, 0L); + setl(b, T_ARG3, h_grpamt); + } + if (h_grpamt > topamt) { + topamt = h_grpamt; + strncpy(topdim, h_dim, sizeof(topdim)-1); topdim[sizeof(topdim)-1]=0; + } + groups++; + } + EXEC SQL CLOSE crpt; + + /* --- 섹션3: 상위그룹 판정 분기부 --- */ + if (groups == 0) { + o_rc = 1; strcpy(o_msg, "채널별 집계 결과 없음"); + } else if (topamt == 0) { + o_rc = 2; strcpy(o_msg, "채널별 집계 금액 0"); + } else { + o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "채널별 최다=%s(%ld)", topdim, topamt); + } + Bchg(b, T_STR1, 0, topdim, 0L); + o_count = groups; o_amount = topamt; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_RPTCHN 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 500000000L) { + z_sev = 3; + strcpy(z_msg, "고액 마감구간"); + } else if (h_zamt >= 50000000L) { + z_sev = 2; + strcpy(z_msg, "중액 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "소액 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_RPTCHN 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_RPTCHN 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_RPTCHN 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_RPTCHN 채널별 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_RPTCHN 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_RPTCHN 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_RPTCXL.pgc b/app/src/cl/svc/CL_RPTCXL.pgc index 4130662..2e6ba96 100644 --- a/app/src/cl/svc/CL_RPTCXL.pgc +++ b/app/src/cl/svc/CL_RPTCXL.pgc @@ -8,28 +8,161 @@ void CL_RPTCXL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long occ = 0; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long groups = 0; long topamt = 0; char topdim[64]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - char h_memo[64]; - long h_id, h_amt; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_grpcnt, h_grpamt, h_cid; + char h_dim[64], h_key[32]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; - EXEC SQL DECLARE crcxl CURSOR FOR - SELECT close_id, memo, total_amount - FROM cl_close_log WHERE status = 'X' ORDER BY close_id DESC; - EXEC SQL OPEN crcxl; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH crcxl INTO :h_id, :h_memo, :h_amt; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE crcxl; FAIL(b); } - if (occ >= 30) break; - Bchg(b, T_ARG1, (BFLDOCC)occ, (char *)&h_id, 0L); - Bchg(b, T_STR1, (BFLDOCC)occ, h_memo, 0L); - Bchg(b, T_AMT1, (BFLDOCC)occ, (char *)&h_amt, 0L); - occ++; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_RPTCXL"); + userlog("CL_RPTCXL 진입: 마감처리 시작 (취소마감)", svc); + + + /* --- 섹션1: 입력 검증부 --- */ + /* 전역 리포트 - 별도 필수입력 없음 */ + + /* --- 섹션2: 그룹 수 사전 확인부 --- */ + EXEC SQL SELECT count(DISTINCT memo) INTO :h_grpcnt + FROM cl_close_log WHERE status = 'X'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6504); FAIL(b); } + if (h_grpcnt == 0) { + o_rc = 1; o_count = 0; + strcpy(o_msg, "리포트 대상 그룹 없음"); + setl(b, T_COUNT, 0); Bchg(b, T_MSG, 0, o_msg, 0L); OK(b); } - EXEC SQL CLOSE crcxl; - setl(b, T_COUNT, occ); + + /* --- 섹션4: 그룹별 집계 커서(상위추출) --- */ + strcpy(topdim, ""); + EXEC SQL DECLARE crpt CURSOR FOR + SELECT memo, count(*), coalesce(sum(total_amount),0) FROM cl_close_log + WHERE status = 'X' GROUP BY memo ORDER BY 2 DESC; + EXEC SQL OPEN crpt; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6505); FAIL(b); } + for (;;) { + EXEC SQL FETCH crpt INTO :h_dim, :h_grpcnt, :h_grpamt; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE crpt; setl(b, T_RC, 6506); FAIL(b); } + if (groups < 20) { + Bchg(b, T_STR2, (BFLDOCC)groups, h_dim, 0L); + setl(b, T_ARG3, h_grpamt); + } + if (h_grpamt > topamt) { + topamt = h_grpamt; + strncpy(topdim, h_dim, sizeof(topdim)-1); topdim[sizeof(topdim)-1]=0; + } + groups++; + } + EXEC SQL CLOSE crpt; + + /* --- 섹션3: 상위그룹 판정 분기부 --- */ + if (groups == 0) { + o_rc = 1; strcpy(o_msg, "취소마감 집계 결과 없음"); + } else if (topamt == 0) { + o_rc = 2; strcpy(o_msg, "취소마감 집계 금액 0"); + } else { + o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "취소마감 최다=%s(%ld)", topdim, topamt); + } + Bchg(b, T_STR1, 0, topdim, 0L); + o_count = groups; o_amount = topamt; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_RPTCXL 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 2000000000L) { + z_sev = 3; + strcpy(z_msg, "초과대형 마감구간"); + } else if (h_zamt >= 200000000L) { + z_sev = 2; + strcpy(z_msg, "중대형 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "일반 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_RPTCXL 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_RPTCXL 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_RPTCXL 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_RPTCXL 취소마감 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_RPTCXL 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_RPTCXL 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_RPTDELAY.pgc b/app/src/cl/svc/CL_RPTDELAY.pgc index 352b334..6215dee 100644 --- a/app/src/cl/svc/CL_RPTDELAY.pgc +++ b/app/src/cl/svc/CL_RPTDELAY.pgc @@ -9,31 +9,165 @@ void CL_RPTDELAY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long occ = 0, totcnt = 0; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long groups = 0; long topamt = 0; char topdim[64]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16], h_day[16]; - long h_cnt, h_amt; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_grpcnt, h_grpamt, h_cid; + char h_dim[64], h_key[32]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_RPTDELAY"); + userlog("CL_RPTDELAY 진입: 마감처리 시작 (지연매입)", svc); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL DECLARE crdly CURSOR FOR - SELECT to_char(biz_date,'YYYY-MM-DD'), count(*), coalesce(sum(amount),0) - FROM purchase WHERE status = 'M' AND biz_date < :h_bizdate::date - GROUP BY biz_date ORDER BY biz_date; - EXEC SQL OPEN crdly; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH crdly INTO :h_day, :h_cnt, :h_amt; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE crdly; FAIL(b); } - Bchg(b, T_KEY1, (BFLDOCC)occ, h_day, 0L); - Bchg(b, T_ARG1, (BFLDOCC)occ, (char *)&h_cnt, 0L); - Bchg(b, T_AMT1, (BFLDOCC)occ, (char *)&h_amt, 0L); - totcnt += h_cnt; - occ++; + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0) { + setl(b, T_RC, 6501); + Bchg(b, T_MSG, 0, "리포트 기준 영업일 필수", 0L); FAIL(b); } - EXEC SQL CLOSE crdly; - setl(b, T_COUNT, occ); - setl(b, T_ARG2, totcnt); + + /* --- 섹션2: 그룹 수 사전 확인부 --- */ + EXEC SQL SELECT count(DISTINCT merchant_id) INTO :h_grpcnt + FROM purchase WHERE biz_date < :h_bizdate AND status = 'M'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6504); FAIL(b); } + if (h_grpcnt == 0) { + o_rc = 1; o_count = 0; + strcpy(o_msg, "리포트 대상 그룹 없음"); + setl(b, T_COUNT, 0); Bchg(b, T_MSG, 0, o_msg, 0L); OK(b); + } + + /* --- 섹션4: 그룹별 집계 커서(상위추출) --- */ + strcpy(topdim, ""); + EXEC SQL DECLARE crpt CURSOR FOR + SELECT merchant_id, count(*), coalesce(sum(amount),0) FROM purchase + WHERE biz_date < :h_bizdate AND status = 'M' GROUP BY merchant_id ORDER BY 2 DESC; + EXEC SQL OPEN crpt; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6505); FAIL(b); } + for (;;) { + EXEC SQL FETCH crpt INTO :h_dim, :h_grpcnt, :h_grpamt; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE crpt; setl(b, T_RC, 6506); FAIL(b); } + if (groups < 20) { + Bchg(b, T_STR2, (BFLDOCC)groups, h_dim, 0L); + setl(b, T_ARG3, h_grpamt); + } + if (h_grpamt > topamt) { + topamt = h_grpamt; + strncpy(topdim, h_dim, sizeof(topdim)-1); topdim[sizeof(topdim)-1]=0; + } + groups++; + } + EXEC SQL CLOSE crpt; + + /* --- 섹션3: 상위그룹 판정 분기부 --- */ + if (groups == 0) { + o_rc = 1; strcpy(o_msg, "지연매입 집계 결과 없음"); + } else if (topamt == 0) { + o_rc = 2; strcpy(o_msg, "지연매입 집계 금액 0"); + } else { + o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "지연매입 최다=%s(%ld)", topdim, topamt); + } + Bchg(b, T_STR1, 0, topdim, 0L); + o_count = groups; o_amount = topamt; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_RPTDELAY 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1500000000L) { + z_sev = 3; + strcpy(z_msg, "광역 마감규모"); + } else if (h_zamt >= 150000000L) { + z_sev = 2; + strcpy(z_msg, "표준 마감규모"); + } else { + z_sev = 1; + strcpy(z_msg, "경량 마감규모"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_RPTDELAY 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_RPTDELAY 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_RPTDELAY 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_RPTDELAY 지연매입 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_RPTDELAY 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_RPTDELAY 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_RPTDIV.pgc b/app/src/cl/svc/CL_RPTDIV.pgc index a6222ad..cbd6056 100644 --- a/app/src/cl/svc/CL_RPTDIV.pgc +++ b/app/src/cl/svc/CL_RPTDIV.pgc @@ -9,31 +9,165 @@ void CL_RPTDIV(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long occ = 0, totamt = 0; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long groups = 0; long topamt = 0; char topdim[64]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16], h_cat[16]; - long h_cnt, h_amt; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_grpcnt, h_grpamt, h_cid; + char h_dim[64], h_key[32]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_RPTDIV"); + userlog("CL_RPTDIV 진입: 마감처리 시작 (부문별)", svc); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL DECLARE crdiv CURSOR FOR - SELECT category, count(*), coalesce(sum(amount),0) - FROM purchase WHERE biz_date = :h_bizdate - GROUP BY category ORDER BY 3 DESC; - EXEC SQL OPEN crdiv; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH crdiv INTO :h_cat, :h_cnt, :h_amt; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE crdiv; FAIL(b); } - Bchg(b, T_CATEGORY, (BFLDOCC)occ, h_cat, 0L); - Bchg(b, T_ARG1, (BFLDOCC)occ, (char *)&h_cnt, 0L); - Bchg(b, T_AMT1, (BFLDOCC)occ, (char *)&h_amt, 0L); - totamt += h_amt; - occ++; + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0) { + setl(b, T_RC, 6501); + Bchg(b, T_MSG, 0, "리포트 기준 영업일 필수", 0L); FAIL(b); } - EXEC SQL CLOSE crdiv; - setl(b, T_COUNT, occ); - setl(b, T_GROSS, totamt); + + /* --- 섹션2: 그룹 수 사전 확인부 --- */ + EXEC SQL SELECT count(DISTINCT category) INTO :h_grpcnt + FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6504); FAIL(b); } + if (h_grpcnt == 0) { + o_rc = 1; o_count = 0; + strcpy(o_msg, "리포트 대상 그룹 없음"); + setl(b, T_COUNT, 0); Bchg(b, T_MSG, 0, o_msg, 0L); OK(b); + } + + /* --- 섹션4: 그룹별 집계 커서(상위추출) --- */ + strcpy(topdim, ""); + EXEC SQL DECLARE crpt CURSOR FOR + SELECT category, count(*), coalesce(sum(amount),0) FROM purchase + WHERE biz_date = :h_bizdate GROUP BY category ORDER BY 2 DESC; + EXEC SQL OPEN crpt; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6505); FAIL(b); } + for (;;) { + EXEC SQL FETCH crpt INTO :h_dim, :h_grpcnt, :h_grpamt; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE crpt; setl(b, T_RC, 6506); FAIL(b); } + if (groups < 20) { + Bchg(b, T_STR2, (BFLDOCC)groups, h_dim, 0L); + setl(b, T_ARG3, h_grpamt); + } + if (h_grpamt > topamt) { + topamt = h_grpamt; + strncpy(topdim, h_dim, sizeof(topdim)-1); topdim[sizeof(topdim)-1]=0; + } + groups++; + } + EXEC SQL CLOSE crpt; + + /* --- 섹션3: 상위그룹 판정 분기부 --- */ + if (groups == 0) { + o_rc = 1; strcpy(o_msg, "부문별 집계 결과 없음"); + } else if (topamt == 0) { + o_rc = 2; strcpy(o_msg, "부문별 집계 금액 0"); + } else { + o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "부문별 최다=%s(%ld)", topdim, topamt); + } + Bchg(b, T_STR1, 0, topdim, 0L); + o_count = groups; o_amount = topamt; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_RPTDIV 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1000000000L) { + z_sev = 3; + strcpy(z_msg, "대규모 마감기간"); + } else if (h_zamt >= 100000000L) { + z_sev = 2; + strcpy(z_msg, "중규모 마감기간"); + } else { + z_sev = 1; + strcpy(z_msg, "소규모 마감기간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_RPTDIV 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_RPTDIV 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_RPTDIV 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_RPTDIV 부문별 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_RPTDIV 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_RPTDIV 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_RPTISSUER.pgc b/app/src/cl/svc/CL_RPTISSUER.pgc index 7ff43de..6c129a0 100644 --- a/app/src/cl/svc/CL_RPTISSUER.pgc +++ b/app/src/cl/svc/CL_RPTISSUER.pgc @@ -9,32 +9,165 @@ void CL_RPTISSUER(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long occ = 0; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long groups = 0; long topamt = 0; char topdim[64]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_min, h_cnt, h_amt; - char h_bizdate[16], h_iss[24]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_grpcnt, h_grpamt, h_cid; + char h_dim[64], h_key[32]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_RPTISSUER"); + userlog("CL_RPTISSUER 진입: 마감처리 시작 (발급사별)", svc); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - h_min = getl(b, T_AMT1); - if (h_min < 0) h_min = 0; - EXEC SQL DECLARE criss CURSOR FOR - SELECT issuer, count(*), sum(amount) FROM purchase - WHERE biz_date = :h_bizdate - GROUP BY issuer HAVING sum(amount) >= :h_min - ORDER BY sum(amount) DESC; - EXEC SQL OPEN criss; - if (sqlca.sqlcode < 0) FAIL(b); - while (occ < 30) { - EXEC SQL FETCH criss INTO :h_iss, :h_cnt, :h_amt; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE criss; FAIL(b); } - Bchg(b, T_ISSUER, (BFLDOCC)occ, h_iss, 0L); - Bchg(b, T_ARG2, (BFLDOCC)occ, (char *)&h_cnt, 0L); - Bchg(b, T_AMT2, (BFLDOCC)occ, (char *)&h_amt, 0L); - occ++; + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0) { + setl(b, T_RC, 6501); + Bchg(b, T_MSG, 0, "리포트 기준 영업일 필수", 0L); FAIL(b); } - EXEC SQL CLOSE criss; - setl(b, T_COUNT, occ); + + /* --- 섹션2: 그룹 수 사전 확인부 --- */ + EXEC SQL SELECT count(DISTINCT issuer) INTO :h_grpcnt + FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6504); FAIL(b); } + if (h_grpcnt == 0) { + o_rc = 1; o_count = 0; + strcpy(o_msg, "리포트 대상 그룹 없음"); + setl(b, T_COUNT, 0); Bchg(b, T_MSG, 0, o_msg, 0L); OK(b); + } + + /* --- 섹션4: 그룹별 집계 커서(상위추출) --- */ + strcpy(topdim, ""); + EXEC SQL DECLARE crpt CURSOR FOR + SELECT issuer, count(*), coalesce(sum(amount),0) FROM purchase + WHERE biz_date = :h_bizdate GROUP BY issuer ORDER BY 2 DESC; + EXEC SQL OPEN crpt; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6505); FAIL(b); } + for (;;) { + EXEC SQL FETCH crpt INTO :h_dim, :h_grpcnt, :h_grpamt; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE crpt; setl(b, T_RC, 6506); FAIL(b); } + if (groups < 20) { + Bchg(b, T_STR2, (BFLDOCC)groups, h_dim, 0L); + setl(b, T_ARG3, h_grpamt); + } + if (h_grpamt > topamt) { + topamt = h_grpamt; + strncpy(topdim, h_dim, sizeof(topdim)-1); topdim[sizeof(topdim)-1]=0; + } + groups++; + } + EXEC SQL CLOSE crpt; + + /* --- 섹션3: 상위그룹 판정 분기부 --- */ + if (groups == 0) { + o_rc = 1; strcpy(o_msg, "발급사별 집계 결과 없음"); + } else if (topamt == 0) { + o_rc = 2; strcpy(o_msg, "발급사별 집계 금액 0"); + } else { + o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "발급사별 최다=%s(%ld)", topdim, topamt); + } + Bchg(b, T_STR1, 0, topdim, 0L); + o_count = groups; o_amount = topamt; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_RPTISSUER 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 500000000L) { + z_sev = 3; + strcpy(z_msg, "고액 마감구간"); + } else if (h_zamt >= 50000000L) { + z_sev = 2; + strcpy(z_msg, "중액 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "소액 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_RPTISSUER 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_RPTISSUER 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_RPTISSUER 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_RPTISSUER 발급사별 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_RPTISSUER 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_RPTISSUER 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_RPTPERIOD.pgc b/app/src/cl/svc/CL_RPTPERIOD.pgc index b8ceb9c..8518222 100644 --- a/app/src/cl/svc/CL_RPTPERIOD.pgc +++ b/app/src/cl/svc/CL_RPTPERIOD.pgc @@ -8,33 +8,166 @@ void CL_RPTPERIOD(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long occ = 0, totamt = 0; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long groups = 0; long topamt = 0; char topdim[64]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - char h_ct[16], h_pk[24]; - long h_cnt, h_amt; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_grpcnt, h_grpamt, h_cid; + char h_dim[64], h_key[32]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, h_ct, sizeof(h_ct)); - if (h_ct[0] == 0) strcpy(h_ct, "DAILY"); - EXEC SQL DECLARE crper CURSOR FOR - SELECT period_key, txn_count, total_amount - FROM cl_close_log WHERE close_type = :h_ct AND status <> 'X' - ORDER BY period_key DESC; - EXEC SQL OPEN crper; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH crper INTO :h_pk, :h_cnt, :h_amt; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE crper; FAIL(b); } - if (occ >= 20) break; - Bchg(b, T_KEY1, (BFLDOCC)occ, h_pk, 0L); - Bchg(b, T_ARG1, (BFLDOCC)occ, (char *)&h_cnt, 0L); - Bchg(b, T_AMT1, (BFLDOCC)occ, (char *)&h_amt, 0L); - totamt += h_amt; - occ++; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_RPTPERIOD"); + userlog("CL_RPTPERIOD 진입: 마감처리 시작 (기간별)", svc); + + gets_(b, T_KEY1, h_key, sizeof(h_key)); + if (h_key[0] == 0) gets_(b, T_STR1, h_key, sizeof(h_key)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_key[0] == 0) { + setl(b, T_RC, 6502); + Bchg(b, T_MSG, 0, "리포트 키(close_type/기간) 필수", 0L); FAIL(b); } - EXEC SQL CLOSE crper; - setl(b, T_COUNT, occ); - setl(b, T_GROSS, totamt); + + /* --- 섹션2: 그룹 수 사전 확인부 --- */ + EXEC SQL SELECT count(DISTINCT period_key) INTO :h_grpcnt + FROM cl_close_log WHERE close_type = :h_key; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6504); FAIL(b); } + if (h_grpcnt == 0) { + o_rc = 1; o_count = 0; + strcpy(o_msg, "리포트 대상 그룹 없음"); + setl(b, T_COUNT, 0); Bchg(b, T_MSG, 0, o_msg, 0L); OK(b); + } + + /* --- 섹션4: 그룹별 집계 커서(상위추출) --- */ + strcpy(topdim, ""); + EXEC SQL DECLARE crpt CURSOR FOR + SELECT period_key, count(*), coalesce(sum(total_amount),0) FROM cl_close_log + WHERE close_type = :h_key GROUP BY period_key ORDER BY 2 DESC; + EXEC SQL OPEN crpt; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6505); FAIL(b); } + for (;;) { + EXEC SQL FETCH crpt INTO :h_dim, :h_grpcnt, :h_grpamt; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE crpt; setl(b, T_RC, 6506); FAIL(b); } + if (groups < 20) { + Bchg(b, T_STR2, (BFLDOCC)groups, h_dim, 0L); + setl(b, T_ARG3, h_grpamt); + } + if (h_grpamt > topamt) { + topamt = h_grpamt; + strncpy(topdim, h_dim, sizeof(topdim)-1); topdim[sizeof(topdim)-1]=0; + } + groups++; + } + EXEC SQL CLOSE crpt; + + /* --- 섹션3: 상위그룹 판정 분기부 --- */ + if (groups == 0) { + o_rc = 1; strcpy(o_msg, "기간별 집계 결과 없음"); + } else if (topamt == 0) { + o_rc = 2; strcpy(o_msg, "기간별 집계 금액 0"); + } else { + o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "기간별 최다=%s(%ld)", topdim, topamt); + } + Bchg(b, T_STR1, 0, topdim, 0L); + o_count = groups; o_amount = topamt; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_RPTPERIOD 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 500000000L) { + z_sev = 3; + strcpy(z_msg, "고액 마감구간"); + } else if (h_zamt >= 50000000L) { + z_sev = 2; + strcpy(z_msg, "중액 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "소액 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_RPTPERIOD 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_RPTPERIOD 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_RPTPERIOD 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_RPTPERIOD 기간별 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_RPTPERIOD 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_RPTPERIOD 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_RPTSTATUS.pgc b/app/src/cl/svc/CL_RPTSTATUS.pgc index 7cf1d24..bf26291 100644 --- a/app/src/cl/svc/CL_RPTSTATUS.pgc +++ b/app/src/cl/svc/CL_RPTSTATUS.pgc @@ -8,27 +8,161 @@ void CL_RPTSTATUS(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long occ = 0; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long groups = 0; long topamt = 0; char topdim[64]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - char h_st[4]; - long h_cnt, h_amt; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_grpcnt, h_grpamt, h_cid; + char h_dim[64], h_key[32]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; - EXEC SQL DECLARE crsta CURSOR FOR - SELECT status, count(*), coalesce(sum(total_amount),0) - FROM cl_close_log GROUP BY status ORDER BY 1; - EXEC SQL OPEN crsta; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH crsta INTO :h_st, :h_cnt, :h_amt; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE crsta; FAIL(b); } - Bchg(b, T_STATUS, (BFLDOCC)occ, h_st, 0L); - Bchg(b, T_ARG1, (BFLDOCC)occ, (char *)&h_cnt, 0L); - Bchg(b, T_AMT1, (BFLDOCC)occ, (char *)&h_amt, 0L); - occ++; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_RPTSTATUS"); + userlog("CL_RPTSTATUS 진입: 마감처리 시작 (상태별)", svc); + + + /* --- 섹션1: 입력 검증부 --- */ + /* 전역 리포트 - 별도 필수입력 없음 */ + + /* --- 섹션2: 그룹 수 사전 확인부 --- */ + EXEC SQL SELECT count(DISTINCT status) INTO :h_grpcnt + FROM cl_close_log WHERE 1=1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6504); FAIL(b); } + if (h_grpcnt == 0) { + o_rc = 1; o_count = 0; + strcpy(o_msg, "리포트 대상 그룹 없음"); + setl(b, T_COUNT, 0); Bchg(b, T_MSG, 0, o_msg, 0L); OK(b); } - EXEC SQL CLOSE crsta; - setl(b, T_COUNT, occ); + + /* --- 섹션4: 그룹별 집계 커서(상위추출) --- */ + strcpy(topdim, ""); + EXEC SQL DECLARE crpt CURSOR FOR + SELECT status, count(*), coalesce(sum(total_amount),0) FROM cl_close_log + WHERE 1=1 GROUP BY status ORDER BY 2 DESC; + EXEC SQL OPEN crpt; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6505); FAIL(b); } + for (;;) { + EXEC SQL FETCH crpt INTO :h_dim, :h_grpcnt, :h_grpamt; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE crpt; setl(b, T_RC, 6506); FAIL(b); } + if (groups < 20) { + Bchg(b, T_STR2, (BFLDOCC)groups, h_dim, 0L); + setl(b, T_ARG3, h_grpamt); + } + if (h_grpamt > topamt) { + topamt = h_grpamt; + strncpy(topdim, h_dim, sizeof(topdim)-1); topdim[sizeof(topdim)-1]=0; + } + groups++; + } + EXEC SQL CLOSE crpt; + + /* --- 섹션3: 상위그룹 판정 분기부 --- */ + if (groups == 0) { + o_rc = 1; strcpy(o_msg, "상태별 집계 결과 없음"); + } else if (topamt == 0) { + o_rc = 2; strcpy(o_msg, "상태별 집계 금액 0"); + } else { + o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "상태별 최다=%s(%ld)", topdim, topamt); + } + Bchg(b, T_STR1, 0, topdim, 0L); + o_count = groups; o_amount = topamt; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_RPTSTATUS 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1000000000L) { + z_sev = 3; + strcpy(z_msg, "대규모 마감기간"); + } else if (h_zamt >= 100000000L) { + z_sev = 2; + strcpy(z_msg, "중규모 마감기간"); + } else { + z_sev = 1; + strcpy(z_msg, "소규모 마감기간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_RPTSTATUS 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_RPTSTATUS 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_RPTSTATUS 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_RPTSTATUS 상태별 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_RPTSTATUS 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_RPTSTATUS 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_RPTTOPMER.pgc b/app/src/cl/svc/CL_RPTTOPMER.pgc index f0b7e21..9a97616 100644 --- a/app/src/cl/svc/CL_RPTTOPMER.pgc +++ b/app/src/cl/svc/CL_RPTTOPMER.pgc @@ -9,29 +9,165 @@ void CL_RPTTOPMER(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long topn, occ = 0; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long groups = 0; long topamt = 0; char topdim[64]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid, h_net; - char h_merch[24]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_grpcnt, h_grpamt, h_cid; + char h_dim[64], h_key[32]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_RPTTOPMER"); + userlog("CL_RPTTOPMER 진입: 마감처리 시작 (상위가맹점)", svc); + h_cid = getl(b, T_ID1); - topn = getl(b, T_COUNT); - if (topn <= 0 || topn > 50) topn = 5; - EXEC SQL DECLARE crtop CURSOR FOR - SELECT merchant_id, net_amount FROM cl_snapshot - WHERE close_id = :h_cid ORDER BY net_amount DESC, merchant_id; - EXEC SQL OPEN crtop; - if (sqlca.sqlcode < 0) FAIL(b); - while (occ < topn) { - EXEC SQL FETCH crtop INTO :h_merch, :h_net; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE crtop; FAIL(b); } - Bchg(b, T_MERCHANT, (BFLDOCC)occ, h_merch, 0L); - Bchg(b, T_NET, (BFLDOCC)occ, (char *)&h_net, 0L); - occ++; + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0) { + setl(b, T_RC, 6503); + Bchg(b, T_MSG, 0, "대상 마감ID 필수", 0L); FAIL(b); } - EXEC SQL CLOSE crtop; - setl(b, T_COUNT, occ); + + /* --- 섹션2: 그룹 수 사전 확인부 --- */ + EXEC SQL SELECT count(DISTINCT merchant_id) INTO :h_grpcnt + FROM cl_snapshot WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6504); FAIL(b); } + if (h_grpcnt == 0) { + o_rc = 1; o_count = 0; + strcpy(o_msg, "리포트 대상 그룹 없음"); + setl(b, T_COUNT, 0); Bchg(b, T_MSG, 0, o_msg, 0L); OK(b); + } + + /* --- 섹션4: 그룹별 집계 커서(상위추출) --- */ + strcpy(topdim, ""); + EXEC SQL DECLARE crpt CURSOR FOR + SELECT merchant_id, count(*), coalesce(sum(net_amount),0) FROM cl_snapshot + WHERE close_id = :h_cid GROUP BY merchant_id ORDER BY coalesce(sum(net_amount),0) DESC; + EXEC SQL OPEN crpt; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6505); FAIL(b); } + for (;;) { + EXEC SQL FETCH crpt INTO :h_dim, :h_grpcnt, :h_grpamt; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE crpt; setl(b, T_RC, 6506); FAIL(b); } + if (groups < 20) { + Bchg(b, T_STR2, (BFLDOCC)groups, h_dim, 0L); + setl(b, T_ARG3, h_grpamt); + } + if (h_grpamt > topamt) { + topamt = h_grpamt; + strncpy(topdim, h_dim, sizeof(topdim)-1); topdim[sizeof(topdim)-1]=0; + } + groups++; + } + EXEC SQL CLOSE crpt; + + /* --- 섹션3: 상위그룹 판정 분기부 --- */ + if (groups == 0) { + o_rc = 1; strcpy(o_msg, "상위가맹점 집계 결과 없음"); + } else if (topamt == 0) { + o_rc = 2; strcpy(o_msg, "상위가맹점 집계 금액 0"); + } else { + o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "상위가맹점 최다=%s(%ld)", topdim, topamt); + } + Bchg(b, T_STR1, 0, topdim, 0L); + o_count = groups; o_amount = topamt; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_RPTTOPMER 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 500000000L) { + z_sev = 3; + strcpy(z_msg, "고액 마감구간"); + } else if (h_zamt >= 50000000L) { + z_sev = 2; + strcpy(z_msg, "중액 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "소액 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_RPTTOPMER 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_RPTTOPMER 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_RPTTOPMER 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_RPTTOPMER 상위가맹점 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_RPTTOPMER 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_RPTTOPMER 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_SCHEDNEXT.pgc b/app/src/cl/svc/CL_SCHEDNEXT.pgc index ef28334..d29b651 100644 --- a/app/src/cl/svc/CL_SCHEDNEXT.pgc +++ b/app/src/cl/svc/CL_SCHEDNEXT.pgc @@ -8,18 +8,149 @@ void CL_SCHEDNEXT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long metric = 0; int i = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16], h_next[16]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_n1, h_n2, h_n3, h_cid; + char h_key[32], h_status[4], h_out[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_SCHEDNEXT"); + userlog("CL_SCHEDNEXT 진입: 마감처리 시작 (다음 마감 일정: 기준일 다음날부터 주말(토/일)을 건너뛴 첫 영업일을 마감 예정일로 산출.)", svc); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT to_char(d,'YYYY-MM-DD') INTO :h_next FROM ( - SELECT (:h_bizdate::date + s) AS d, - extract(dow from (:h_bizdate::date + s)) AS dw - FROM generate_series(1,7) s) t - WHERE dw NOT IN (0,6) ORDER BY d LIMIT 1; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - Bchg(b, T_KEY1, 0, h_next, 0L); - Bchg(b, T_MSG, 0, "다음 마감 예정 영업일", 0L); + gets_(b, T_STR1, h_period, sizeof(h_period)); + gets_(b, T_KEY1, h_key, sizeof(h_key)); + h_cid = getl(b, T_ID1); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0 && h_cid <= 0) { + setl(b, T_RC, 6601); + Bchg(b, T_MSG, 0, "기준(영업일/기간/마감ID) 중 하나는 필수", 0L); FAIL(b); + } + + /* --- 섹션2·섹션4: 익영업일 산출(공통유틸) --- */ + if (h_bizdate[0] == 0) { + setl(b, T_RC, 6602); + Bchg(b, T_MSG, 0, "기준 영업일 필수", 0L); FAIL(b); + } + { char cur[16], nxt[16]; int skipped = 0; + strncpy(cur, h_bizdate, sizeof(cur)-1); cur[sizeof(cur)-1]=0; + for (i = 0; i < 10; i++) { + if (acq_next_bizday(cur, nxt) < 0) { + setl(b, T_RC, 6603); + Bchg(b, T_MSG, 0, "영업일 계산 오류", 0L); FAIL(b); + } + skipped++; + strncpy(cur, nxt, sizeof(cur)-1); cur[sizeof(cur)-1]=0; + break; + } + Bchg(b, T_STR1, 0, cur, 0L); + metric = skipped; o_count = skipped; } + strcpy(o_msg, "익영업일 산출 완료"); + o_rc = 0; + + /* --- 섹션3b: 규모 부가판정 --- */ + if (metric < 0) metric = 0; + setl(b, T_ARG5, metric); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_SCHEDNEXT 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1000000000L) { + z_sev = 3; + strcpy(z_msg, "대규모 마감기간"); + } else if (h_zamt >= 100000000L) { + z_sev = 2; + strcpy(z_msg, "중규모 마감기간"); + } else { + z_sev = 1; + strcpy(z_msg, "소규모 마감기간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_SCHEDNEXT 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_SCHEDNEXT 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_SCHEDNEXT 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_SCHEDNEXT 다음 마감 일정: 기준일 다음날부터 주말(토/일)을 건너뛴 첫 영업일을 마감 예정일로 산출. cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_SCHEDNEXT 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_SCHEDNEXT 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_SCHEDULE.pgc b/app/src/cl/svc/CL_SCHEDULE.pgc index 74dea36..1eb1888 100644 --- a/app/src/cl/svc/CL_SCHEDULE.pgc +++ b/app/src/cl/svc/CL_SCHEDULE.pgc @@ -9,28 +9,144 @@ void CL_SCHEDULE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long metric = 0; int i = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_have, h_behind; - char h_last[16], h_next[16]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_n1, h_n2, h_n3, h_cid; + char h_key[32], h_status[4], h_out[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; - EXEC SQL SELECT count(*) INTO :h_have FROM cl_close_log - WHERE close_type = 'DAILY' AND status IN ('C','A','L'); - if (sqlca.sqlcode < 0) FAIL(b); - if (h_have == 0) { - setl(b, T_RC, 1); - Bchg(b, T_MSG, 0, "일마감 이력 없음: 초기 일정 수립 필요", 0L); - OK(b); + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_SCHEDULE"); + userlog("CL_SCHEDULE 진입: 마감처리 시작 (마감 일정 관리: 최종 일마감(biz_date 최대) 기준으로 차기 마감 예정일과)", svc); + + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + gets_(b, T_STR1, h_period, sizeof(h_period)); + gets_(b, T_KEY1, h_key, sizeof(h_key)); + h_cid = getl(b, T_ID1); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0 && h_cid <= 0) { + setl(b, T_RC, 6601); + Bchg(b, T_MSG, 0, "기준(영업일/기간/마감ID) 중 하나는 필수", 0L); FAIL(b); } - EXEC SQL SELECT to_char(max(biz_date),'YYYY-MM-DD'), - to_char(max(biz_date) + 1,'YYYY-MM-DD'), - current_date - max(biz_date) - INTO :h_last, :h_next, :h_behind - FROM cl_close_log WHERE close_type = 'DAILY' AND status IN ('C','A','L'); - if (sqlca.sqlcode < 0) FAIL(b); - Bchg(b, T_STR1, 0, h_last, 0L); - Bchg(b, T_STR2, 0, h_next, 0L); - setl(b, T_ARG1, h_behind); - setl(b, T_RC, (h_behind > 1) ? 2 : 0); + + /* --- 섹션2·섹션4: 마감율/일정/SLA 산출 --- */ + EXEC SQL SELECT count(DISTINCT biz_date) INTO :h_n1 FROM purchase + WHERE to_char(biz_date,'YYYY-MM') = substr(:h_period,1,7); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6623); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_n2 FROM cl_close_log + WHERE close_type = 'DAILY' AND status <> 'X' + AND substr(period_key,1,7) = substr(:h_period,1,7); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6624); FAIL(b); } + metric = (h_n1 == 0) ? 0 : (h_n2 * 100 / h_n1); + o_count = h_n2; o_amount = h_n1; + if (h_n1 == 0) { o_rc = 1; strcpy(o_msg, "대상 영업일 없음"); } + else if (metric >= 100) { o_rc = 0; strcpy(o_msg, "마감 100% 완료"); } + else if (metric >= 50) { o_rc = 2; strcpy(o_msg, "마감 진행중"); } + else { o_rc = 3; strcpy(o_msg, "마감 지연"); } + setl(b, T_ARG1, metric); + + /* --- 섹션3b: 규모 부가판정 --- */ + if (metric < 0) metric = 0; + setl(b, T_ARG5, metric); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_SCHEDULE 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1500000000L) { + z_sev = 3; + strcpy(z_msg, "광역 마감규모"); + } else if (h_zamt >= 150000000L) { + z_sev = 2; + strcpy(z_msg, "표준 마감규모"); + } else { + z_sev = 1; + strcpy(z_msg, "경량 마감규모"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_SCHEDULE 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_SCHEDULE 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_SCHEDULE 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_SCHEDULE 마감 일정 관리: 최종 일마감(biz_date 최대) 기준으로 차기 마감 예정일과 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_SCHEDULE 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_SCHEDULE 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_SEGCLOSE.pgc b/app/src/cl/svc/CL_SEGCLOSE.pgc index 51c00ef..7cea8f2 100644 --- a/app/src/cl/svc/CL_SEGCLOSE.pgc +++ b/app/src/cl/svc/CL_SEGCLOSE.pgc @@ -8,27 +8,212 @@ void CL_SEGCLOSE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char memo[48]; - long cid; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long cid = 0; + long fee_calc = 0; + int grade = 0; + char memo[128]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16], h_cat[16]; - long h_cnt, h_amt; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cnt, h_amt, h_fee, h_net; + long h_prev, h_bps; + char h_key[32]; + char h_status[4]; + char h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_SEGCLOSE"); + userlog("CL_SEGCLOSE 진입: 마감처리 시작 (세그먼트(업종) 마감)", svc); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - gets_(b, T_CATEGORY, h_cat, sizeof(h_cat)); - if (h_cat[0] == 0) strcpy(h_cat, "GEN"); - EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_amt - FROM purchase WHERE biz_date = :h_bizdate AND category = :h_cat; - if (sqlca.sqlcode < 0) FAIL(b); + gets_(b, T_STR1, h_period, sizeof(h_period)); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + gets_(b, T_CATEGORY, h_key, sizeof(h_key)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0) { + setl(b, T_RC, 5001); + Bchg(b, T_MSG, 0, "영업일/기간 미입력 - 세그먼트(업종) 마감 불가", 0L); + FAIL(b); + } + if (strlen(h_bizdate) > 0 && strlen(h_bizdate) < 8) { + setl(b, T_RC, 5002); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + FAIL(b); + } + if (h_key[0] == 0) { + setl(b, T_RC, 5003); + Bchg(b, T_MSG, 0, "세그먼트(업종) 구분값 미입력", 0L); + FAIL(b); + } + + /* --- 섹션2: 정책/기존마감 상태 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_prev FROM cl_close_log + WHERE close_type = 'SEG' AND period_key = :h_period AND status <> 'X'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5010); + Bchg(b, T_MSG, 0, "기존 마감 조회 실패", 0L); FAIL(b); + } + if (h_prev > 0) { + userlog("CL_SEGCLOSE %s 이미 마감 %ld건 존재 - 재집계 진행", h_period, h_prev); + } + EXEC SQL SELECT count(*) INTO :h_bps FROM cl_period_lock + WHERE period_key = :h_period AND locked = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5011); FAIL(b); } + if (h_bps > 0) { + setl(b, T_RC, 5012); + Bchg(b, T_MSG, 0, "기간이 잠금상태 - 마감 불가", 0L); FAIL(b); + } + + /* --- 섹션4: 본처리부(집계) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) + INTO :h_cnt, :h_amt, :h_fee, :h_net + FROM purchase WHERE biz_date = :h_bizdate AND category = :h_key; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5020); + Bchg(b, T_MSG, 0, "매입 집계 실패", 0L); FAIL(b); + } + + /* --- 섹션3: 업무규칙 분기부(규모/등급) --- */ + if (h_cnt == 0) { + grade = 0; + strcpy(memo, "SEG:무거래"); + } else if (h_amt >= 1000000000) { + grade = 3; + strcpy(memo, "SEG:대규모"); + } else if (h_amt >= 100000000) { + grade = 2; + strcpy(memo, "SEG:중규모"); + } else { + grade = 1; + strcpy(memo, "SEG:소규모"); + } + fee_calc = acq_fee(h_amt, 25); + if (h_fee == 0 && h_amt > 0) h_fee = fee_calc; + + /* --- 섹션4b: 마감이력 생성 --- */ cid = cldb_next_close_id(); - if (cid < 0) FAIL(b); - snprintf(memo, sizeof(memo), "부문마감:%s", h_cat); - if (cldb_insert_close(cid, "SEG", h_bizdate, h_bizdate, "C", h_cnt, h_amt, memo) < 0) FAIL(b); + if (cid < 0) { setl(b, T_RC, 5030); FAIL(b); } + { char m2[128]; snprintf(m2, sizeof(m2), "%s|%s", memo, h_key); + strncpy(memo, m2, sizeof(memo)-1); memo[sizeof(memo)-1]=0; } + if (cldb_insert_close(cid, "SEG", h_period, + (h_bizdate[0] ? h_bizdate : h_period), + "C", h_cnt, h_amt, memo) < 0) { + setl(b, T_RC, 5031); + Bchg(b, T_MSG, 0, "마감이력 생성 실패", 0L); FAIL(b); + } + + /* --- 섹션6: 집계 확인/요약 재조회부 --- */ + h_kcid = cid; + EXEC SQL SELECT txn_count, total_amount INTO :h_prev, :h_amt + FROM cl_close_log WHERE close_id = :h_kcid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5040); FAIL(b); } + o_count = h_cnt; o_amount = h_amt; + + /* --- 섹션8prep: 상태/결과 세팅 --- */ setl(b, T_ID1, cid); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_amt); - Bchg(b, T_CATEGORY, 0, h_cat, 0L); - userlog("CL_SEGCLOSE cid=%ld cat=%s cnt=%ld 부문마감", cid, h_cat, h_cnt); + setl(b, T_FEE, h_fee); + setl(b, T_NET, h_net); + Bchg(b, T_STATUS, 0, "C", 0L); + snprintf(o_msg, sizeof(o_msg), "세그먼트(업종) 마감완료 등급%d cid=%ld", grade, cid); + o_rc = 0; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_SEGCLOSE 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 2000000000L) { + z_sev = 3; + strcpy(z_msg, "초과대형 마감구간"); + } else if (h_zamt >= 200000000L) { + z_sev = 2; + strcpy(z_msg, "중대형 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "일반 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_SEGCLOSE 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_SEGCLOSE 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_SEGCLOSE 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_SEGCLOSE 세그먼트(업종)마감 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_SEGCLOSE 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_SEGCLOSE 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_SEGVERIFY.pgc b/app/src/cl/svc/CL_SEGVERIFY.pgc index b0da0b3..17d812c 100644 --- a/app/src/cl/svc/CL_SEGVERIFY.pgc +++ b/app/src/cl/svc/CL_SEGVERIFY.pgc @@ -8,25 +8,168 @@ void CL_SEGVERIFY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long delta; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long dcnt = 0, damt = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - char h_cat[16], h_date[16]; - long h_cid, h_la, h_ra; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_hdr_cnt, h_hdr_amt; + long h_real_cnt, h_real_amt; + char h_date[16], h_type[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_SEGVERIFY"); + userlog("CL_SEGVERIFY 진입: 마감처리 시작 (마감 정합 검증)", svc); + h_cid = getl(b, T_ID1); - gets_(b, T_CATEGORY, h_cat, sizeof(h_cat)); - if (h_cat[0] == 0) strcpy(h_cat, "GEN"); - EXEC SQL SELECT total_amount, to_char(biz_date,'YYYY-MM-DD') INTO :h_la, :h_date - FROM cl_close_log WHERE close_id = :h_cid AND status <> 'X'; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_ra - FROM purchase WHERE biz_date = :h_date AND category = :h_cat; - if (sqlca.sqlcode < 0) FAIL(b); - delta = h_la - h_ra; if (delta < 0) delta = -delta; - setl(b, T_AMT1, h_la); - setl(b, T_AMT2, h_ra); - setl(b, T_DELTA, delta); - setl(b, T_RC, (delta == 0) ? 0 : 1); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0 && h_bizdate[0] == 0) { + setl(b, T_RC, 5401); + Bchg(b, T_MSG, 0, "마감ID/영업일 중 하나는 필수", 0L); FAIL(b); + } + + /* --- 섹션2: 마감이력 헤더 조회부 --- */ + if (h_cid > 0) { + EXEC SQL SELECT close_type, txn_count, total_amount, + to_char(biz_date,'YYYY-MM-DD') + INTO :h_type, :h_hdr_cnt, :h_hdr_amt, :h_date + FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5402); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5403); FAIL(b); } + } else { + strcpy(h_date, h_bizdate); + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(total_amount),0) + INTO :h_hdr_cnt, :h_hdr_amt FROM cl_close_log + WHERE period_key = :h_bizdate AND status <> 'X'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5404); FAIL(b); } + } + + /* --- 섹션4: 실매입 재집계부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_real_cnt, :h_real_amt + FROM purchase WHERE biz_date = :h_date; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5405); FAIL(b); } + + /* --- 섹션3: 대사 분기부 --- */ + dcnt = h_hdr_cnt - h_real_cnt; if (dcnt < 0) dcnt = -dcnt; + damt = h_hdr_amt - h_real_amt; if (damt < 0) damt = -damt; + if (dcnt == 0 && damt == 0) { + o_rc = 0; strcpy(o_msg, "건수·금액 완전 일치"); + } else if (dcnt == 0 && damt != 0) { + o_rc = 1; strcpy(o_msg, "건수 일치·금액 불일치"); + } else if (dcnt != 0 && damt == 0) { + o_rc = 2; strcpy(o_msg, "건수 불일치·금액 일치"); + } else { + o_rc = 3; strcpy(o_msg, "건수·금액 모두 불일치"); + } + + /* --- 섹션6: 결과 세팅 --- */ + setl(b, T_ARG1, h_hdr_cnt); + setl(b, T_ARG2, h_real_cnt); + setl(b, T_AMT1, h_hdr_amt); + setl(b, T_AMT2, h_real_amt); + setl(b, T_DELTA, damt); + o_count = dcnt; o_amount = damt; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_SEGVERIFY 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 2000000000L) { + z_sev = 3; + strcpy(z_msg, "초과대형 마감구간"); + } else if (h_zamt >= 200000000L) { + z_sev = 2; + strcpy(z_msg, "중대형 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "일반 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_SEGVERIFY 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_SEGVERIFY 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_SEGVERIFY 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_SEGVERIFY 정합검증 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_SEGVERIFY 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_SEGVERIFY 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_SEMIMONTH.pgc b/app/src/cl/svc/CL_SEMIMONTH.pgc index 0a25b2e..a3329eb 100644 --- a/app/src/cl/svc/CL_SEMIMONTH.pgc +++ b/app/src/cl/svc/CL_SEMIMONTH.pgc @@ -8,32 +8,205 @@ void CL_SEMIMONTH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long cid, half; - char period[24], memo[32]; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long cid = 0; + long fee_calc = 0; + int grade = 0; + char memo[128]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16], h_ym[16]; - long h_day, h_lo, h_hi, h_cnt, h_amt; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cnt, h_amt, h_fee, h_net; + long h_prev, h_bps; + char h_key[32]; + char h_status[4]; + char h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_SEMIMONTH"); + userlog("CL_SEMIMONTH 진입: 마감처리 시작 (반월 마감)", svc); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT extract(day from :h_bizdate::date)::int, - to_char(:h_bizdate::date, 'YYYY-MM') INTO :h_day, :h_ym; - if (sqlca.sqlcode < 0) FAIL(b); - half = (h_day <= 15) ? 1 : 2; - h_lo = (half == 1) ? 1 : 16; - h_hi = (half == 1) ? 15 : 31; - EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_amt - FROM purchase WHERE to_char(biz_date,'YYYY-MM') = :h_ym - AND extract(day from biz_date) BETWEEN :h_lo AND :h_hi; - if (sqlca.sqlcode < 0) FAIL(b); - snprintf(period, sizeof(period), "%s-H%ld", h_ym, half); - snprintf(memo, sizeof(memo), "반월마감:%s", (half == 1) ? "전반" : "후반"); + gets_(b, T_STR1, h_period, sizeof(h_period)); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + strcpy(h_key, ""); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0) { + setl(b, T_RC, 5001); + Bchg(b, T_MSG, 0, "영업일/기간 미입력 - 반월 마감 불가", 0L); + FAIL(b); + } + if (strlen(h_bizdate) > 0 && strlen(h_bizdate) < 8) { + setl(b, T_RC, 5002); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + FAIL(b); + } + + /* --- 섹션2: 정책/기존마감 상태 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_prev FROM cl_close_log + WHERE close_type = 'SEMIMON' AND period_key = :h_period AND status <> 'X'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5010); + Bchg(b, T_MSG, 0, "기존 마감 조회 실패", 0L); FAIL(b); + } + if (h_prev > 0) { + userlog("CL_SEMIMONTH %s 이미 마감 %ld건 존재 - 재집계 진행", h_period, h_prev); + } + EXEC SQL SELECT count(*) INTO :h_bps FROM cl_period_lock + WHERE period_key = :h_period AND locked = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5011); FAIL(b); } + if (h_bps > 0) { + setl(b, T_RC, 5012); + Bchg(b, T_MSG, 0, "기간이 잠금상태 - 마감 불가", 0L); FAIL(b); + } + + /* --- 섹션4: 본처리부(집계) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) + INTO :h_cnt, :h_amt, :h_fee, :h_net + FROM purchase WHERE to_char(biz_date,'YYYY-MM') = :h_period; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5020); + Bchg(b, T_MSG, 0, "매입 집계 실패", 0L); FAIL(b); + } + + /* --- 섹션3: 업무규칙 분기부(규모/등급) --- */ + if (h_cnt == 0) { + grade = 0; + strcpy(memo, "SEMIMON:무거래"); + } else if (h_amt >= 1000000000) { + grade = 3; + strcpy(memo, "SEMIMON:대규모"); + } else if (h_amt >= 100000000) { + grade = 2; + strcpy(memo, "SEMIMON:중규모"); + } else { + grade = 1; + strcpy(memo, "SEMIMON:소규모"); + } + fee_calc = acq_fee(h_amt, 25); + if (h_fee == 0 && h_amt > 0) h_fee = fee_calc; + + /* --- 섹션4b: 마감이력 생성 --- */ cid = cldb_next_close_id(); - if (cid < 0) FAIL(b); - if (cldb_insert_close(cid, "SEMIMON", period, h_bizdate, "C", h_cnt, h_amt, memo) < 0) FAIL(b); + if (cid < 0) { setl(b, T_RC, 5030); FAIL(b); } + if (cldb_insert_close(cid, "SEMIMON", h_period, + (h_bizdate[0] ? h_bizdate : h_period), + "C", h_cnt, h_amt, memo) < 0) { + setl(b, T_RC, 5031); + Bchg(b, T_MSG, 0, "마감이력 생성 실패", 0L); FAIL(b); + } + + /* --- 섹션6: 집계 확인/요약 재조회부 --- */ + h_kcid = cid; + EXEC SQL SELECT txn_count, total_amount INTO :h_prev, :h_amt + FROM cl_close_log WHERE close_id = :h_kcid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5040); FAIL(b); } + o_count = h_cnt; o_amount = h_amt; + + /* --- 섹션8prep: 상태/결과 세팅 --- */ setl(b, T_ID1, cid); - setl(b, T_ARG1, half); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_amt); + setl(b, T_FEE, h_fee); + setl(b, T_NET, h_net); + Bchg(b, T_STATUS, 0, "C", 0L); + snprintf(o_msg, sizeof(o_msg), "반월 마감완료 등급%d cid=%ld", grade, cid); + o_rc = 0; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_SEMIMONTH 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 500000000L) { + z_sev = 3; + strcpy(z_msg, "고액 마감구간"); + } else if (h_zamt >= 50000000L) { + z_sev = 2; + strcpy(z_msg, "중액 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "소액 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_SEMIMONTH 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_SEMIMONTH 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_SEMIMONTH 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_SEMIMONTH 반월마감 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_SEMIMONTH 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_SEMIMONTH 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_SEQ.pgc b/app/src/cl/svc/CL_SEQ.pgc index cd0f5f8..23bdcb0 100644 --- a/app/src/cl/svc/CL_SEQ.pgc +++ b/app/src/cl/svc/CL_SEQ.pgc @@ -9,12 +9,140 @@ void CL_SEQ(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char ctype[16]; - long seq; - gets_(b, T_STR2, ctype, sizeof(ctype)); if (ctype[0] == 0) strcpy(ctype, "DAILY"); - seq = cldb_bump_seq(ctype); - if (seq < 0) FAIL(b); - setl(b, T_ID2, seq); + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long metric = 0; int i = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; + EXEC SQL BEGIN DECLARE SECTION; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_n1, h_n2, h_n3, h_cid; + char h_key[32], h_status[4], h_out[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; + EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_SEQ"); + userlog("CL_SEQ 진입: 마감처리 시작 (마감 순번 발번(dbio upsert).)", svc); + + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + gets_(b, T_STR1, h_period, sizeof(h_period)); + gets_(b, T_KEY1, h_key, sizeof(h_key)); + h_cid = getl(b, T_ID1); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0 && h_cid <= 0) { + setl(b, T_RC, 6601); + Bchg(b, T_MSG, 0, "기준(영업일/기간/마감ID) 중 하나는 필수", 0L); FAIL(b); + } + + /* --- 섹션2·섹션4: 마감 순번/SLA 발번(seq registry) --- */ + if (h_key[0] == 0) strcpy(h_key, "DAILY"); + { long seq = cldb_bump_seq(h_key); + if (seq < 0) { + setl(b, T_RC, 6604); + Bchg(b, T_MSG, 0, "순번 발번 실패", 0L); FAIL(b); + } + metric = seq; o_count = seq; + setl(b, T_ID1, seq); } + snprintf(o_msg, sizeof(o_msg), "%s 순번=%ld 발번", metric); + o_rc = 0; + + /* --- 섹션3b: 규모 부가판정 --- */ + if (metric < 0) metric = 0; + setl(b, T_ARG5, metric); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_SEQ 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1000000000L) { + z_sev = 3; + strcpy(z_msg, "대규모 마감기간"); + } else if (h_zamt >= 100000000L) { + z_sev = 2; + strcpy(z_msg, "중규모 마감기간"); + } else { + z_sev = 1; + strcpy(z_msg, "소규모 마감기간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_SEQ 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_SEQ 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_SEQ 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_SEQ 마감 순번 발번(dbio upsert). cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_SEQ 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_SEQ 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_SEQAUDIT.pgc b/app/src/cl/svc/CL_SEQAUDIT.pgc index 551d538..c0cae6b 100644 --- a/app/src/cl/svc/CL_SEQAUDIT.pgc +++ b/app/src/cl/svc/CL_SEQAUDIT.pgc @@ -9,31 +9,142 @@ void CL_SEQAUDIT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long types = 0, drift = 0; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long metric = 0; int i = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_seq, h_actual; - char h_type[16]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_n1, h_n2, h_n3, h_cid; + char h_key[32], h_status[4], h_out[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; - EXEC SQL DECLARE cseqa CURSOR FOR - SELECT r.close_type, r.last_seq, - (SELECT count(*) FROM cl_close_log c WHERE c.close_type = r.close_type) - FROM cl_seq_reg r ORDER BY r.close_type; - EXEC SQL OPEN cseqa; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH cseqa INTO :h_type, :h_seq, :h_actual; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cseqa; FAIL(b); } - types++; - if (h_seq < h_actual) { - Bchg(b, T_STR2, (BFLDOCC)drift, h_type, 0L); - drift++; - } + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_SEQAUDIT"); + userlog("CL_SEQAUDIT 진입: 마감처리 시작 (마감 순번 감사: cl_seq_reg 의 유형별 last_seq 와 실제 cl_close_log 의)", svc); + + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + gets_(b, T_STR1, h_period, sizeof(h_period)); + gets_(b, T_KEY1, h_key, sizeof(h_key)); + h_cid = getl(b, T_ID1); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0 && h_cid <= 0) { + setl(b, T_RC, 6601); + Bchg(b, T_MSG, 0, "기준(영업일/기간/마감ID) 중 하나는 필수", 0L); FAIL(b); } - EXEC SQL CLOSE cseqa; - setl(b, T_COUNT, types); - setl(b, T_ARG1, drift); - setl(b, T_RC, (drift == 0) ? 0 : 1); + + /* --- 섹션2·섹션4: 마감 이력 감사 집계 --- */ + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN status='X' THEN 1 ELSE 0 END),0), + coalesce(sum(CASE WHEN status='L' THEN 1 ELSE 0 END),0) + INTO :h_n1, :h_n2, :h_n3 FROM cl_close_log + WHERE (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6622); FAIL(b); } + metric = h_n1; o_count = h_n1; + setl(b, T_ARG1, h_n2); + setl(b, T_ARG2, h_n3); + if (h_n1 == 0) { o_rc = 1; strcpy(o_msg, "감사 대상 없음"); } + else if (h_n2 > 0) { o_rc = 2; strcpy(o_msg, "취소 이력 포함"); } + else { o_rc = 0; strcpy(o_msg, "감사 정상"); } + + /* --- 섹션3b: 규모 부가판정 --- */ + if (metric < 0) metric = 0; + setl(b, T_ARG5, metric); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_SEQAUDIT 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1000000000L) { + z_sev = 3; + strcpy(z_msg, "대규모 마감기간"); + } else if (h_zamt >= 100000000L) { + z_sev = 2; + strcpy(z_msg, "중규모 마감기간"); + } else { + z_sev = 1; + strcpy(z_msg, "소규모 마감기간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_SEQAUDIT 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_SEQAUDIT 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_SEQAUDIT 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_SEQAUDIT 마감 순번 감사: cl_seq_reg 의 유형별 last_seq 와 실제 cl_close_log 의 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_SEQAUDIT 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_SEQAUDIT 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_SLABREACH.pgc b/app/src/cl/svc/CL_SLABREACH.pgc index f7e86dc..ae29cac 100644 --- a/app/src/cl/svc/CL_SLABREACH.pgc +++ b/app/src/cl/svc/CL_SLABREACH.pgc @@ -8,17 +8,149 @@ void CL_SLABREACH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long mask = 0; + long blockers = 0; + int over = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; - long h_n; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_n1, h_n2, h_n3, h_n4; + long h_lim; + char h_mer[16]; + char h_status[4]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_SLABREACH"); + userlog("CL_SLABREACH 진입: 마감처리 시작 (SLA 위반 개시(O) 점검)", svc); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT count(*) INTO :h_n FROM cl_close_log - WHERE status = 'O' AND biz_date < :h_bizdate::date; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_n); - setl(b, T_RC, (h_n > 0) ? 1 : 0); - Bchg(b, T_MSG, 0, (h_n > 0) ? "SLA 위반 미완결 마감 존재" : "SLA 위반 없음", 0L); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0) { + setl(b, T_RC, 5308); + Bchg(b, T_MSG, 0, "영업일 미입력 - SLA 위반 개시(O) 점검 불가", 0L); + FAIL(b); + } + if (strlen(h_bizdate) < 8) { + setl(b, T_RC, 5408); + Bchg(b, T_MSG, 0, "영업일 형식 오류", 0L); FAIL(b); + } + + /* --- 섹션2: 기준/정책 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_n4 FROM cl_close_log + WHERE close_type = 'DAILY' AND period_key = :h_bizdate AND status <> 'X'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5309); FAIL(b); } + + /* --- 섹션3·섹션4: 단일조건 건수 판정 --- */ + EXEC SQL SELECT count(*) INTO :h_n1 FROM cl_close_log WHERE status = 'O' AND period_key < :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5310); FAIL(b); } + o_count = h_n1; + if (h_n1 == 0) { + o_rc = 0; + strcpy(o_msg, "SLA 위반 개시(O) 없음 - 마감 가능"); + } else if (h_n1 < 10) { + o_rc = 1; + strcpy(o_msg, "SLA 위반 개시(O) 소량 존재 - 확인 필요"); + } else { + o_rc = 2; + strcpy(o_msg, "SLA 위반 개시(O) 다수 존재 - 마감 불가"); + } + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_SLABREACH 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1000000000L) { + z_sev = 3; + strcpy(z_msg, "대규모 마감기간"); + } else if (h_zamt >= 100000000L) { + z_sev = 2; + strcpy(z_msg, "중규모 마감기간"); + } else { + z_sev = 1; + strcpy(z_msg, "소규모 마감기간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_SLABREACH 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_SLABREACH 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_SLABREACH 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_SLABREACH SLA 위반 개시(O)점검 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_SLABREACH 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_SLABREACH 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_SLAMON.pgc b/app/src/cl/svc/CL_SLAMON.pgc index 8afac4a..ea657a5 100644 --- a/app/src/cl/svc/CL_SLAMON.pgc +++ b/app/src/cl/svc/CL_SLAMON.pgc @@ -9,29 +9,144 @@ void CL_SLAMON(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long metric = 0; int i = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_late, h_all; - char h_prefix[16], h_cut[8], h_worst[8]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_n1, h_n2, h_n3, h_cid; + char h_key[32], h_status[4], h_out[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; - gets_(b, T_STR1, h_prefix, sizeof(h_prefix)); - gets_(b, T_STR2, h_cut, sizeof(h_cut)); - if (h_cut[0] == 0) strcpy(h_cut, "1000"); - if (h_prefix[0] == 0) { Bchg(b, T_MSG, 0, "기간 프리픽스 필요", 0L); FAIL(b); } - EXEC SQL SELECT count(*), - coalesce(sum(CASE WHEN to_char(created_at,'HH24MI') > :h_cut - THEN 1 ELSE 0 END),0), - coalesce(max(to_char(created_at,'HH24MI')),'0000') - INTO :h_all, :h_late, :h_worst + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_SLAMON"); + userlog("CL_SLAMON 진입: 마감처리 시작 (마감 SLA 모니터: 기간 프리픽스의 일마감 중 기준시각(T_STR2=HHMI, 기본 1000))", svc); + + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + gets_(b, T_STR1, h_period, sizeof(h_period)); + gets_(b, T_KEY1, h_key, sizeof(h_key)); + h_cid = getl(b, T_ID1); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0 && h_cid <= 0) { + setl(b, T_RC, 6601); + Bchg(b, T_MSG, 0, "기준(영업일/기간/마감ID) 중 하나는 필수", 0L); FAIL(b); + } + + /* --- 섹션2·섹션4: 마감율/일정/SLA 산출 --- */ + EXEC SQL SELECT count(DISTINCT biz_date) INTO :h_n1 FROM purchase + WHERE to_char(biz_date,'YYYY-MM') = substr(:h_period,1,7); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6623); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_n2 FROM cl_close_log + WHERE close_type = 'DAILY' AND status <> 'X' + AND substr(period_key,1,7) = substr(:h_period,1,7); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6624); FAIL(b); } + metric = (h_n1 == 0) ? 0 : (h_n2 * 100 / h_n1); + o_count = h_n2; o_amount = h_n1; + if (h_n1 == 0) { o_rc = 1; strcpy(o_msg, "대상 영업일 없음"); } + else if (metric >= 100) { o_rc = 0; strcpy(o_msg, "마감 100% 완료"); } + else if (metric >= 50) { o_rc = 2; strcpy(o_msg, "마감 진행중"); } + else { o_rc = 3; strcpy(o_msg, "마감 지연"); } + setl(b, T_ARG1, metric); + + /* --- 섹션3b: 규모 부가판정 --- */ + if (metric < 0) metric = 0; + setl(b, T_ARG5, metric); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt FROM cl_close_log - WHERE close_type = 'DAILY' AND period_key LIKE :h_prefix || '%' - AND status <> 'X'; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_all); - setl(b, T_ARG1, h_late); - Bchg(b, T_STR3, 0, h_worst, 0L); - setl(b, T_RC, (h_late > 0) ? 1 : 0); - if (h_late > 0) userlog("CL_SLAMON %s SLA 위반 %ld/%ld건 (최지연 %s)", - h_prefix, h_late, h_all, h_worst); + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_SLAMON 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 2000000000L) { + z_sev = 3; + strcpy(z_msg, "초과대형 마감구간"); + } else if (h_zamt >= 200000000L) { + z_sev = 2; + strcpy(z_msg, "중대형 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "일반 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_SLAMON 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_SLAMON 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_SLAMON 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_SLAMON 마감 SLA 모니터: 기간 프리픽스의 일마감 중 기준시각(T_STR2=HHMI, 기본 1000) cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_SLAMON 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_SLAMON 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_SLASET.pgc b/app/src/cl/svc/CL_SLASET.pgc index 1029af5..795d3d1 100644 --- a/app/src/cl/svc/CL_SLASET.pgc +++ b/app/src/cl/svc/CL_SLASET.pgc @@ -8,21 +8,140 @@ void CL_SLASET(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long metric = 0; int i = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - char h_ct[16]; - long h_hours; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_n1, h_n2, h_n3, h_cid; + char h_key[32], h_status[4], h_out[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, h_ct, sizeof(h_ct)); - if (h_ct[0] == 0) strcpy(h_ct, "DAILY"); - h_hours = getl(b, T_ARG1); - if (h_hours <= 0) h_hours = 2; - EXEC SQL INSERT INTO cl_seq_reg (close_type, last_seq, updated_at) - VALUES (:h_ct, :h_hours, now()) - ON CONFLICT (close_type) DO UPDATE SET last_seq = :h_hours, updated_at = now(); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_ARG1, h_hours); - Bchg(b, T_KEY1, 0, h_ct, 0L); - Bchg(b, T_MSG, 0, "마감 SLA 목표 등록", 0L); + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_SLASET"); + userlog("CL_SLASET 진입: 마감처리 시작 (마감 SLA 등록: close_type(T_KEY1)별 마감 목표시간(T_ARG1, 시간)을 seq_reg 에 upsert.)", svc); + + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + gets_(b, T_STR1, h_period, sizeof(h_period)); + gets_(b, T_KEY1, h_key, sizeof(h_key)); + h_cid = getl(b, T_ID1); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0 && h_cid <= 0) { + setl(b, T_RC, 6601); + Bchg(b, T_MSG, 0, "기준(영업일/기간/마감ID) 중 하나는 필수", 0L); FAIL(b); + } + + /* --- 섹션2·섹션4: 마감 순번/SLA 발번(seq registry) --- */ + if (h_key[0] == 0) strcpy(h_key, "DAILY"); + { long seq = cldb_bump_seq(h_key); + if (seq < 0) { + setl(b, T_RC, 6604); + Bchg(b, T_MSG, 0, "순번 발번 실패", 0L); FAIL(b); + } + metric = seq; o_count = seq; + setl(b, T_ID1, seq); } + snprintf(o_msg, sizeof(o_msg), "%s 순번=%ld 발번", metric); + o_rc = 0; + + /* --- 섹션3b: 규모 부가판정 --- */ + if (metric < 0) metric = 0; + setl(b, T_ARG5, metric); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_SLASET 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1000000000L) { + z_sev = 3; + strcpy(z_msg, "대규모 마감기간"); + } else if (h_zamt >= 100000000L) { + z_sev = 2; + strcpy(z_msg, "중규모 마감기간"); + } else { + z_sev = 1; + strcpy(z_msg, "소규모 마감기간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_SLASET 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_SLASET 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_SLASET 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_SLASET 마감 SLA 등록: close_type(T_KEY1)별 마감 목표시간(T_ARG1, 시간)을 seq_reg 에 upsert. cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_SLASET 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_SLASET 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_SNAPDIFF.pgc b/app/src/cl/svc/CL_SNAPDIFF.pgc index 03a391e..573aeed 100644 --- a/app/src/cl/svc/CL_SNAPDIFF.pgc +++ b/app/src/cl/svc/CL_SNAPDIFF.pgc @@ -9,24 +9,151 @@ void CL_SNAPDIFF(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long dg = 0, dn = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_a, h_b, h_ga, h_gb, h_na, h_nb, h_ma, h_mb; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_c1, h_c2, h_g1, h_g2, h_n1, h_n2; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; - h_a = getl(b, T_ID1); - h_b = getl(b, T_ID2); - if (h_a == h_b) { Bchg(b, T_MSG, 0, "서로 다른 두 마감 필요", 0L); FAIL(b); } + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_SNAPDIFF"); + userlog("CL_SNAPDIFF 진입: 마감처리 시작 (스냅샷 비교)", svc); + + h_c1 = getl(b, T_ID1); + h_c2 = getl(b, T_ID2); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_c1 <= 0 || h_c2 <= 0) { + setl(b, T_RC, 6001); + Bchg(b, T_MSG, 0, "비교할 두 마감ID(T_ID1,T_ID2) 필수", 0L); FAIL(b); + } + if (h_c1 == h_c2) { + setl(b, T_RC, 6002); + Bchg(b, T_MSG, 0, "동일 마감 비교 불가", 0L); FAIL(b); + } + + /* --- 섹션2·섹션4: 두 스냅샷 집계부 --- */ EXEC SQL SELECT coalesce(sum(gross_amount),0), coalesce(sum(net_amount),0), count(DISTINCT merchant_id) - INTO :h_ga, :h_na, :h_ma FROM cl_snapshot WHERE close_id = :h_a; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT coalesce(sum(gross_amount),0), coalesce(sum(net_amount),0), - count(DISTINCT merchant_id) - INTO :h_gb, :h_nb, :h_mb FROM cl_snapshot WHERE close_id = :h_b; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_AMT1, h_ga - h_gb); - setl(b, T_AMT2, h_na - h_nb); - setl(b, T_ARG1, h_ma - h_mb); - setl(b, T_RC, (h_ga == h_gb && h_na == h_nb && h_ma == h_mb) ? 0 : 1); + INTO :h_g1, :h_n1, :h_c1 + FROM cl_snapshot WHERE close_id = :h_c1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6003); FAIL(b); } + h_c1 = getl(b, T_ID1); + EXEC SQL SELECT coalesce(sum(gross_amount),0), coalesce(sum(net_amount),0) + INTO :h_g2, :h_n2 + FROM cl_snapshot WHERE close_id = :h_c2; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6004); FAIL(b); } + + /* --- 섹션3: 편차 분기부 --- */ + dg = h_g1 - h_g2; if (dg < 0) dg = -dg; + dn = h_n1 - h_n2; if (dn < 0) dn = -dn; + if (dg == 0 && dn == 0) { + o_rc = 0; strcpy(o_msg, "두 마감 스냅샷 동일"); + } else if (dg > dn) { + o_rc = 1; strcpy(o_msg, "gross 편차 우세"); + } else { + o_rc = 2; strcpy(o_msg, "net 편차 우세"); + } + setl(b, T_AMT1, h_g1); + setl(b, T_AMT2, h_g2); + setl(b, T_DELTA, dg); + o_amount = dg; o_count = dn; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_SNAPDIFF 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1500000000L) { + z_sev = 3; + strcpy(z_msg, "광역 마감규모"); + } else if (h_zamt >= 150000000L) { + z_sev = 2; + strcpy(z_msg, "표준 마감규모"); + } else { + z_sev = 1; + strcpy(z_msg, "경량 마감규모"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_SNAPDIFF 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_SNAPDIFF 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_SNAPDIFF 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_SNAPDIFF 스냅샷비교 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_SNAPDIFF 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_SNAPDIFF 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_SNAPMER.pgc b/app/src/cl/svc/CL_SNAPMER.pgc index be67355..c8681b3 100644 --- a/app/src/cl/svc/CL_SNAPMER.pgc +++ b/app/src/cl/svc/CL_SNAPMER.pgc @@ -9,24 +9,151 @@ void CL_SNAPMER(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long dg = 0, dn = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_a, h_b, h_cnt, h_worst; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_c1, h_c2, h_g1, h_g2, h_n1, h_n2; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; - h_a = getl(b, T_ID1); - h_b = getl(b, T_ID2); - if (h_a == h_b) { Bchg(b, T_MSG, 0, "서로 다른 두 마감 필요", 0L); FAIL(b); } - EXEC SQL SELECT count(*), coalesce(max(abs(d)),0) INTO :h_cnt, :h_worst FROM ( - SELECT coalesce(sa.g,0) - coalesce(sb.g,0) AS d FROM - (SELECT merchant_id, sum(gross_amount) AS g FROM cl_snapshot - WHERE close_id = :h_a GROUP BY merchant_id) sa - FULL JOIN - (SELECT merchant_id, sum(gross_amount) AS g FROM cl_snapshot - WHERE close_id = :h_b GROUP BY merchant_id) sb - ON sa.merchant_id = sb.merchant_id) z; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); - setl(b, T_DELTA, h_worst); - setl(b, T_RC, (h_worst == 0) ? 0 : 1); + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_SNAPMER"); + userlog("CL_SNAPMER 진입: 마감처리 시작 (스냅샷 비교)", svc); + + h_c1 = getl(b, T_ID1); + h_c2 = getl(b, T_ID2); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_c1 <= 0 || h_c2 <= 0) { + setl(b, T_RC, 6001); + Bchg(b, T_MSG, 0, "비교할 두 마감ID(T_ID1,T_ID2) 필수", 0L); FAIL(b); + } + if (h_c1 == h_c2) { + setl(b, T_RC, 6002); + Bchg(b, T_MSG, 0, "동일 마감 비교 불가", 0L); FAIL(b); + } + + /* --- 섹션2·섹션4: 두 스냅샷 집계부 --- */ + EXEC SQL SELECT coalesce(sum(gross_amount),0), coalesce(sum(net_amount),0), + count(DISTINCT merchant_id) + INTO :h_g1, :h_n1, :h_c1 + FROM cl_snapshot WHERE close_id = :h_c1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6003); FAIL(b); } + h_c1 = getl(b, T_ID1); + EXEC SQL SELECT coalesce(sum(gross_amount),0), coalesce(sum(net_amount),0) + INTO :h_g2, :h_n2 + FROM cl_snapshot WHERE close_id = :h_c2; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6004); FAIL(b); } + + /* --- 섹션3: 편차 분기부 --- */ + dg = h_g1 - h_g2; if (dg < 0) dg = -dg; + dn = h_n1 - h_n2; if (dn < 0) dn = -dn; + if (dg == 0 && dn == 0) { + o_rc = 0; strcpy(o_msg, "두 마감 스냅샷 동일"); + } else if (dg > dn) { + o_rc = 1; strcpy(o_msg, "gross 편차 우세"); + } else { + o_rc = 2; strcpy(o_msg, "net 편차 우세"); + } + setl(b, T_AMT1, h_g1); + setl(b, T_AMT2, h_g2); + setl(b, T_DELTA, dg); + o_amount = dg; o_count = dn; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_SNAPMER 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 2000000000L) { + z_sev = 3; + strcpy(z_msg, "초과대형 마감구간"); + } else if (h_zamt >= 200000000L) { + z_sev = 2; + strcpy(z_msg, "중대형 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "일반 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_SNAPMER 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_SNAPMER 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_SNAPMER 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_SNAPMER 스냅샷비교 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_SNAPMER 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_SNAPMER 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_SNAPSHOT.pgc b/app/src/cl/svc/CL_SNAPSHOT.pgc index 2cbc0d3..fec43e0 100644 --- a/app/src/cl/svc/CL_SNAPSHOT.pgc +++ b/app/src/cl/svc/CL_SNAPSHOT.pgc @@ -9,28 +9,172 @@ void CL_SNAPSHOT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], period[16]; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid, h_cnt; - char h_bizdate[16], h_period[16]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_cnt, h_exist; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_SNAPSHOT"); + userlog("CL_SNAPSHOT 진입: 마감처리 시작 (마감 스냅샷 고정)", svc); h_cid = getl(b, T_ID1); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - gets_(b, T_STR1, period, sizeof(period)); if (period[0] == 0) strcpy(period, bizdate); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - strncpy(h_period, period, sizeof(h_period)-1); h_period[sizeof(h_period)-1] = 0; + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + gets_(b, T_STR1, h_period, sizeof(h_period)); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0) { + setl(b, T_RC, 6101); + Bchg(b, T_MSG, 0, "스냅샷 대상 마감ID 필수", 0L); FAIL(b); + } + if (h_bizdate[0] == 0) { + setl(b, T_RC, 6102); + Bchg(b, T_MSG, 0, "스냅샷 기준 영업일 필수", 0L); FAIL(b); + } + + /* --- 섹션2: 마감 상태/기존 스냅샷 확인부 --- */ + EXEC SQL SELECT count(*) INTO :h_exist FROM cl_close_log + WHERE close_id = :h_cid AND status <> 'X'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6103); FAIL(b); } + if (h_exist == 0) { + setl(b, T_RC, 6104); + Bchg(b, T_MSG, 0, "유효 마감 없음 - 스냅샷 불가", 0L); FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_exist FROM cl_snapshot WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6105); FAIL(b); } + if (h_exist > 0) { + EXEC SQL DELETE FROM cl_snapshot WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6106); + Bchg(b, T_MSG, 0, "기존 스냅샷 제거 실패", 0L); FAIL(b); } + userlog("CL_SNAPSHOT cid=%ld 기존 스냅샷 %ld건 재작성", h_cid, h_exist); + } + + /* --- 섹션4: merchant_summary -> cl_snapshot 고정 복사 --- */ EXEC SQL INSERT INTO cl_snapshot (snapshot_id, close_id, period_key, biz_date, merchant_id, gross_amount, fee_amount, net_amount, txn_count) SELECT nextval('cl_snapshot_seq'), :h_cid, :h_period, :h_bizdate, - ms.merchant_id, ms.gross_amount, ms.fee_amount, ms.net_amount, ms.txn_count + ms.merchant_id, ms.gross_amount, ms.fee_amount, + ms.net_amount, ms.txn_count FROM merchant_summary ms WHERE ms.biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) { userlog("CL_SNAPSHOT FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 6107); + Bchg(b, T_MSG, 0, "스냅샷 적재 실패", 0L); FAIL(b); + } h_cnt = sqlca.sqlerrd[2]; - setl(b, T_COUNT, h_cnt); - userlog("CL_SNAPSHOT cid=%ld 가맹점 %ld건 스냅샷", h_cid, h_cnt); + + /* --- 섹션3: 결과 규모 분기부 --- */ + if (h_cnt == 0) { + o_rc = 1; strcpy(o_msg, "복사 대상 요약 없음 - 빈 스냅샷"); + } else if (h_cnt < 100) { + o_rc = 0; strcpy(o_msg, "스냅샷 정상 고정"); + } else { + o_rc = 0; strcpy(o_msg, "대량 스냅샷 고정 완료"); + } + + /* --- 섹션6: 스냅샷 총액 재조회부 --- */ + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :h_exist + FROM cl_snapshot WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6108); FAIL(b); } + o_count = h_cnt; o_amount = h_exist; + setl(b, T_GROSS, h_exist); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_SNAPSHOT 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1000000000L) { + z_sev = 3; + strcpy(z_msg, "대규모 마감기간"); + } else if (h_zamt >= 100000000L) { + z_sev = 2; + strcpy(z_msg, "중규모 마감기간"); + } else { + z_sev = 1; + strcpy(z_msg, "소규모 마감기간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_SNAPSHOT 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_SNAPSHOT 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_SNAPSHOT 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_SNAPSHOT 스냅샷 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_SNAPSHOT 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_SNAPSHOT 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_SNAPXCHECK.pgc b/app/src/cl/svc/CL_SNAPXCHECK.pgc index bcc3e45..8c7e51b 100644 --- a/app/src/cl/svc/CL_SNAPXCHECK.pgc +++ b/app/src/cl/svc/CL_SNAPXCHECK.pgc @@ -9,33 +9,159 @@ void CL_SNAPXCHECK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long diff, bps = 0; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long d1 = 0, d2 = 0, worst = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid, h_snap, h_led; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_hdr, h_real, h_snap; char h_date[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_SNAPXCHECK"); + userlog("CL_SNAPXCHECK 진입: 마감처리 시작 (스냅샷정산 크로스검증)", svc); + h_cid = getl(b, T_ID1); - EXEC SQL SELECT to_char(biz_date,'YYYY-MM-DD') INTO :h_date - FROM cl_close_log WHERE close_id = :h_cid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0) { + setl(b, T_RC, 5501); + Bchg(b, T_MSG, 0, "마감ID(T_ID1) 필수", 0L); FAIL(b); + } + + /* --- 섹션2: 마감이력 헤더 조회부 --- */ + EXEC SQL SELECT total_amount, to_char(biz_date,'YYYY-MM-DD') + INTO :h_hdr, :h_date FROM cl_close_log + WHERE close_id = :h_cid AND status <> 'X'; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5502); + Bchg(b, T_MSG, 0, "유효 마감이력 없음(취소/미존재)", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5503); FAIL(b); } + + /* --- 섹션4: 실매입/스냅샷 집계부 --- */ + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_real + FROM purchase WHERE biz_date = :h_date; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5504); FAIL(b); } EXEC SQL SELECT coalesce(sum(net_amount),0) INTO :h_snap FROM cl_snapshot WHERE close_id = :h_cid; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_led - FROM ledger WHERE biz_date = :h_date AND entry_type = 'SETTLE'; - if (sqlca.sqlcode < 0) FAIL(b); - diff = h_snap - h_led; - if (h_snap != 0) { - bps = (diff * 10000) / h_snap; - if (bps < 0) bps = -bps; - } else if (h_led != 0) { - bps = 10000; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5505); FAIL(b); } + + /* --- 섹션3: 3원 대사 분기부 --- */ + d1 = h_hdr - h_real; if (d1 < 0) d1 = -d1; + d2 = (h_snap == 0) ? 0 : (h_hdr - h_snap); if (d2 < 0) d2 = -d2; + worst = (d1 > d2) ? d1 : d2; + if (worst == 0) { + o_rc = 0; strcpy(o_msg, "헤더=매입=스냅샷 3원 일치"); + } else if (d1 != 0 && d2 == 0) { + o_rc = 1; strcpy(o_msg, "헤더-매입 불일치(스냅샷은 일치)"); + } else if (d1 == 0 && d2 != 0) { + o_rc = 2; strcpy(o_msg, "헤더-스냅샷 불일치(매입은 일치)"); + } else { + o_rc = 3; strcpy(o_msg, "3원 전부 불일치 - 재마감 검토"); } - setl(b, T_AMT1, h_snap); - setl(b, T_AMT2, h_led); - setl(b, T_DELTA, diff); - setl(b, T_ARG1, bps); - setl(b, T_RC, (bps > 0) ? 1 : 0); + + /* --- 섹션6: 편차 세팅 --- */ + setl(b, T_AMT1, h_hdr); + setl(b, T_AMT2, h_real); + setl(b, T_AMT3, h_snap); + setl(b, T_DELTA, worst); + o_count = (worst == 0) ? 0 : 1; o_amount = worst; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_SNAPXCHECK 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 2000000000L) { + z_sev = 3; + strcpy(z_msg, "초과대형 마감구간"); + } else if (h_zamt >= 200000000L) { + z_sev = 2; + strcpy(z_msg, "중대형 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "일반 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_SNAPXCHECK 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_SNAPXCHECK 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_SNAPXCHECK 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_SNAPXCHECK 스냅샷정산크로스 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_SNAPXCHECK 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_SNAPXCHECK 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_STATS.pgc b/app/src/cl/svc/CL_STATS.pgc index 91232a4..9d6da79 100644 --- a/app/src/cl/svc/CL_STATS.pgc +++ b/app/src/cl/svc/CL_STATS.pgc @@ -9,18 +9,161 @@ void CL_STATS(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char ctype[16]; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long groups = 0; long topamt = 0; char topdim[64]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_amt; - char h_type[16]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_grpcnt, h_grpamt, h_cid; + char h_dim[64], h_key[32]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; - gets_(b, T_STR2, ctype, sizeof(ctype)); if (ctype[0] == 0) strcpy(ctype, "DAILY"); - strncpy(h_type, ctype, sizeof(h_type)-1); h_type[sizeof(h_type)-1] = 0; - EXEC SQL SELECT count(*), coalesce(sum(total_amount),0) INTO :h_cnt, :h_amt - FROM cl_close_log WHERE close_type = :h_type AND status <> 'X'; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_amt); + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_STATS"); + userlog("CL_STATS 진입: 마감처리 시작 (유형별통계)", svc); + + + /* --- 섹션1: 입력 검증부 --- */ + /* 전역 리포트 - 별도 필수입력 없음 */ + + /* --- 섹션2: 그룹 수 사전 확인부 --- */ + EXEC SQL SELECT count(DISTINCT close_type) INTO :h_grpcnt + FROM cl_close_log WHERE 1=1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6504); FAIL(b); } + if (h_grpcnt == 0) { + o_rc = 1; o_count = 0; + strcpy(o_msg, "리포트 대상 그룹 없음"); + setl(b, T_COUNT, 0); Bchg(b, T_MSG, 0, o_msg, 0L); OK(b); + } + + /* --- 섹션4: 그룹별 집계 커서(상위추출) --- */ + strcpy(topdim, ""); + EXEC SQL DECLARE crpt CURSOR FOR + SELECT close_type, count(*), coalesce(sum(total_amount),0) FROM cl_close_log + WHERE 1=1 GROUP BY close_type ORDER BY 2 DESC; + EXEC SQL OPEN crpt; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6505); FAIL(b); } + for (;;) { + EXEC SQL FETCH crpt INTO :h_dim, :h_grpcnt, :h_grpamt; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE crpt; setl(b, T_RC, 6506); FAIL(b); } + if (groups < 20) { + Bchg(b, T_STR2, (BFLDOCC)groups, h_dim, 0L); + setl(b, T_ARG3, h_grpamt); + } + if (h_grpamt > topamt) { + topamt = h_grpamt; + strncpy(topdim, h_dim, sizeof(topdim)-1); topdim[sizeof(topdim)-1]=0; + } + groups++; + } + EXEC SQL CLOSE crpt; + + /* --- 섹션3: 상위그룹 판정 분기부 --- */ + if (groups == 0) { + o_rc = 1; strcpy(o_msg, "유형별통계 집계 결과 없음"); + } else if (topamt == 0) { + o_rc = 2; strcpy(o_msg, "유형별통계 집계 금액 0"); + } else { + o_rc = 0; + snprintf(o_msg, sizeof(o_msg), "유형별통계 최다=%s(%ld)", topdim, topamt); + } + Bchg(b, T_STR1, 0, topdim, 0L); + o_count = groups; o_amount = topamt; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_STATS 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1500000000L) { + z_sev = 3; + strcpy(z_msg, "광역 마감규모"); + } else if (h_zamt >= 150000000L) { + z_sev = 2; + strcpy(z_msg, "표준 마감규모"); + } else { + z_sev = 1; + strcpy(z_msg, "경량 마감규모"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_STATS 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_STATS 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_STATS 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_STATS 유형별통계 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_STATS 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_STATS 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_STATUS.pgc b/app/src/cl/svc/CL_STATUS.pgc index cccbddf..9f7aea3 100644 --- a/app/src/cl/svc/CL_STATUS.pgc +++ b/app/src/cl/svc/CL_STATUS.pgc @@ -9,10 +9,140 @@ void CL_STATUS(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long cid = getl(b, T_ID1); - char st[4]; - if (cldb_get_close_status(cid, st) < 0) FAIL(b); - Bchg(b, T_STATUS, 0, st, 0L); + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long metric = 0; int i = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; + EXEC SQL BEGIN DECLARE SECTION; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_n1, h_n2, h_n3, h_cid; + char h_key[32], h_status[4], h_out[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; + EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_STATUS"); + userlog("CL_STATUS 진입: 마감처리 시작 (마감 상태 조회(dbio getter).)", svc); + + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + gets_(b, T_STR1, h_period, sizeof(h_period)); + gets_(b, T_KEY1, h_key, sizeof(h_key)); + h_cid = getl(b, T_ID1); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0 && h_cid <= 0) { + setl(b, T_RC, 6601); + Bchg(b, T_MSG, 0, "기준(영업일/기간/마감ID) 중 하나는 필수", 0L); FAIL(b); + } + + /* --- 섹션2·섹션4: 기간 마감이력 종합 집계 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_n1, :h_n2, :h_n3 FROM cl_close_log + WHERE (:h_period = '' OR period_key LIKE :h_period || '%') + AND status <> 'X'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6625); FAIL(b); } + metric = h_n1; o_count = h_n2; o_amount = h_n3; + if (h_n1 == 0) { o_rc = 1; strcpy(o_msg, "대상 마감이력 없음"); } + else if (h_n3 == 0) { o_rc = 2; strcpy(o_msg, "집계금액 0"); } + else { o_rc = 0; strcpy(o_msg, "마감이력 집계 완료"); } + + /* --- 섹션3b: 규모 부가판정 --- */ + if (metric < 0) metric = 0; + setl(b, T_ARG5, metric); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_STATUS 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1500000000L) { + z_sev = 3; + strcpy(z_msg, "광역 마감규모"); + } else if (h_zamt >= 150000000L) { + z_sev = 2; + strcpy(z_msg, "표준 마감규모"); + } else { + z_sev = 1; + strcpy(z_msg, "경량 마감규모"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_STATUS 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_STATUS 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_STATUS 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_STATUS 마감 상태 조회(dbio getter). cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_STATUS 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_STATUS 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_STCHGAUDIT.pgc b/app/src/cl/svc/CL_STCHGAUDIT.pgc index 4ba9440..370fa23 100644 --- a/app/src/cl/svc/CL_STCHGAUDIT.pgc +++ b/app/src/cl/svc/CL_STCHGAUDIT.pgc @@ -9,28 +9,142 @@ void CL_STCHGAUDIT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long metric = 0; int i = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; - long h_o, h_c, h_a, h_l, h_x, h_r; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_n1, h_n2, h_n3, h_cid; + char h_key[32], h_status[4], h_out[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_STCHGAUDIT"); + userlog("CL_STCHGAUDIT 진입: 마감처리 시작 (마감 상태변경 감사: 특정 기간(period_key) 마감이력의 상태 분포를 상태별로 집계해)", svc); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT - count(*) FILTER (WHERE status = 'O'), - count(*) FILTER (WHERE status = 'C'), - count(*) FILTER (WHERE status = 'A'), - count(*) FILTER (WHERE status = 'L'), - count(*) FILTER (WHERE status = 'X'), - count(*) FILTER (WHERE status = 'R') - INTO :h_o, :h_c, :h_a, :h_l, :h_x, :h_r - FROM cl_close_log WHERE period_key = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_ARG1, h_o); - setl(b, T_ARG2, h_c); - setl(b, T_ARG3, h_a); - setl(b, T_ARG4, h_l); - setl(b, T_ARG5, h_x); - setl(b, T_ARG6, h_r); - setl(b, T_COUNT, h_o + h_c + h_a + h_l + h_x + h_r); + gets_(b, T_STR1, h_period, sizeof(h_period)); + gets_(b, T_KEY1, h_key, sizeof(h_key)); + h_cid = getl(b, T_ID1); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0 && h_cid <= 0) { + setl(b, T_RC, 6601); + Bchg(b, T_MSG, 0, "기준(영업일/기간/마감ID) 중 하나는 필수", 0L); FAIL(b); + } + + /* --- 섹션2·섹션4: 마감 이력 감사 집계 --- */ + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN status='X' THEN 1 ELSE 0 END),0), + coalesce(sum(CASE WHEN status='L' THEN 1 ELSE 0 END),0) + INTO :h_n1, :h_n2, :h_n3 FROM cl_close_log + WHERE (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6622); FAIL(b); } + metric = h_n1; o_count = h_n1; + setl(b, T_ARG1, h_n2); + setl(b, T_ARG2, h_n3); + if (h_n1 == 0) { o_rc = 1; strcpy(o_msg, "감사 대상 없음"); } + else if (h_n2 > 0) { o_rc = 2; strcpy(o_msg, "취소 이력 포함"); } + else { o_rc = 0; strcpy(o_msg, "감사 정상"); } + + /* --- 섹션3b: 규모 부가판정 --- */ + if (metric < 0) metric = 0; + setl(b, T_ARG5, metric); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_STCHGAUDIT 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 2000000000L) { + z_sev = 3; + strcpy(z_msg, "초과대형 마감구간"); + } else if (h_zamt >= 200000000L) { + z_sev = 2; + strcpy(z_msg, "중대형 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "일반 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_STCHGAUDIT 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_STCHGAUDIT 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_STCHGAUDIT 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_STCHGAUDIT 마감 상태변경 감사: 특정 기간(period_key) 마감이력의 상태 분포를 상태별로 집계해 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_STCHGAUDIT 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_STCHGAUDIT 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_STCROSS.pgc b/app/src/cl/svc/CL_STCROSS.pgc index 5ea20e0..b96dc00 100644 --- a/app/src/cl/svc/CL_STCROSS.pgc +++ b/app/src/cl/svc/CL_STCROSS.pgc @@ -9,28 +9,159 @@ void CL_STCROSS(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long unclosed; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long d1 = 0, d2 = 0, worst = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; - long h_a, h_m, h_s, h_c, h_t; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_hdr, h_real, h_snap; + char h_date[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT - count(*) FILTER (WHERE status = 'A'), - count(*) FILTER (WHERE status = 'M'), - count(*) FILTER (WHERE status = 'S'), - count(*) FILTER (WHERE status = 'C'), - count(*) - INTO :h_a, :h_m, :h_s, :h_c, :h_t - FROM purchase WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - unclosed = h_a + h_m; - setl(b, T_ARG1, h_a); - setl(b, T_ARG2, h_m); - setl(b, T_ARG3, h_s); - setl(b, T_ARG4, h_c); - setl(b, T_COUNT, h_t); - setl(b, T_RC, (unclosed == 0) ? 0 : 1); + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_STCROSS"); + userlog("CL_STCROSS 진입: 마감처리 시작 (상태분포 크로스검증)", svc); + + h_cid = getl(b, T_ID1); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0) { + setl(b, T_RC, 5501); + Bchg(b, T_MSG, 0, "마감ID(T_ID1) 필수", 0L); FAIL(b); + } + + /* --- 섹션2: 마감이력 헤더 조회부 --- */ + EXEC SQL SELECT txn_count, to_char(biz_date,'YYYY-MM-DD') + INTO :h_hdr, :h_date FROM cl_close_log + WHERE close_id = :h_cid AND status <> 'X'; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5502); + Bchg(b, T_MSG, 0, "유효 마감이력 없음(취소/미존재)", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5503); FAIL(b); } + + /* --- 섹션4: 실매입/스냅샷 집계부 --- */ + EXEC SQL SELECT count(*) INTO :h_real + FROM purchase WHERE biz_date = :h_date; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5504); FAIL(b); } + EXEC SQL SELECT coalesce(sum(txn_count),0) INTO :h_snap + FROM cl_snapshot WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5505); FAIL(b); } + + /* --- 섹션3: 3원 대사 분기부 --- */ + d1 = h_hdr - h_real; if (d1 < 0) d1 = -d1; + d2 = (h_snap == 0) ? 0 : (h_hdr - h_snap); if (d2 < 0) d2 = -d2; + worst = (d1 > d2) ? d1 : d2; + if (worst == 0) { + o_rc = 0; strcpy(o_msg, "헤더=매입=스냅샷 3원 일치"); + } else if (d1 != 0 && d2 == 0) { + o_rc = 1; strcpy(o_msg, "헤더-매입 불일치(스냅샷은 일치)"); + } else if (d1 == 0 && d2 != 0) { + o_rc = 2; strcpy(o_msg, "헤더-스냅샷 불일치(매입은 일치)"); + } else { + o_rc = 3; strcpy(o_msg, "3원 전부 불일치 - 재마감 검토"); + } + + /* --- 섹션6: 편차 세팅 --- */ + setl(b, T_AMT1, h_hdr); + setl(b, T_AMT2, h_real); + setl(b, T_AMT3, h_snap); + setl(b, T_DELTA, worst); + o_count = (worst == 0) ? 0 : 1; o_amount = worst; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_STCROSS 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 2000000000L) { + z_sev = 3; + strcpy(z_msg, "초과대형 마감구간"); + } else if (h_zamt >= 200000000L) { + z_sev = 2; + strcpy(z_msg, "중대형 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "일반 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_STCROSS 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_STCROSS 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_STCROSS 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_STCROSS 상태분포크로스 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_STCROSS 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_STCROSS 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_SUMMARY.pgc b/app/src/cl/svc/CL_SUMMARY.pgc index 74bdcf4..881a312 100644 --- a/app/src/cl/svc/CL_SUMMARY.pgc +++ b/app/src/cl/svc/CL_SUMMARY.pgc @@ -9,17 +9,147 @@ void CL_SUMMARY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid, h_gross, h_fee, h_net; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_cnt, h_g, h_f, h_nt; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_SUMMARY"); + userlog("CL_SUMMARY 진입: 마감처리 시작 (요약)", svc); + h_cid = getl(b, T_ID1); - EXEC SQL SELECT coalesce(sum(gross_amount),0), coalesce(sum(fee_amount),0), - coalesce(sum(net_amount),0) - INTO :h_gross, :h_fee, :h_net FROM cl_snapshot WHERE close_id = :h_cid; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_GROSS, h_gross); - setl(b, T_FEE, h_fee); - setl(b, T_NET, h_net); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0) { + setl(b, T_RC, 6301); + Bchg(b, T_MSG, 0, "대상 마감ID 필수", 0L); FAIL(b); + } + + /* --- 섹션2: 마감 존재 확인부 --- */ + EXEC SQL SELECT count(*) INTO :h_cnt FROM cl_close_log + WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6302); FAIL(b); } + if (h_cnt == 0) { + setl(b, T_RC, 6303); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); + } + + /* --- 섹션4: 스냅샷 gross/fee/net 합계부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(gross_amount),0), + coalesce(sum(fee_amount),0), coalesce(sum(net_amount),0) + INTO :h_cnt, :h_g, :h_f, :h_nt + FROM cl_snapshot WHERE close_id = :h_cid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6304); FAIL(b); } + + /* --- 섹션3: 요약 규모 분기부 --- */ + if (h_cnt == 0) { + o_rc = 1; strcpy(o_msg, "스냅샷 없음"); + } else if (h_g == h_f + h_nt) { + o_rc = 0; strcpy(o_msg, "gross=fee+net 정합"); + } else { + o_rc = 2; strcpy(o_msg, "gross<>fee+net 불일치"); + } + setl(b, T_GROSS, h_g); + setl(b, T_FEE, h_f); + setl(b, T_NET, h_nt); + o_count = h_cnt; o_amount = h_g; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_SUMMARY 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1000000000L) { + z_sev = 3; + strcpy(z_msg, "대규모 마감기간"); + } else if (h_zamt >= 100000000L) { + z_sev = 2; + strcpy(z_msg, "중규모 마감기간"); + } else { + z_sev = 1; + strcpy(z_msg, "소규모 마감기간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_SUMMARY 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_SUMMARY 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_SUMMARY 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_SUMMARY 요약 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_SUMMARY 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_SUMMARY 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_TENDAY.pgc b/app/src/cl/svc/CL_TENDAY.pgc index 58cc335..dcdb436 100644 --- a/app/src/cl/svc/CL_TENDAY.pgc +++ b/app/src/cl/svc/CL_TENDAY.pgc @@ -8,33 +8,205 @@ void CL_TENDAY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long cid, dec, lo, hi; - char period[24], memo[32]; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long cid = 0; + long fee_calc = 0; + int grade = 0; + char memo[128]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16], h_ym[16]; - long h_day, h_lo, h_hi, h_cnt, h_amt; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cnt, h_amt, h_fee, h_net; + long h_prev, h_bps; + char h_key[32]; + char h_status[4]; + char h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_TENDAY"); + userlog("CL_TENDAY 진입: 마감처리 시작 (순(10일) 마감)", svc); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT extract(day from :h_bizdate::date)::int, - to_char(:h_bizdate::date, 'YYYY-MM') INTO :h_day, :h_ym; - if (sqlca.sqlcode < 0) FAIL(b); - dec = (h_day <= 10) ? 1 : (h_day <= 20 ? 2 : 3); - lo = (dec - 1) * 10 + 1; - hi = (dec == 3) ? 31 : dec * 10; - h_lo = lo; h_hi = hi; - EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_amt - FROM purchase WHERE to_char(biz_date,'YYYY-MM') = :h_ym - AND extract(day from biz_date) BETWEEN :h_lo AND :h_hi; - if (sqlca.sqlcode < 0) FAIL(b); - snprintf(period, sizeof(period), "%s-T%ld", h_ym, dec); - snprintf(memo, sizeof(memo), "순마감:%ld순", dec); + gets_(b, T_STR1, h_period, sizeof(h_period)); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + strcpy(h_key, ""); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0) { + setl(b, T_RC, 5001); + Bchg(b, T_MSG, 0, "영업일/기간 미입력 - 순(10일) 마감 불가", 0L); + FAIL(b); + } + if (strlen(h_bizdate) > 0 && strlen(h_bizdate) < 8) { + setl(b, T_RC, 5002); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + FAIL(b); + } + + /* --- 섹션2: 정책/기존마감 상태 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_prev FROM cl_close_log + WHERE close_type = 'TENDAY' AND period_key = :h_period AND status <> 'X'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5010); + Bchg(b, T_MSG, 0, "기존 마감 조회 실패", 0L); FAIL(b); + } + if (h_prev > 0) { + userlog("CL_TENDAY %s 이미 마감 %ld건 존재 - 재집계 진행", h_period, h_prev); + } + EXEC SQL SELECT count(*) INTO :h_bps FROM cl_period_lock + WHERE period_key = :h_period AND locked = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5011); FAIL(b); } + if (h_bps > 0) { + setl(b, T_RC, 5012); + Bchg(b, T_MSG, 0, "기간이 잠금상태 - 마감 불가", 0L); FAIL(b); + } + + /* --- 섹션4: 본처리부(집계) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) + INTO :h_cnt, :h_amt, :h_fee, :h_net + FROM purchase WHERE to_char(biz_date,'YYYY-MM') = :h_period; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5020); + Bchg(b, T_MSG, 0, "매입 집계 실패", 0L); FAIL(b); + } + + /* --- 섹션3: 업무규칙 분기부(규모/등급) --- */ + if (h_cnt == 0) { + grade = 0; + strcpy(memo, "TENDAY:무거래"); + } else if (h_amt >= 1000000000) { + grade = 3; + strcpy(memo, "TENDAY:대규모"); + } else if (h_amt >= 100000000) { + grade = 2; + strcpy(memo, "TENDAY:중규모"); + } else { + grade = 1; + strcpy(memo, "TENDAY:소규모"); + } + fee_calc = acq_fee(h_amt, 25); + if (h_fee == 0 && h_amt > 0) h_fee = fee_calc; + + /* --- 섹션4b: 마감이력 생성 --- */ cid = cldb_next_close_id(); - if (cid < 0) FAIL(b); - if (cldb_insert_close(cid, "TENDAY", period, h_bizdate, "C", h_cnt, h_amt, memo) < 0) FAIL(b); + if (cid < 0) { setl(b, T_RC, 5030); FAIL(b); } + if (cldb_insert_close(cid, "TENDAY", h_period, + (h_bizdate[0] ? h_bizdate : h_period), + "C", h_cnt, h_amt, memo) < 0) { + setl(b, T_RC, 5031); + Bchg(b, T_MSG, 0, "마감이력 생성 실패", 0L); FAIL(b); + } + + /* --- 섹션6: 집계 확인/요약 재조회부 --- */ + h_kcid = cid; + EXEC SQL SELECT txn_count, total_amount INTO :h_prev, :h_amt + FROM cl_close_log WHERE close_id = :h_kcid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5040); FAIL(b); } + o_count = h_cnt; o_amount = h_amt; + + /* --- 섹션8prep: 상태/결과 세팅 --- */ setl(b, T_ID1, cid); - setl(b, T_ARG1, dec); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_amt); + setl(b, T_FEE, h_fee); + setl(b, T_NET, h_net); + Bchg(b, T_STATUS, 0, "C", 0L); + snprintf(o_msg, sizeof(o_msg), "순(10일) 마감완료 등급%d cid=%ld", grade, cid); + o_rc = 0; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_TENDAY 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 2000000000L) { + z_sev = 3; + strcpy(z_msg, "초과대형 마감구간"); + } else if (h_zamt >= 200000000L) { + z_sev = 2; + strcpy(z_msg, "중대형 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "일반 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_TENDAY 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_TENDAY 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_TENDAY 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_TENDAY 순(10일)마감 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_TENDAY 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_TENDAY 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_TENTCLOSE.pgc b/app/src/cl/svc/CL_TENTCLOSE.pgc index 606653c..fe6b8d6 100644 --- a/app/src/cl/svc/CL_TENTCLOSE.pgc +++ b/app/src/cl/svc/CL_TENTCLOSE.pgc @@ -8,23 +8,205 @@ void CL_TENTCLOSE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long cid; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long cid = 0; + long fee_calc = 0; + int grade = 0; + char memo[128]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; - long h_cnt, h_amt; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cnt, h_amt, h_fee, h_net; + long h_prev, h_bps; + char h_key[32]; + char h_status[4]; + char h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_TENTCLOSE"); + userlog("CL_TENTCLOSE 진입: 마감처리 시작 (가마감(잠정) 마감)", svc); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_amt + gets_(b, T_STR1, h_period, sizeof(h_period)); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + strcpy(h_key, ""); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0) { + setl(b, T_RC, 5001); + Bchg(b, T_MSG, 0, "영업일/기간 미입력 - 가마감(잠정) 마감 불가", 0L); + FAIL(b); + } + if (strlen(h_bizdate) > 0 && strlen(h_bizdate) < 8) { + setl(b, T_RC, 5002); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + FAIL(b); + } + + /* --- 섹션2: 정책/기존마감 상태 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_prev FROM cl_close_log + WHERE close_type = 'TENT' AND period_key = :h_period AND status <> 'X'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5010); + Bchg(b, T_MSG, 0, "기존 마감 조회 실패", 0L); FAIL(b); + } + if (h_prev > 0) { + userlog("CL_TENTCLOSE %s 이미 마감 %ld건 존재 - 재집계 진행", h_period, h_prev); + } + EXEC SQL SELECT count(*) INTO :h_bps FROM cl_period_lock + WHERE period_key = :h_period AND locked = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5011); FAIL(b); } + if (h_bps > 0) { + setl(b, T_RC, 5012); + Bchg(b, T_MSG, 0, "기간이 잠금상태 - 마감 불가", 0L); FAIL(b); + } + + /* --- 섹션4: 본처리부(집계) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) + INTO :h_cnt, :h_amt, :h_fee, :h_net FROM purchase WHERE biz_date = :h_bizdate AND status IN ('A','M'); - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5020); + Bchg(b, T_MSG, 0, "매입 집계 실패", 0L); FAIL(b); + } + + /* --- 섹션3: 업무규칙 분기부(규모/등급) --- */ + if (h_cnt == 0) { + grade = 0; + strcpy(memo, "TENT:무거래"); + } else if (h_amt >= 1000000000) { + grade = 3; + strcpy(memo, "TENT:대규모"); + } else if (h_amt >= 100000000) { + grade = 2; + strcpy(memo, "TENT:중규모"); + } else { + grade = 1; + strcpy(memo, "TENT:소규모"); + } + fee_calc = acq_fee(h_amt, 25); + if (h_fee == 0 && h_amt > 0) h_fee = fee_calc; + + /* --- 섹션4b: 마감이력 생성 --- */ cid = cldb_next_close_id(); - if (cid < 0) FAIL(b); - if (cldb_insert_close(cid, "TENT", h_bizdate, h_bizdate, "O", h_cnt, h_amt, "가마감") < 0) FAIL(b); + if (cid < 0) { setl(b, T_RC, 5030); FAIL(b); } + if (cldb_insert_close(cid, "TENT", h_period, + (h_bizdate[0] ? h_bizdate : h_period), + "C", h_cnt, h_amt, memo) < 0) { + setl(b, T_RC, 5031); + Bchg(b, T_MSG, 0, "마감이력 생성 실패", 0L); FAIL(b); + } + + /* --- 섹션6: 집계 확인/요약 재조회부 --- */ + h_kcid = cid; + EXEC SQL SELECT txn_count, total_amount INTO :h_prev, :h_amt + FROM cl_close_log WHERE close_id = :h_kcid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5040); FAIL(b); } + o_count = h_cnt; o_amount = h_amt; + + /* --- 섹션8prep: 상태/결과 세팅 --- */ setl(b, T_ID1, cid); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_amt); - Bchg(b, T_STATUS, 0, "O", 0L); - userlog("CL_TENTCLOSE cid=%ld cnt=%ld 가마감(잠정)", cid, h_cnt); + setl(b, T_FEE, h_fee); + setl(b, T_NET, h_net); + Bchg(b, T_STATUS, 0, "C", 0L); + snprintf(o_msg, sizeof(o_msg), "가마감(잠정) 마감완료 등급%d cid=%ld", grade, cid); + o_rc = 0; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_TENTCLOSE 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1500000000L) { + z_sev = 3; + strcpy(z_msg, "광역 마감규모"); + } else if (h_zamt >= 150000000L) { + z_sev = 2; + strcpy(z_msg, "표준 마감규모"); + } else { + z_sev = 1; + strcpy(z_msg, "경량 마감규모"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_TENTCLOSE 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_TENTCLOSE 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_TENTCLOSE 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_TENTCLOSE 가마감(잠정)마감 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_TENTCLOSE 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_TENTCLOSE 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_TREND.pgc b/app/src/cl/svc/CL_TREND.pgc index 7410ea6..15b38b5 100644 --- a/app/src/cl/svc/CL_TREND.pgc +++ b/app/src/cl/svc/CL_TREND.pgc @@ -9,24 +9,153 @@ void CL_TREND(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long recs = 0; long avg = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_n, h_cnt, h_avg, h_min, h_max; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_n, h_amt, h_min, h_max, h_sum; + char h_pk[24]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; - h_n = getl(b, T_ARG1); - if (h_n <= 0 || h_n > 90) h_n = 7; - EXEC SQL SELECT count(*), coalesce(cast(avg(t.total_amount) as bigint),0), - coalesce(min(t.total_amount),0), coalesce(max(t.total_amount),0) - INTO :h_cnt, :h_avg, :h_min, :h_max - FROM (SELECT total_amount FROM cl_close_log - WHERE close_type = 'DAILY' AND status IN ('C','A','L') - ORDER BY biz_date DESC LIMIT :h_n) t; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_cnt == 0) { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "추이 표본 없음", 0L); OK(b); } - setl(b, T_COUNT, h_cnt); - setl(b, T_AMT1, h_avg); - setl(b, T_AMT2, h_min); + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_TREND"); + userlog("CL_TREND 진입: 마감처리 시작 (금액추이)", svc); + + h_n = getl(b, T_ARG1); if (h_n <= 0) h_n = 7; + + /* --- 섹션1: 입력 검증부 --- */ + if (h_n > 366) { + setl(b, T_RC, 6401); + Bchg(b, T_MSG, 0, "조회 회차(T_ARG1)가 과다", 0L); FAIL(b); + } + h_min = -1; h_max = 0; h_sum = 0; + + /* --- 섹션2·섹션4: 최근 N회 일마감 총액 커서 --- */ + EXEC SQL DECLARE ctrend CURSOR FOR + SELECT period_key, total_amount FROM cl_close_log + WHERE close_type = 'DAILY' AND status <> 'X' + ORDER BY period_key DESC; + EXEC SQL OPEN ctrend; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6402); FAIL(b); } + for (;;) { + if (recs >= h_n) break; + EXEC SQL FETCH ctrend INTO :h_pk, :h_amt; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE ctrend; setl(b, T_RC, 6403); FAIL(b); } + h_sum += h_amt; + if (h_min < 0 || h_amt < h_min) h_min = h_amt; + if (h_amt > h_max) h_max = h_amt; + recs++; + } + EXEC SQL CLOSE ctrend; + + /* --- 섹션3: 추이 판정 분기부 --- */ + if (recs == 0) { + o_rc = 1; strcpy(o_msg, "일마감 이력 없음"); + } else { + avg = h_sum / recs; + if (h_max - h_min > avg) { o_rc = 2; strcpy(o_msg, "변동성 높음"); } + else { o_rc = 0; strcpy(o_msg, "추이 안정"); } + } + setl(b, T_AMT1, avg); + setl(b, T_AMT2, (h_min < 0) ? 0 : h_min); setl(b, T_AMT3, h_max); - setl(b, T_RC, 0); + o_count = recs; o_amount = avg; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_TREND 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1000000000L) { + z_sev = 3; + strcpy(z_msg, "대규모 마감기간"); + } else if (h_zamt >= 100000000L) { + z_sev = 2; + strcpy(z_msg, "중규모 마감기간"); + } else { + z_sev = 1; + strcpy(z_msg, "소규모 마감기간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_TREND 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_TREND 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_TREND 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_TREND 금액추이 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_TREND 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_TREND 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_TRIGRPT.pgc b/app/src/cl/svc/CL_TRIGRPT.pgc index 136b3e2..9a3f434 100644 --- a/app/src/cl/svc/CL_TRIGRPT.pgc +++ b/app/src/cl/svc/CL_TRIGRPT.pgc @@ -9,18 +9,145 @@ void CL_TRIGRPT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long metric = 0; int i = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; - long h_n; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_n1, h_n2, h_n3, h_cid; + char h_key[32], h_status[4], h_out[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_TRIGRPT"); + userlog("CL_TRIGRPT 진입: 마감처리 시작 (마감 후 리포트 트리거: 마감완료(C)이나 리포트 미생성(memo!=RPT:*)인 마감을 대상으로)", svc); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT count(*) INTO :h_n FROM cl_close_log - WHERE period_key = :h_bizdate AND status = 'C' - AND memo NOT LIKE 'RPT:%'; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_n); - setl(b, T_RC, (h_n > 0) ? 0 : 1); - Bchg(b, T_SVCNAME, 0, "CL_REPORT", 0L); + gets_(b, T_STR1, h_period, sizeof(h_period)); + gets_(b, T_KEY1, h_key, sizeof(h_key)); + h_cid = getl(b, T_ID1); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0 && h_cid <= 0) { + setl(b, T_RC, 6601); + Bchg(b, T_MSG, 0, "기준(영업일/기간/마감ID) 중 하나는 필수", 0L); FAIL(b); + } + + /* --- 섹션2·섹션4: 마감 후속 트리거 대상 산출 --- */ + if (h_bizdate[0] == 0) { + setl(b, T_RC, 6620); + Bchg(b, T_MSG, 0, "기준 영업일 필수", 0L); FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_n1, :h_n2 + FROM purchase WHERE biz_date = :h_bizdate AND status = 'S'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6621); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_n3 FROM ledger + WHERE biz_date = :h_bizdate AND entry_type = 'SETTLE'; + if (sqlca.sqlcode < 0) h_n3 = 0; + metric = h_n1; o_count = h_n1; o_amount = h_n2; + if (h_n1 == 0) { o_rc = 1; strcpy(o_msg, "후속 대상 없음"); } + else if (h_n3 >= h_n1) { o_rc = 0; strcpy(o_msg, "후속 전기 완료상태"); } + else { o_rc = 2; strcpy(o_msg, "후속 전기 대기 존재"); } + setl(b, T_ARG1, h_n3); + + /* --- 섹션3b: 규모 부가판정 --- */ + if (metric < 0) metric = 0; + setl(b, T_ARG5, metric); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_TRIGRPT 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 500000000L) { + z_sev = 3; + strcpy(z_msg, "고액 마감구간"); + } else if (h_zamt >= 50000000L) { + z_sev = 2; + strcpy(z_msg, "중액 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "소액 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_TRIGRPT 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_TRIGRPT 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_TRIGRPT 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_TRIGRPT 마감 후 리포트 트리거: 마감완료(C)이나 리포트 미생성(memo!=RPT:*)인 마감을 대상으로 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_TRIGRPT 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_TRIGRPT 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_TRIGSETTLE.pgc b/app/src/cl/svc/CL_TRIGSETTLE.pgc index 26297a0..211349b 100644 --- a/app/src/cl/svc/CL_TRIGSETTLE.pgc +++ b/app/src/cl/svc/CL_TRIGSETTLE.pgc @@ -9,18 +9,145 @@ void CL_TRIGSETTLE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long metric = 0; int i = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; - long h_n, h_net; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_n1, h_n2, h_n3, h_cid; + char h_key[32], h_status[4], h_out[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_TRIGSETTLE"); + userlog("CL_TRIGSETTLE 진입: 마감처리 시작 (마감 후 정산 트리거: 마감 완료 당일 정산완료(S) 대상 건수/정산액을 산출하고)", svc); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_n, :h_net + gets_(b, T_STR1, h_period, sizeof(h_period)); + gets_(b, T_KEY1, h_key, sizeof(h_key)); + h_cid = getl(b, T_ID1); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0 && h_cid <= 0) { + setl(b, T_RC, 6601); + Bchg(b, T_MSG, 0, "기준(영업일/기간/마감ID) 중 하나는 필수", 0L); FAIL(b); + } + + /* --- 섹션2·섹션4: 마감 후속 트리거 대상 산출 --- */ + if (h_bizdate[0] == 0) { + setl(b, T_RC, 6620); + Bchg(b, T_MSG, 0, "기준 영업일 필수", 0L); FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_n1, :h_n2 FROM purchase WHERE biz_date = :h_bizdate AND status = 'S'; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_n); - setl(b, T_NET, h_net); - setl(b, T_RC, (h_n > 0) ? 0 : 1); - Bchg(b, T_SVCNAME, 0, "AC_SETTLE", 0L); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6621); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_n3 FROM ledger + WHERE biz_date = :h_bizdate AND entry_type = 'SETTLE'; + if (sqlca.sqlcode < 0) h_n3 = 0; + metric = h_n1; o_count = h_n1; o_amount = h_n2; + if (h_n1 == 0) { o_rc = 1; strcpy(o_msg, "후속 대상 없음"); } + else if (h_n3 >= h_n1) { o_rc = 0; strcpy(o_msg, "후속 전기 완료상태"); } + else { o_rc = 2; strcpy(o_msg, "후속 전기 대기 존재"); } + setl(b, T_ARG1, h_n3); + + /* --- 섹션3b: 규모 부가판정 --- */ + if (metric < 0) metric = 0; + setl(b, T_ARG5, metric); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_TRIGSETTLE 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1500000000L) { + z_sev = 3; + strcpy(z_msg, "광역 마감규모"); + } else if (h_zamt >= 150000000L) { + z_sev = 2; + strcpy(z_msg, "표준 마감규모"); + } else { + z_sev = 1; + strcpy(z_msg, "경량 마감규모"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_TRIGSETTLE 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_TRIGSETTLE 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_TRIGSETTLE 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_TRIGSETTLE 마감 후 정산 트리거: 마감 완료 당일 정산완료(S) 대상 건수/정산액을 산출하고 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_TRIGSETTLE 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_TRIGSETTLE 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_TRIGSTAT.pgc b/app/src/cl/svc/CL_TRIGSTAT.pgc index 8f4cc96..4789498 100644 --- a/app/src/cl/svc/CL_TRIGSTAT.pgc +++ b/app/src/cl/svc/CL_TRIGSTAT.pgc @@ -9,26 +9,145 @@ void CL_TRIGSTAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long pct = 0; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long metric = 0; int i = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_settle, h_xfer; char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_n1, h_n2, h_n3, h_cid; + char h_key[32], h_status[4], h_out[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_TRIGSTAT"); + userlog("CL_TRIGSTAT 진입: 마감처리 시작 (마감 처리)", svc); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT count(*) INTO :h_settle FROM settlement WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_xfer FROM ledger - WHERE biz_date = :h_bizdate AND entry_type = 'XFER'; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_settle > 0) { - pct = (h_xfer * 10000) / h_settle; - if (pct > 10000) pct = 10000; + gets_(b, T_STR1, h_period, sizeof(h_period)); + gets_(b, T_KEY1, h_key, sizeof(h_key)); + h_cid = getl(b, T_ID1); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0 && h_cid <= 0) { + setl(b, T_RC, 6601); + Bchg(b, T_MSG, 0, "기준(영업일/기간/마감ID) 중 하나는 필수", 0L); FAIL(b); } - setl(b, T_ARG1, h_settle); - setl(b, T_ARG2, h_xfer); - setl(b, T_ARG3, pct); - setl(b, T_DELTA, h_settle - h_xfer); - setl(b, T_RC, (h_settle <= h_xfer) ? 0 : 1); + + /* --- 섹션2·섹션4: 마감 후속 트리거 대상 산출 --- */ + if (h_bizdate[0] == 0) { + setl(b, T_RC, 6620); + Bchg(b, T_MSG, 0, "기준 영업일 필수", 0L); FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_n1, :h_n2 + FROM purchase WHERE biz_date = :h_bizdate AND status = 'S'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6621); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_n3 FROM ledger + WHERE biz_date = :h_bizdate AND entry_type = 'SETTLE'; + if (sqlca.sqlcode < 0) h_n3 = 0; + metric = h_n1; o_count = h_n1; o_amount = h_n2; + if (h_n1 == 0) { o_rc = 1; strcpy(o_msg, "후속 대상 없음"); } + else if (h_n3 >= h_n1) { o_rc = 0; strcpy(o_msg, "후속 전기 완료상태"); } + else { o_rc = 2; strcpy(o_msg, "후속 전기 대기 존재"); } + setl(b, T_ARG1, h_n3); + + /* --- 섹션3b: 규모 부가판정 --- */ + if (metric < 0) metric = 0; + setl(b, T_ARG5, metric); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_TRIGSTAT 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1000000000L) { + z_sev = 3; + strcpy(z_msg, "대규모 마감기간"); + } else if (h_zamt >= 100000000L) { + z_sev = 2; + strcpy(z_msg, "중규모 마감기간"); + } else { + z_sev = 1; + strcpy(z_msg, "소규모 마감기간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_TRIGSTAT 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_TRIGSTAT 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_TRIGSTAT 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_TRIGSTAT 마감 처리 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_TRIGSTAT 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_TRIGSTAT 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_UNCLOSED.pgc b/app/src/cl/svc/CL_UNCLOSED.pgc index 5819025..01cff9e 100644 --- a/app/src/cl/svc/CL_UNCLOSED.pgc +++ b/app/src/cl/svc/CL_UNCLOSED.pgc @@ -9,16 +9,205 @@ void CL_UNCLOSED(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long cid = 0; + long fee_calc = 0; + int grade = 0; + char memo[128]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cnt, h_amt, h_fee, h_net; + long h_prev, h_bps; + char h_key[32]; + char h_status[4]; + char h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; - EXEC SQL SELECT count(DISTINCT p.biz_date) INTO :h_cnt - FROM purchase p - WHERE NOT EXISTS (SELECT 1 FROM cl_close_log c - WHERE c.close_type = 'DAILY' AND c.biz_date = p.biz_date - AND c.status <> 'X'); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_UNCLOSED"); + userlog("CL_UNCLOSED 진입: 마감처리 시작 (일 마감)", svc); + + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + gets_(b, T_STR1, h_period, sizeof(h_period)); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + strcpy(h_key, ""); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0) { + setl(b, T_RC, 5001); + Bchg(b, T_MSG, 0, "영업일/기간 미입력 - 일 마감 불가", 0L); + FAIL(b); + } + if (strlen(h_bizdate) > 0 && strlen(h_bizdate) < 8) { + setl(b, T_RC, 5002); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + FAIL(b); + } + + /* --- 섹션2: 정책/기존마감 상태 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_prev FROM cl_close_log + WHERE close_type = 'DAILY' AND period_key = :h_period AND status <> 'X'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5010); + Bchg(b, T_MSG, 0, "기존 마감 조회 실패", 0L); FAIL(b); + } + if (h_prev > 0) { + userlog("CL_UNCLOSED %s 이미 마감 %ld건 존재 - 재집계 진행", h_period, h_prev); + } + EXEC SQL SELECT count(*) INTO :h_bps FROM cl_period_lock + WHERE period_key = :h_period AND locked = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5011); FAIL(b); } + if (h_bps > 0) { + setl(b, T_RC, 5012); + Bchg(b, T_MSG, 0, "기간이 잠금상태 - 마감 불가", 0L); FAIL(b); + } + + /* --- 섹션4: 본처리부(집계) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) + INTO :h_cnt, :h_amt, :h_fee, :h_net + FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5020); + Bchg(b, T_MSG, 0, "매입 집계 실패", 0L); FAIL(b); + } + + /* --- 섹션3: 업무규칙 분기부(규모/등급) --- */ + if (h_cnt == 0) { + grade = 0; + strcpy(memo, "DAILY:무거래"); + } else if (h_amt >= 1000000000) { + grade = 3; + strcpy(memo, "DAILY:대규모"); + } else if (h_amt >= 100000000) { + grade = 2; + strcpy(memo, "DAILY:중규모"); + } else { + grade = 1; + strcpy(memo, "DAILY:소규모"); + } + fee_calc = acq_fee(h_amt, 25); + if (h_fee == 0 && h_amt > 0) h_fee = fee_calc; + + /* --- 섹션4b: 마감이력 생성 --- */ + cid = cldb_next_close_id(); + if (cid < 0) { setl(b, T_RC, 5030); FAIL(b); } + if (cldb_insert_close(cid, "DAILY", h_period, + (h_bizdate[0] ? h_bizdate : h_period), + "C", h_cnt, h_amt, memo) < 0) { + setl(b, T_RC, 5031); + Bchg(b, T_MSG, 0, "마감이력 생성 실패", 0L); FAIL(b); + } + + /* --- 섹션6: 집계 확인/요약 재조회부 --- */ + h_kcid = cid; + EXEC SQL SELECT txn_count, total_amount INTO :h_prev, :h_amt + FROM cl_close_log WHERE close_id = :h_kcid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5040); FAIL(b); } + o_count = h_cnt; o_amount = h_amt; + + /* --- 섹션8prep: 상태/결과 세팅 --- */ + setl(b, T_ID1, cid); + setl(b, T_FEE, h_fee); + setl(b, T_NET, h_net); + Bchg(b, T_STATUS, 0, "C", 0L); + snprintf(o_msg, sizeof(o_msg), "일 마감완료 등급%d cid=%ld", grade, cid); + o_rc = 0; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_UNCLOSED 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1500000000L) { + z_sev = 3; + strcpy(z_msg, "광역 마감규모"); + } else if (h_zamt >= 150000000L) { + z_sev = 2; + strcpy(z_msg, "표준 마감규모"); + } else { + z_sev = 1; + strcpy(z_msg, "경량 마감규모"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_UNCLOSED 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_UNCLOSED 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_UNCLOSED 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_UNCLOSED 일마감 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_UNCLOSED 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_UNCLOSED 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_UNLOCK.pgc b/app/src/cl/svc/CL_UNLOCK.pgc index 9252c61..719dcf5 100644 --- a/app/src/cl/svc/CL_UNLOCK.pgc +++ b/app/src/cl/svc/CL_UNLOCK.pgc @@ -9,16 +9,142 @@ void CL_UNLOCK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char period[16]; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long affected = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - char h_period[16]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_n, h_cnt; + char h_status[4]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; - gets_(b, T_STR1, period, sizeof(period)); if (period[0] == 0) gets_(b, T_BIZDATE, period, sizeof(period)); - strncpy(h_period, period, sizeof(h_period)-1); h_period[sizeof(h_period)-1] = 0; - EXEC SQL UPDATE cl_period_lock SET locked = false, locked_at = now() - WHERE period_key = :h_period; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, sqlca.sqlerrd[2]); + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_UNLOCK"); + userlog("CL_UNLOCK 진입: 마감처리 시작 (기간 잠금 해제)", svc); + + h_cid = getl(b, T_ID1); + gets_(b, T_STR1, h_period, sizeof(h_period)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_period[0] == 0) { + setl(b, T_RC, 5901); + Bchg(b, T_MSG, 0, "기간키(period) 미입력", 0L); FAIL(b); + } + + /* --- 섹션2: 기존 잠금상태 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_n FROM cl_period_lock + WHERE period_key = :h_period AND locked = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5902); FAIL(b); } + + /* --- 섹션4: 잠금 upsert 본처리부 --- */ + EXEC SQL INSERT INTO cl_period_lock (period_key, locked, locked_at) + VALUES (:h_period, false, now()) + ON CONFLICT (period_key) DO UPDATE SET locked = false, locked_at = now(); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5907); + Bchg(b, T_MSG, 0, "기간 잠금 해제 실패", 0L); FAIL(b); + } + + /* --- 섹션4b: 잠금 해제 반영 --- */ + affected = sqlca.sqlerrd[2]; + o_count = affected; o_rc = 0; + strcpy(o_msg, "기간 잠금 해제 완료"); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_UNLOCK 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1000000000L) { + z_sev = 3; + strcpy(z_msg, "대규모 마감기간"); + } else if (h_zamt >= 100000000L) { + z_sev = 2; + strcpy(z_msg, "중규모 마감기간"); + } else { + z_sev = 1; + strcpy(z_msg, "소규모 마감기간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_UNLOCK 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_UNLOCK 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_UNLOCK 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_UNLOCK 기간 잠금 해제 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_UNLOCK 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_UNLOCK 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_VERIFY.pgc b/app/src/cl/svc/CL_VERIFY.pgc index e7d1762..a7b4520 100644 --- a/app/src/cl/svc/CL_VERIFY.pgc +++ b/app/src/cl/svc/CL_VERIFY.pgc @@ -9,20 +9,168 @@ void CL_VERIFY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long dcnt = 0, damt = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cid, h_stored, h_actual; char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_hdr_cnt, h_hdr_amt; + long h_real_cnt, h_real_amt; + char h_date[16], h_type[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_VERIFY"); + userlog("CL_VERIFY 진입: 마감처리 시작 (마감 정합 검증)", svc); + h_cid = getl(b, T_ID1); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT txn_count INTO :h_stored FROM cl_close_log WHERE close_id = :h_cid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_actual FROM purchase WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_actual); - setl(b, T_RC, (h_stored == h_actual) ? 0 : 1); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0 && h_bizdate[0] == 0) { + setl(b, T_RC, 5401); + Bchg(b, T_MSG, 0, "마감ID/영업일 중 하나는 필수", 0L); FAIL(b); + } + + /* --- 섹션2: 마감이력 헤더 조회부 --- */ + if (h_cid > 0) { + EXEC SQL SELECT close_type, txn_count, total_amount, + to_char(biz_date,'YYYY-MM-DD') + INTO :h_type, :h_hdr_cnt, :h_hdr_amt, :h_date + FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5402); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5403); FAIL(b); } + } else { + strcpy(h_date, h_bizdate); + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(total_amount),0) + INTO :h_hdr_cnt, :h_hdr_amt FROM cl_close_log + WHERE period_key = :h_bizdate AND status <> 'X'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5404); FAIL(b); } + } + + /* --- 섹션4: 실매입 재집계부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_real_cnt, :h_real_amt + FROM purchase WHERE biz_date = :h_date; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5405); FAIL(b); } + + /* --- 섹션3: 대사 분기부 --- */ + dcnt = h_hdr_cnt - h_real_cnt; if (dcnt < 0) dcnt = -dcnt; + damt = h_hdr_amt - h_real_amt; if (damt < 0) damt = -damt; + if (dcnt == 0 && damt == 0) { + o_rc = 0; strcpy(o_msg, "건수·금액 완전 일치"); + } else if (dcnt == 0 && damt != 0) { + o_rc = 1; strcpy(o_msg, "건수 일치·금액 불일치"); + } else if (dcnt != 0 && damt == 0) { + o_rc = 2; strcpy(o_msg, "건수 불일치·금액 일치"); + } else { + o_rc = 3; strcpy(o_msg, "건수·금액 모두 불일치"); + } + + /* --- 섹션6: 결과 세팅 --- */ + setl(b, T_ARG1, h_hdr_cnt); + setl(b, T_ARG2, h_real_cnt); + setl(b, T_AMT1, h_hdr_amt); + setl(b, T_AMT2, h_real_amt); + setl(b, T_DELTA, damt); + o_count = dcnt; o_amount = damt; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_VERIFY 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 500000000L) { + z_sev = 3; + strcpy(z_msg, "고액 마감구간"); + } else if (h_zamt >= 50000000L) { + z_sev = 2; + strcpy(z_msg, "중액 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "소액 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_VERIFY 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_VERIFY 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_VERIFY 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_VERIFY 정합검증 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_VERIFY 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_VERIFY 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_WEEKLY.pgc b/app/src/cl/svc/CL_WEEKLY.pgc index a5545e8..6e5a580 100644 --- a/app/src/cl/svc/CL_WEEKLY.pgc +++ b/app/src/cl/svc/CL_WEEKLY.pgc @@ -8,25 +8,205 @@ void CL_WEEKLY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long cid; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long cid = 0; + long fee_calc = 0; + int grade = 0; + char memo[128]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16], h_wk[16]; - long h_cnt, h_amt; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cnt, h_amt, h_fee, h_net; + long h_prev, h_bps; + char h_key[32]; + char h_status[4]; + char h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_WEEKLY"); + userlog("CL_WEEKLY 진입: 마감처리 시작 (주간 마감)", svc); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT to_char(date_trunc('week', :h_bizdate::date), 'IYYY"W"IW'), - count(*), coalesce(sum(amount),0) - INTO :h_wk, :h_cnt, :h_amt FROM purchase - WHERE date_trunc('week', biz_date) = date_trunc('week', :h_bizdate::date); - if (sqlca.sqlcode < 0) FAIL(b); + gets_(b, T_STR1, h_period, sizeof(h_period)); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + strcpy(h_key, ""); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0) { + setl(b, T_RC, 5001); + Bchg(b, T_MSG, 0, "영업일/기간 미입력 - 주간 마감 불가", 0L); + FAIL(b); + } + if (strlen(h_bizdate) > 0 && strlen(h_bizdate) < 8) { + setl(b, T_RC, 5002); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + FAIL(b); + } + + /* --- 섹션2: 정책/기존마감 상태 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_prev FROM cl_close_log + WHERE close_type = 'WEEKLY' AND period_key = :h_period AND status <> 'X'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5010); + Bchg(b, T_MSG, 0, "기존 마감 조회 실패", 0L); FAIL(b); + } + if (h_prev > 0) { + userlog("CL_WEEKLY %s 이미 마감 %ld건 존재 - 재집계 진행", h_period, h_prev); + } + EXEC SQL SELECT count(*) INTO :h_bps FROM cl_period_lock + WHERE period_key = :h_period AND locked = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5011); FAIL(b); } + if (h_bps > 0) { + setl(b, T_RC, 5012); + Bchg(b, T_MSG, 0, "기간이 잠금상태 - 마감 불가", 0L); FAIL(b); + } + + /* --- 섹션4: 본처리부(집계) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) + INTO :h_cnt, :h_amt, :h_fee, :h_net + FROM purchase WHERE biz_date >= :h_period::date AND biz_date < :h_period::date + 7; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5020); + Bchg(b, T_MSG, 0, "매입 집계 실패", 0L); FAIL(b); + } + + /* --- 섹션3: 업무규칙 분기부(규모/등급) --- */ + if (h_cnt == 0) { + grade = 0; + strcpy(memo, "WEEKLY:무거래"); + } else if (h_amt >= 1000000000) { + grade = 3; + strcpy(memo, "WEEKLY:대규모"); + } else if (h_amt >= 100000000) { + grade = 2; + strcpy(memo, "WEEKLY:중규모"); + } else { + grade = 1; + strcpy(memo, "WEEKLY:소규모"); + } + fee_calc = acq_fee(h_amt, 25); + if (h_fee == 0 && h_amt > 0) h_fee = fee_calc; + + /* --- 섹션4b: 마감이력 생성 --- */ cid = cldb_next_close_id(); - if (cid < 0) FAIL(b); - if (cldb_insert_close(cid, "WEEKLY", h_wk, h_bizdate, "C", h_cnt, h_amt, "주마감") < 0) FAIL(b); + if (cid < 0) { setl(b, T_RC, 5030); FAIL(b); } + if (cldb_insert_close(cid, "WEEKLY", h_period, + (h_bizdate[0] ? h_bizdate : h_period), + "C", h_cnt, h_amt, memo) < 0) { + setl(b, T_RC, 5031); + Bchg(b, T_MSG, 0, "마감이력 생성 실패", 0L); FAIL(b); + } + + /* --- 섹션6: 집계 확인/요약 재조회부 --- */ + h_kcid = cid; + EXEC SQL SELECT txn_count, total_amount INTO :h_prev, :h_amt + FROM cl_close_log WHERE close_id = :h_kcid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5040); FAIL(b); } + o_count = h_cnt; o_amount = h_amt; + + /* --- 섹션8prep: 상태/결과 세팅 --- */ setl(b, T_ID1, cid); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_amt); - Bchg(b, T_KEY1, 0, h_wk, 0L); - userlog("CL_WEEKLY cid=%ld wk=%s cnt=%ld 주마감", cid, h_wk, h_cnt); + setl(b, T_FEE, h_fee); + setl(b, T_NET, h_net); + Bchg(b, T_STATUS, 0, "C", 0L); + snprintf(o_msg, sizeof(o_msg), "주간 마감완료 등급%d cid=%ld", grade, cid); + o_rc = 0; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_WEEKLY 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 500000000L) { + z_sev = 3; + strcpy(z_msg, "고액 마감구간"); + } else if (h_zamt >= 50000000L) { + z_sev = 2; + strcpy(z_msg, "중액 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "소액 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_WEEKLY 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_WEEKLY 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_WEEKLY 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_WEEKLY 주간마감 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_WEEKLY 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_WEEKLY 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_WEEKVERIFY.pgc b/app/src/cl/svc/CL_WEEKVERIFY.pgc index ef33075..c7f19d0 100644 --- a/app/src/cl/svc/CL_WEEKVERIFY.pgc +++ b/app/src/cl/svc/CL_WEEKVERIFY.pgc @@ -8,25 +8,168 @@ void CL_WEEKVERIFY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long da; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long dcnt = 0, damt = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; - long h_la, h_ra; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_hdr_cnt, h_hdr_amt; + long h_real_cnt, h_real_amt; + char h_date[16], h_type[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_WEEKVERIFY"); + userlog("CL_WEEKVERIFY 진입: 마감처리 시작 (마감 정합 검증)", svc); + + h_cid = getl(b, T_ID1); gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT coalesce(sum(total_amount),0) INTO :h_la FROM cl_close_log - WHERE close_type = 'WEEKLY' - AND period_key = to_char(date_trunc('week', :h_bizdate::date), 'IYYY"W"IW') - AND status <> 'X'; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_ra FROM purchase - WHERE date_trunc('week', biz_date) = date_trunc('week', :h_bizdate::date); - if (sqlca.sqlcode < 0) FAIL(b); - da = h_la - h_ra; if (da < 0) da = -da; - setl(b, T_AMT1, h_la); - setl(b, T_AMT2, h_ra); - setl(b, T_DELTA, da); - setl(b, T_RC, (da == 0) ? 0 : 1); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0 && h_bizdate[0] == 0) { + setl(b, T_RC, 5401); + Bchg(b, T_MSG, 0, "마감ID/영업일 중 하나는 필수", 0L); FAIL(b); + } + + /* --- 섹션2: 마감이력 헤더 조회부 --- */ + if (h_cid > 0) { + EXEC SQL SELECT close_type, txn_count, total_amount, + to_char(biz_date,'YYYY-MM-DD') + INTO :h_type, :h_hdr_cnt, :h_hdr_amt, :h_date + FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5402); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5403); FAIL(b); } + } else { + strcpy(h_date, h_bizdate); + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(total_amount),0) + INTO :h_hdr_cnt, :h_hdr_amt FROM cl_close_log + WHERE period_key = :h_bizdate AND status <> 'X'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5404); FAIL(b); } + } + + /* --- 섹션4: 실매입 재집계부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_real_cnt, :h_real_amt + FROM purchase WHERE biz_date = :h_date; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5405); FAIL(b); } + + /* --- 섹션3: 대사 분기부 --- */ + dcnt = h_hdr_cnt - h_real_cnt; if (dcnt < 0) dcnt = -dcnt; + damt = h_hdr_amt - h_real_amt; if (damt < 0) damt = -damt; + if (dcnt == 0 && damt == 0) { + o_rc = 0; strcpy(o_msg, "건수·금액 완전 일치"); + } else if (dcnt == 0 && damt != 0) { + o_rc = 1; strcpy(o_msg, "건수 일치·금액 불일치"); + } else if (dcnt != 0 && damt == 0) { + o_rc = 2; strcpy(o_msg, "건수 불일치·금액 일치"); + } else { + o_rc = 3; strcpy(o_msg, "건수·금액 모두 불일치"); + } + + /* --- 섹션6: 결과 세팅 --- */ + setl(b, T_ARG1, h_hdr_cnt); + setl(b, T_ARG2, h_real_cnt); + setl(b, T_AMT1, h_hdr_amt); + setl(b, T_AMT2, h_real_amt); + setl(b, T_DELTA, damt); + o_count = dcnt; o_amount = damt; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_WEEKVERIFY 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 500000000L) { + z_sev = 3; + strcpy(z_msg, "고액 마감구간"); + } else if (h_zamt >= 50000000L) { + z_sev = 2; + strcpy(z_msg, "중액 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "소액 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_WEEKVERIFY 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_WEEKVERIFY 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_WEEKVERIFY 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_WEEKVERIFY 정합검증 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_WEEKVERIFY 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_WEEKVERIFY 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_YEAREND.pgc b/app/src/cl/svc/CL_YEAREND.pgc index 6c53f6d..ca51f7f 100644 --- a/app/src/cl/svc/CL_YEAREND.pgc +++ b/app/src/cl/svc/CL_YEAREND.pgc @@ -9,14 +9,205 @@ void CL_YEAREND(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], period[16]; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; long cid = 0; - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - strncpy(period, bizdate, 4); period[4] = 0; - if (cl_open_close(b, "YEAREND", period, bizdate, &cid) < 0) FAIL(b); + long fee_calc = 0; + int grade = 0; + char memo[128]; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; + EXEC SQL BEGIN DECLARE SECTION; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cnt, h_amt, h_fee, h_net; + long h_prev, h_bps; + char h_key[32]; + char h_status[4]; + char h_memo[128]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; + EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_YEAREND"); + userlog("CL_YEAREND 진입: 마감처리 시작 (연말 마감)", svc); + + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + gets_(b, T_STR1, h_period, sizeof(h_period)); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + strcpy(h_key, ""); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0) { + setl(b, T_RC, 5001); + Bchg(b, T_MSG, 0, "영업일/기간 미입력 - 연말 마감 불가", 0L); + FAIL(b); + } + if (strlen(h_bizdate) > 0 && strlen(h_bizdate) < 8) { + setl(b, T_RC, 5002); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + FAIL(b); + } + + /* --- 섹션2: 정책/기존마감 상태 조회부 --- */ + EXEC SQL SELECT count(*) INTO :h_prev FROM cl_close_log + WHERE close_type = 'YEAREND' AND period_key = :h_period AND status <> 'X'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5010); + Bchg(b, T_MSG, 0, "기존 마감 조회 실패", 0L); FAIL(b); + } + if (h_prev > 0) { + userlog("CL_YEAREND %s 이미 마감 %ld건 존재 - 재집계 진행", h_period, h_prev); + } + EXEC SQL SELECT count(*) INTO :h_bps FROM cl_period_lock + WHERE period_key = :h_period AND locked = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5011); FAIL(b); } + if (h_bps > 0) { + setl(b, T_RC, 5012); + Bchg(b, T_MSG, 0, "기간이 잠금상태 - 마감 불가", 0L); FAIL(b); + } + + /* --- 섹션4: 본처리부(집계) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) + INTO :h_cnt, :h_amt, :h_fee, :h_net + FROM purchase WHERE to_char(biz_date,'YYYY') = :h_period; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 5020); + Bchg(b, T_MSG, 0, "매입 집계 실패", 0L); FAIL(b); + } + + /* --- 섹션3: 업무규칙 분기부(규모/등급) --- */ + if (h_cnt == 0) { + grade = 0; + strcpy(memo, "YEAREND:무거래"); + } else if (h_amt >= 1000000000) { + grade = 3; + strcpy(memo, "YEAREND:대규모"); + } else if (h_amt >= 100000000) { + grade = 2; + strcpy(memo, "YEAREND:중규모"); + } else { + grade = 1; + strcpy(memo, "YEAREND:소규모"); + } + fee_calc = acq_fee(h_amt, 25); + if (h_fee == 0 && h_amt > 0) h_fee = fee_calc; + + /* --- 섹션4b: 마감이력 생성 --- */ + cid = cldb_next_close_id(); + if (cid < 0) { setl(b, T_RC, 5030); FAIL(b); } + if (cldb_insert_close(cid, "YEAREND", h_period, + (h_bizdate[0] ? h_bizdate : h_period), + "C", h_cnt, h_amt, memo) < 0) { + setl(b, T_RC, 5031); + Bchg(b, T_MSG, 0, "마감이력 생성 실패", 0L); FAIL(b); + } + + /* --- 섹션6: 집계 확인/요약 재조회부 --- */ + h_kcid = cid; + EXEC SQL SELECT txn_count, total_amount INTO :h_prev, :h_amt + FROM cl_close_log WHERE close_id = :h_kcid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5040); FAIL(b); } + o_count = h_cnt; o_amount = h_amt; + + /* --- 섹션8prep: 상태/결과 세팅 --- */ setl(b, T_ID1, cid); + setl(b, T_FEE, h_fee); + setl(b, T_NET, h_net); Bchg(b, T_STATUS, 0, "C", 0L); - userlog("CL_YEAREND cid=%ld 연도=%s 연마감 완료", cid, period); + snprintf(o_msg, sizeof(o_msg), "연말 마감완료 등급%d cid=%ld", grade, cid); + o_rc = 0; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_YEAREND 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 500000000L) { + z_sev = 3; + strcpy(z_msg, "고액 마감구간"); + } else if (h_zamt >= 50000000L) { + z_sev = 2; + strcpy(z_msg, "중액 마감구간"); + } else { + z_sev = 1; + strcpy(z_msg, "소액 마감구간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_YEAREND 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_YEAREND 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_YEAREND 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_YEAREND 연말마감 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_YEAREND 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_YEAREND 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_YEARPREP.pgc b/app/src/cl/svc/CL_YEARPREP.pgc index d309273..dd4cacf 100644 --- a/app/src/cl/svc/CL_YEARPREP.pgc +++ b/app/src/cl/svc/CL_YEARPREP.pgc @@ -9,28 +9,149 @@ void CL_YEARPREP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long missing; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long metric = 0; int i = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_months, h_ycnt; - char h_year[8]; + char h_bizdate[16]; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_n1, h_n2, h_n3, h_cid; + char h_key[32], h_status[4], h_out[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; - gets_(b, T_STR1, h_year, sizeof(h_year)); - if (h_year[0] == 0) strcpy(h_year, "2026"); - EXEC SQL SELECT count(DISTINCT period_key) INTO :h_months FROM cl_close_log - WHERE close_type = 'MONTHLY' AND status IN ('C','A','L') - AND period_key LIKE :h_year || '-%'; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_ycnt FROM cl_close_log - WHERE close_type = 'YEAREND' AND period_key = :h_year AND status <> 'X'; - if (sqlca.sqlcode < 0) FAIL(b); - missing = 12 - h_months; - if (missing < 0) missing = 0; - setl(b, T_ARG1, h_months); - setl(b, T_ARG2, missing); - setl(b, T_ARG3, h_ycnt); - if (h_ycnt > 0) setl(b, T_RC, 2); /* 이미 연말결산 존재 */ - else if (missing) setl(b, T_RC, 1); /* 월마감 누락 */ - else setl(b, T_RC, 0); /* 진행 가능 */ + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_YEARPREP"); + userlog("CL_YEARPREP 진입: 마감처리 시작 (연말 결산 사전점검: 연도 내 완료된 월마감(MONTHLY, C/A/L)의 distinct)", svc); + + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + gets_(b, T_STR1, h_period, sizeof(h_period)); + gets_(b, T_KEY1, h_key, sizeof(h_key)); + h_cid = getl(b, T_ID1); + if (h_period[0] == 0) strcpy(h_period, h_bizdate); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_bizdate[0] == 0 && h_period[0] == 0 && h_cid <= 0) { + setl(b, T_RC, 6601); + Bchg(b, T_MSG, 0, "기준(영업일/기간/마감ID) 중 하나는 필수", 0L); FAIL(b); + } + + /* --- 섹션2·섹션4: 기간내 미마감/누락 영업일 산출 --- */ + if (h_period[0] == 0) { + setl(b, T_RC, 6612); + Bchg(b, T_MSG, 0, "대상 기간(YYYY-MM 등) 필수", 0L); FAIL(b); + } + EXEC SQL SELECT count(DISTINCT p.biz_date) INTO :h_n1 + FROM purchase p + WHERE to_char(p.biz_date,'YYYY-MM') = substr(:h_period,1,7) + AND NOT EXISTS (SELECT 1 FROM cl_close_log c + WHERE c.close_type = 'DAILY' + AND c.period_key = to_char(p.biz_date,'YYYY-MM-DD') + AND c.status <> 'X'); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6613); FAIL(b); } + EXEC SQL SELECT count(DISTINCT biz_date) INTO :h_n2 FROM purchase + WHERE to_char(biz_date,'YYYY-MM') = substr(:h_period,1,7); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 6614); FAIL(b); } + metric = h_n1; o_count = h_n1; o_amount = h_n2; + if (h_n1 == 0) { o_rc = 0; strcpy(o_msg, "미마감 영업일 없음"); } + else if (h_n1 * 2 > h_n2) { o_rc = 2; strcpy(o_msg, "미마감 비중 과다"); } + else { o_rc = 1; strcpy(o_msg, "일부 미마감 잔존"); } + + /* --- 섹션3b: 규모 부가판정 --- */ + if (metric < 0) metric = 0; + setl(b, T_ARG5, metric); + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_YEARPREP 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1000000000L) { + z_sev = 3; + strcpy(z_msg, "대규모 마감기간"); + } else if (h_zamt >= 100000000L) { + z_sev = 2; + strcpy(z_msg, "중규모 마감기간"); + } else { + z_sev = 1; + strcpy(z_msg, "소규모 마감기간"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_YEARPREP 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_YEARPREP 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_YEARPREP 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_YEARPREP 연말 결산 사전점검: 연도 내 완료된 월마감(MONTHLY, C/A/L)의 distinct cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_YEARPREP 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_YEARPREP 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cl/svc/CL_YEARVERIFY.pgc b/app/src/cl/svc/CL_YEARVERIFY.pgc index 35a8cb4..a41bb63 100644 --- a/app/src/cl/svc/CL_YEARVERIFY.pgc +++ b/app/src/cl/svc/CL_YEARVERIFY.pgc @@ -8,28 +8,168 @@ void CL_YEARVERIFY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long dc, da; + char svc[24]; + char logmsg[192]; + char o_msg[128]; + long o_rc = 0, o_count = 0, o_amount = 0; + long dcnt = 0, damt = 0; + int z_sev = 0, z_comp = 0; + char z_msg[64]; + char z_buf[192]; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; - long h_lc, h_la, h_rc, h_ra; + char h_period[24]; + char h_svc[24]; + char h_logmsg[192]; + long h_cid, h_hdr_cnt, h_hdr_amt; + long h_real_cnt, h_real_amt; + char h_date[16], h_type[16]; + long h_zn, h_zcnt, h_zamt; + long h_zopen, h_zsev; + long h_kcid; + char h_zbuf[192]; EXEC SQL END DECLARE SECTION; + h_bizdate[0] = 0; h_period[0] = 0; h_zbuf[0] = 0; + z_msg[0] = 0; z_buf[0] = 0; h_zopen = 0; h_zsev = 0; + + + /* --- 섹션9: 진입 로깅 --- */ + strcpy(svc, "CL_YEARVERIFY"); + userlog("CL_YEARVERIFY 진입: 마감처리 시작 (마감 정합 검증)", svc); + + h_cid = getl(b, T_ID1); gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(total_amount),0) - INTO :h_lc, :h_la FROM cl_close_log - WHERE close_type = 'YEAREND' - AND period_key = to_char(:h_bizdate::date, 'YYYY') AND status <> 'X'; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_rc, :h_ra - FROM purchase WHERE to_char(biz_date,'YYYY') = to_char(:h_bizdate::date,'YYYY'); - if (sqlca.sqlcode < 0) FAIL(b); - dc = h_lc - h_rc; if (dc < 0) dc = -dc; - da = h_la - h_ra; if (da < 0) da = -da; - setl(b, T_ARG1, h_lc); - setl(b, T_COUNT, h_rc); - setl(b, T_AMT1, h_la); - setl(b, T_AMT2, h_ra); - setl(b, T_DELTA, da); - setl(b, T_RC, (dc == 0 && da == 0) ? 0 : 1); + + /* --- 섹션1: 입력 검증부 --- */ + if (h_cid <= 0 && h_bizdate[0] == 0) { + setl(b, T_RC, 5401); + Bchg(b, T_MSG, 0, "마감ID/영업일 중 하나는 필수", 0L); FAIL(b); + } + + /* --- 섹션2: 마감이력 헤더 조회부 --- */ + if (h_cid > 0) { + EXEC SQL SELECT close_type, txn_count, total_amount, + to_char(biz_date,'YYYY-MM-DD') + INTO :h_type, :h_hdr_cnt, :h_hdr_amt, :h_date + FROM cl_close_log WHERE close_id = :h_cid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 5402); + Bchg(b, T_MSG, 0, "마감이력 미존재", 0L); FAIL(b); + } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5403); FAIL(b); } + } else { + strcpy(h_date, h_bizdate); + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(total_amount),0) + INTO :h_hdr_cnt, :h_hdr_amt FROM cl_close_log + WHERE period_key = :h_bizdate AND status <> 'X'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5404); FAIL(b); } + } + + /* --- 섹션4: 실매입 재집계부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_real_cnt, :h_real_amt + FROM purchase WHERE biz_date = :h_date; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 5405); FAIL(b); } + + /* --- 섹션3: 대사 분기부 --- */ + dcnt = h_hdr_cnt - h_real_cnt; if (dcnt < 0) dcnt = -dcnt; + damt = h_hdr_amt - h_real_amt; if (damt < 0) damt = -damt; + if (dcnt == 0 && damt == 0) { + o_rc = 0; strcpy(o_msg, "건수·금액 완전 일치"); + } else if (dcnt == 0 && damt != 0) { + o_rc = 1; strcpy(o_msg, "건수 일치·금액 불일치"); + } else if (dcnt != 0 && damt == 0) { + o_rc = 2; strcpy(o_msg, "건수 불일치·금액 일치"); + } else { + o_rc = 3; strcpy(o_msg, "건수·금액 모두 불일치"); + } + + /* --- 섹션6: 결과 세팅 --- */ + setl(b, T_ARG1, h_hdr_cnt); + setl(b, T_ARG2, h_real_cnt); + setl(b, T_AMT1, h_hdr_amt); + setl(b, T_AMT2, h_real_amt); + setl(b, T_DELTA, damt); + o_count = dcnt; o_amount = damt; + + /* --- 섹션6b: 마감 집계/요약 재산출부 --- */ + EXEC SQL SELECT count(*), coalesce(sum(txn_count),0), + coalesce(sum(total_amount),0) + INTO :h_zn, :h_zcnt, :h_zamt + FROM cl_close_log + WHERE status <> 'X' + AND (:h_period = '' OR period_key = :h_period); + if (sqlca.sqlcode < 0) { + userlog("CL_YEARVERIFY 마감요약 재집계 실패[%d] - 요약 생략", sqlca.sqlcode); + h_zn = 0; h_zcnt = 0; h_zamt = 0; + } + setl(b, T_ARG6, h_zcnt); + setl(b, T_ARG7, h_zn); + + /* --- 섹션3c: 마감 규모별 추가 업무규칙 분기부 --- */ + if (h_zn == 0) { + z_sev = 0; + strcpy(z_msg, "마감이력 없음"); + } else if (h_zamt >= 1500000000L) { + z_sev = 3; + strcpy(z_msg, "광역 마감규모"); + } else if (h_zamt >= 150000000L) { + z_sev = 2; + strcpy(z_msg, "표준 마감규모"); + } else { + z_sev = 1; + strcpy(z_msg, "경량 마감규모"); + } + h_zsev = (long)z_sev; + + /* --- 섹션7: 예외/보상 판정부 (부분처리 방지) --- */ + if (o_rc != 0) { + z_comp = 1; + EXEC SQL SELECT count(*) INTO :h_zopen FROM cl_close_log + WHERE status = 'O' + AND (:h_bizdate = '' OR period_key = :h_bizdate); + if (sqlca.sqlcode < 0) h_zopen = 0; + if (h_zopen > 0) + userlog("CL_YEARVERIFY 경고: 미완결 개시(O) %ld건 잔존 - 보상대상", h_zopen); + userlog("CL_YEARVERIFY 예외분기 rc=%ld sev=%d - 상태전이 실패 이력 적재", + o_rc, z_sev); + } else { + z_comp = 0; + h_zopen = 0; + } + + /* --- 섹션5b: 처리 요약 이력 적재부 --- */ + snprintf(z_buf, sizeof(z_buf), "%s sev=%d comp=%d open=%ld %s", + svc, z_sev, z_comp, h_zopen, z_msg); + strncpy(h_zbuf, z_buf, sizeof(h_zbuf)-1); + h_zbuf[sizeof(h_zbuf)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, + CASE WHEN :h_zsev > 2 THEN 'W' ELSE 'I' END, :h_zbuf, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_YEARVERIFY 요약이력 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + setl(b, T_ARG8, h_zsev); + + /* --- 섹션5: 이력/감사 적재 (cm_log, best-effort) --- */ + snprintf(logmsg, sizeof(logmsg), "CL_YEARVERIFY 정합검증 cnt=%ld amt=%ld rc=%ld", + o_count, o_amount, o_rc); + strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); + h_logmsg[sizeof(h_logmsg)-1] = 0; + strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_logmsg, + COALESCE(NULLIF(:h_bizdate,'')::date, CURRENT_DATE)); + if (sqlca.sqlcode < 0) + userlog("CL_YEARVERIFY 감사로그 적재 실패[%d] - 업무는 계속", sqlca.sqlcode); + + /* --- 섹션8: 응답부 --- */ + setl(b, T_RC, o_rc); + setl(b, T_COUNT, o_count); + setl(b, T_GROSS, o_amount); + Bchg(b, T_MSG, 0, o_msg, 0L); + userlog("CL_YEARVERIFY 완료: rc=%ld count=%ld amount=%ld", o_rc, o_count, o_amount); OK(b); } diff --git a/app/src/cm/svc/CM_ADDDAYS.pgc b/app/src/cm/svc/CM_ADDDAYS.pgc index 2ce0d98..fa8cb06 100644 --- a/app/src/cm/svc/CM_ADDDAYS.pgc +++ b/app/src/cm/svc/CM_ADDDAYS.pgc @@ -18,7 +18,11 @@ static int cm_mdays(int y, int m) /* 5. CM_ADDDAYS - 달력일 N일 가산 (순수 계산). */ -void CM_ADDDAYS(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_ADDDAYS__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char in[16], out[16]; @@ -26,12 +30,138 @@ void CM_ADDDAYS(TPSVCINFO *p) int y, m, d; gets_(b, T_STR1, in, sizeof(in)); n = getl(b, T_ARG1); - if (sscanf(in, "%d-%d-%d", &y, &m, &d) != 3) FAIL(b); + if (sscanf(in, "%d-%d-%d", &y, &m, &d) != 3) return -1; for (i = 0; i < n; i++) { d++; if (d > cm_mdays(y, m)) { d = 1; if (++m > 12) { m = 1; y++; } } } sprintf(out, "%04d-%02d-%02d", y, m, d); sets_(b, T_STR3, out); + return 0; + return 0; +} + +/* CM_ADDDAYS 심화용 보조 판정 - 처리등급 산정. */ +static int CM_ADDDAYS__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 35L) return 1; /* 소량 */ + if (v < 1245L) return 2; /* 보통 */ + if (v < 100745L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_ADDDAYS - 달력일 N일 가산 (순수 계산).. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_ADDDAYS(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_ADDDAYS 진입: 달력일 N일 가산 (순수 계산). (svc=CM_ADDDAYS)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + rc = cmdb_is_holiday(hz_bizdate); + if (rc < 0) { userlog("CM_ADDDAYS 휴일 조회 오류"); } + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_holiday + WHERE holi_date >= :hz_bizdate; + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 잔여 휴일수 */ + if (rc == 1) { userlog("CM_ADDDAYS 기준일이 휴일 bizdate=%s", hz_bizdate); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (policy < 0) { + grade = 0; userlog("CM_ADDDAYS 기준정보 이상 - 보류"); + } else if (policy == 0) { + grade = 1; /* 최초 처리 */ + } else if (policy < 145L) { + grade = 2; /* 통상 처리 */ + } else { + grade = 3; userlog("CM_ADDDAYS 누적 과다 policy=%ld", policy); + } + if (grade >= 3 && band > 107450L) { + userlog("CM_ADDDAYS 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_ADDDAYS__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_ADDDAYS 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_ADDDAYS__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_ADDDAYS 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_ADDDAYS", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_holiday + WHERE holi_date BETWEEN :hz_bizdate AND '2026-12-31'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 연말까지 휴일수 */ + if (agg > 1245L) + userlog("CM_ADDDAYS 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_ADDDAYS 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_ADDDAYS 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_ADLER32.pgc b/app/src/cm/svc/CM_ADLER32.pgc index e3143f5..1222a1e 100644 --- a/app/src/cm/svc/CM_ADLER32.pgc +++ b/app/src/cm/svc/CM_ADLER32.pgc @@ -8,7 +8,11 @@ #define CM_MOD_ADLER 65521UL -void CM_ADLER32(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_ADLER32__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char in[256], hex[12]; @@ -16,7 +20,7 @@ void CM_ADLER32(TPSVCINFO *p) int i; gets_(b, T_STR1, in, sizeof(in)); - if (in[0] == 0) FAIL(b); + if (in[0] == 0) return -1; for (i = 0; in[i]; i++) { a = (a + (unsigned char)in[i]) % CM_MOD_ADLER; @@ -29,5 +33,131 @@ void CM_ADLER32(TPSVCINFO *p) setl(b, T_ARG2, (long)a); /* 하위 누산기 */ setl(b, T_ARG3, (long)s); /* 상위 누산기 */ sets_(b, T_STR4, hex); + return 0; + return 0; +} + +/* CM_ADLER32 심화용 보조 판정 - 처리등급 산정. */ +static int CM_ADLER32__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 30L) return 1; /* 소량 */ + if (v < 1200L) return 2; /* 보통 */ + if (v < 100700L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_ADLER32 - * CM_ADLER32.pgc - cm 모듈 서비스 CM_ADLER32 (one service = one f. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_ADLER32(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_ADLER32 진입: * CM_ADLER32.pgc - cm 모듈 서비스 CM_ADLER32 (one service = one f (svc=CM_ADLER32)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + if (cmdb_get_param("VAT_RATE", hz_pval) == 0) policy = atol(hz_pval); + else policy = 10; /* 기본 부가세율 */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log + WHERE svc_name = 'CM_ADLER32' AND biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_ADLER32 처리이력 조회 오류"); } + else { policy += (hz_cnt % 7); } /* 반복호출 보정 */ + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 7L) { + grade = 1; /* 저빈도 */ + } else if (policy < 110L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_ADLER32 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 107000L) { + userlog("CM_ADLER32 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_ADLER32__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_ADLER32 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_ADLER32__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_ADLER32 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_ADLER32", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_ADLER32'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1200L) + userlog("CM_ADLER32 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_ADLER32 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_ADLER32 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_AMT_HANGUL.pgc b/app/src/cm/svc/CM_AMT_HANGUL.pgc index 808c25a..622af05 100644 --- a/app/src/cm/svc/CM_AMT_HANGUL.pgc +++ b/app/src/cm/svc/CM_AMT_HANGUL.pgc @@ -28,7 +28,11 @@ static int cm_han_grp(long v, char *dst) return n; } -void CM_AMT_HANGUL(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_AMT_HANGUL__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char out[256], grp[64]; @@ -37,7 +41,7 @@ void CM_AMT_HANGUL(TPSVCINFO *p) amt = getl(b, T_AMOUNT); if (amt < 0) { n += sprintf(out, "마이너스"); amt = -amt; } - if (amt >= 10000000000000000L) { setl(b, T_RC, 1); FAIL(b); } /* 조 단위 초과 */ + if (amt >= 10000000000000000L) { setl(b, T_RC, 1); return -1; } /* 조 단위 초과 */ for (gi = 0; gi < 4; gi++) { /* 만 단위 그룹 분해 */ part[gi] = amt % 10000; @@ -55,5 +59,127 @@ void CM_AMT_HANGUL(TPSVCINFO *p) sprintf(out + n, "원"); } sets_(b, T_STR4, out); + return 0; + return 0; +} + +/* CM_AMT_HANGUL 심화용 보조 판정 - 처리등급 산정. */ +static int CM_AMT_HANGUL__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 17L) return 1; /* 소량 */ + if (v < 1007L) return 2; /* 보통 */ + if (v < 101007L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_AMT_HANGUL - * CM_AMT_HANGUL.pgc - cm 모듈 서비스 CM_AMT_HANGUL (one service =. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_AMT_HANGUL(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_AMT_HANGUL 진입: * CM_AMT_HANGUL.pgc - cm 모듈 서비스 CM_AMT_HANGUL (one service = (svc=CM_AMT_HANGUL)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + if (cmdb_get_param("VAT_RATE", hz_pval) == 0) policy = atol(hz_pval); + else policy = 10; /* 기본 부가세율 */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log + WHERE svc_name = 'CM_AMT_HANGUL' AND biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_AMT_HANGUL 처리이력 조회 오류"); } + else { policy += (hz_cnt % 7); } /* 반복호출 보정 */ + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band <= 0) { + grade = 0; userlog("CM_AMT_HANGUL 무금액 요청 - 조회전용 경로"); + } else if (band < 107L) { + grade = 1; band = band + acq_fee(band, 25); /* 소액 수수료 가산 */ + } else if (band < 11007L) { + grade = 2; band = band + acq_vat(band / 10); /* 중액 부가세 */ + } else { + grade = 3; band = band + acq_wht_amt(band, 2200); /* 대액 원천 */ + } + if (grade >= 3 && band > 110070L) { + userlog("CM_AMT_HANGUL 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_AMT_HANGUL__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_AMT_HANGUL 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_AMT_HANGUL__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_AMT_HANGUL 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_AMT_HANGUL", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_AMT_HANGUL'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1007L) + userlog("CM_AMT_HANGUL 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_AMT_HANGUL 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_AMT_HANGUL 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_AMT_ROUND.pgc b/app/src/cm/svc/CM_AMT_ROUND.pgc index 5415c42..6978c87 100644 --- a/app/src/cm/svc/CM_AMT_ROUND.pgc +++ b/app/src/cm/svc/CM_AMT_ROUND.pgc @@ -6,12 +6,142 @@ #include "CM_AMT_ROUND.h" /* 8. CM_AMT_ROUND - 금액 반올림 (순수 계산): 단위(T_ARG1)로 반올림. */ -void CM_AMT_ROUND(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_AMT_ROUND__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; long amt = getl(b, T_AMOUNT), unit = getl(b, T_ARG1), r; if (unit <= 0) unit = 1; r = ((amt + unit / 2) / unit) * unit; setl(b, T_AMT1, r); + return 0; + return 0; +} + +/* CM_AMT_ROUND 심화용 보조 판정 - 처리등급 산정. */ +static int CM_AMT_ROUND__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 42L) return 1; /* 소량 */ + if (v < 1452L) return 2; /* 보통 */ + if (v < 100952L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_AMT_ROUND - 금액 반올림 (순수 계산): 단위(T_ARG1)로 반올림.. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_AMT_ROUND(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_AMT_ROUND 진입: 금액 반올림 (순수 계산): 단위(T_ARG1)로 반올림. (svc=CM_AMT_ROUND)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + if (cmdb_get_param("VAT_RATE", hz_pval) == 0) policy = atol(hz_pval); + else policy = 10; /* 기본 부가세율 */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log + WHERE svc_name = 'CM_AMT_ROUND' AND biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_AMT_ROUND 처리이력 조회 오류"); } + else { policy += (hz_cnt % 7); } /* 반복호출 보정 */ + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band <= 0) { + grade = 0; userlog("CM_AMT_ROUND 무금액 요청 - 조회전용 경로"); + } else if (band < 152L) { + grade = 1; band = band + acq_fee(band, 25); /* 소액 수수료 가산 */ + } else if (band < 10952L) { + grade = 2; band = band + acq_vat(band / 10); /* 중액 부가세 */ + } else { + grade = 3; band = band + acq_wht_amt(band, 2200); /* 대액 원천 */ + } + if (grade >= 3 && band > 109520L) { + userlog("CM_AMT_ROUND 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_AMT_ROUND__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_AMT_ROUND 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_AMT_ROUND__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_AMT_ROUND 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_AMT_ROUND", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_AMT_ROUND'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1452L) + userlog("CM_AMT_ROUND 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_AMT_ROUND 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_AMT_ROUND 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_AMT_SPLIT.pgc b/app/src/cm/svc/CM_AMT_SPLIT.pgc index 8330a16..ba466f5 100644 --- a/app/src/cm/svc/CM_AMT_SPLIT.pgc +++ b/app/src/cm/svc/CM_AMT_SPLIT.pgc @@ -6,7 +6,11 @@ #include "CM_AMT_SPLIT.h" /* 9. CM_AMT_SPLIT - 금액 균등 분할 (순수 계산): n등분 + 잔액. */ -void CM_AMT_SPLIT(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_AMT_SPLIT__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; long amt = getl(b, T_AMOUNT), n = getl(b, T_ARG1), each, rem; @@ -16,5 +20,131 @@ void CM_AMT_SPLIT(TPSVCINFO *p) setl(b, T_AMT1, each); setl(b, T_AMT2, rem); setl(b, T_COUNT, n); + return 0; + return 0; +} + +/* CM_AMT_SPLIT 심화용 보조 판정 - 처리등급 산정. */ +static int CM_AMT_SPLIT__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 46L) return 1; /* 소량 */ + if (v < 1456L) return 2; /* 보통 */ + if (v < 100956L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_AMT_SPLIT - 금액 균등 분할 (순수 계산): n등분 + 잔액.. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_AMT_SPLIT(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_AMT_SPLIT 진입: 금액 균등 분할 (순수 계산): n등분 + 잔액. (svc=CM_AMT_SPLIT)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + if (cmdb_get_param("VAT_RATE", hz_pval) == 0) policy = atol(hz_pval); + else policy = 10; /* 기본 부가세율 */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log + WHERE svc_name = 'CM_AMT_SPLIT' AND biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_AMT_SPLIT 처리이력 조회 오류"); } + else { policy += (hz_cnt % 7); } /* 반복호출 보정 */ + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band <= 0) { + grade = 0; userlog("CM_AMT_SPLIT 무금액 요청 - 조회전용 경로"); + } else if (band < 156L) { + grade = 1; band = band + acq_fee(band, 25); /* 소액 수수료 가산 */ + } else if (band < 10956L) { + grade = 2; band = band + acq_vat(band / 10); /* 중액 부가세 */ + } else { + grade = 3; band = band + acq_wht_amt(band, 2200); /* 대액 원천 */ + } + if (grade >= 3 && band > 109560L) { + userlog("CM_AMT_SPLIT 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_AMT_SPLIT__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_AMT_SPLIT 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_AMT_SPLIT__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_AMT_SPLIT 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_AMT_SPLIT", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_AMT_SPLIT'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1456L) + userlog("CM_AMT_SPLIT 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_AMT_SPLIT 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_AMT_SPLIT 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_AUDIT_BYLVL.pgc b/app/src/cm/svc/CM_AUDIT_BYLVL.pgc index 2b29c81..3f646bf 100644 --- a/app/src/cm/svc/CM_AUDIT_BYLVL.pgc +++ b/app/src/cm/svc/CM_AUDIT_BYLVL.pgc @@ -6,7 +6,11 @@ */ #include "CM_AUDIT_BYLVL.h" -void CM_AUDIT_BYLVL(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_AUDIT_BYLVL__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; @@ -17,22 +21,145 @@ void CM_AUDIT_BYLVL(TPSVCINFO *p) gets_(b, T_STR1, h_lvl, sizeof(h_lvl)); gets_(b, T_BIZDATE, h_date, sizeof(h_date)); if (h_lvl[0] != 'I' && h_lvl[0] != 'W' && h_lvl[0] != 'E') - { setl(b, T_RC, 1); FAIL(b); } /* 레벨 오류 */ + { setl(b, T_RC, 1); return -1; } /* 레벨 오류 */ h_lvl[1] = 0; if (h_date[0] == 0) { if (cmdb_get_param("BIZ_DATE", h_date) < 0) strcpy(h_date, "2026-07-19"); } EXEC SQL SELECT count(*) INTO :h_cnt FROM cm_log WHERE log_level = :h_lvl AND biz_date = :h_date; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; h_lastmsg[0] = 0; if (h_cnt > 0) { EXEC SQL SELECT msg INTO :h_lastmsg FROM cm_log WHERE log_level = :h_lvl AND biz_date = :h_date ORDER BY log_id DESC LIMIT 1; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; } setl(b, T_COUNT, h_cnt); sets_(b, T_MSG, h_lastmsg); + return 0; + return 0; +} + +/* CM_AUDIT_BYLVL 심화용 보조 판정 - 처리등급 산정. */ +static int CM_AUDIT_BYLVL__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 32L) return 1; /* 소량 */ + if (v < 1102L) return 2; /* 보통 */ + if (v < 101102L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_AUDIT_BYLVL - 공통 처리. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_AUDIT_BYLVL(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_AUDIT_BYLVL 진입: 공통 처리 (svc=CM_AUDIT_BYLVL)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_AUDIT_BYLVL 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 7L) + userlog("CM_AUDIT_BYLVL 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 13L) { + grade = 1; /* 저빈도 */ + } else if (policy < 112L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_AUDIT_BYLVL 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 111020L) { + userlog("CM_AUDIT_BYLVL 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_AUDIT_BYLVL__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_AUDIT_BYLVL 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_AUDIT_BYLVL__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_AUDIT_BYLVL 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_AUDIT_BYLVL", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_AUDIT_BYLVL'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1102L) + userlog("CM_AUDIT_BYLVL 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_AUDIT_BYLVL 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_AUDIT_BYLVL 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_AUDIT_BYSVC.pgc b/app/src/cm/svc/CM_AUDIT_BYSVC.pgc index cd37334..b62bbd2 100644 --- a/app/src/cm/svc/CM_AUDIT_BYSVC.pgc +++ b/app/src/cm/svc/CM_AUDIT_BYSVC.pgc @@ -6,7 +6,11 @@ */ #include "CM_AUDIT_BYSVC.h" -void CM_AUDIT_BYSVC(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_AUDIT_BYSVC__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; @@ -18,7 +22,7 @@ void CM_AUDIT_BYSVC(TPSVCINFO *p) gets_(b, T_SVCNAME, svc, sizeof(svc)); gets_(b, T_BIZDATE, h_date, sizeof(h_date)); - if (svc[0] == 0) FAIL(b); + if (svc[0] == 0) return -1; if (h_date[0] == 0) { if (cmdb_get_param("BIZ_DATE", h_date) < 0) strcpy(h_date, "2026-07-19"); } sl = strlen(svc); if (svc[sl - 1] == '*') snprintf(h_pat, sizeof(h_pat), "%.*s%%", (int)(sl - 1), svc); @@ -27,10 +31,137 @@ void CM_AUDIT_BYSVC(TPSVCINFO *p) EXEC SQL SELECT count(*), coalesce(min(log_id), 0), coalesce(max(log_id), 0) INTO :h_cnt, :h_lo, :h_hi FROM cm_log WHERE svc_name LIKE :h_pat AND biz_date = :h_date; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; setl(b, T_COUNT, h_cnt); setl(b, T_ID1, h_lo); setl(b, T_ID2, h_hi); + return 0; + return 0; +} + +/* CM_AUDIT_BYSVC 심화용 보조 판정 - 처리등급 산정. */ +static int CM_AUDIT_BYSVC__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 30L) return 1; /* 소량 */ + if (v < 1100L) return 2; /* 보통 */ + if (v < 101100L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_AUDIT_BYSVC - 공통 처리. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_AUDIT_BYSVC(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_AUDIT_BYSVC 진입: 공통 처리 (svc=CM_AUDIT_BYSVC)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_AUDIT_BYSVC 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 5L) + userlog("CM_AUDIT_BYSVC 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 11L) { + grade = 1; /* 저빈도 */ + } else if (policy < 110L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_AUDIT_BYSVC 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 111000L) { + userlog("CM_AUDIT_BYSVC 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_AUDIT_BYSVC__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_AUDIT_BYSVC 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_AUDIT_BYSVC__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_AUDIT_BYSVC 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_AUDIT_BYSVC", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_AUDIT_BYSVC'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1100L) + userlog("CM_AUDIT_BYSVC 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_AUDIT_BYSVC 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_AUDIT_BYSVC 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_AUDIT_ERRPCT.pgc b/app/src/cm/svc/CM_AUDIT_ERRPCT.pgc index 99c3462..be307b1 100644 --- a/app/src/cm/svc/CM_AUDIT_ERRPCT.pgc +++ b/app/src/cm/svc/CM_AUDIT_ERRPCT.pgc @@ -6,7 +6,11 @@ */ #include "CM_AUDIT_ERRPCT.h" -void CM_AUDIT_ERRPCT(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_AUDIT_ERRPCT__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char bizdate[16]; @@ -18,12 +22,135 @@ void CM_AUDIT_ERRPCT(TPSVCINFO *p) if (thr <= 0) thr = 500; ratio = cmdb_log_err_ratio(bizdate); - if (ratio < 0) FAIL(b); + if (ratio < 0) return -1; alarmed = (ratio > thr) ? 1 : 0; setl(b, T_ARG2, ratio); /* 오류 비율(bps) */ setl(b, T_ARG3, thr); /* 적용 임계 */ sets_(b, T_STATUS, alarmed ? "ALERT" : "OK"); setl(b, T_RC, alarmed); + return 0; + return 0; +} + +/* CM_AUDIT_ERRPCT 심화용 보조 판정 - 처리등급 산정. */ +static int CM_AUDIT_ERRPCT__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 23L) return 1; /* 소량 */ + if (v < 1173L) return 2; /* 보통 */ + if (v < 101173L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_AUDIT_ERRPCT - * CM_AUDIT_ERRPCT.pgc - cm 모듈 서비스 CM_AUDIT_ERRPCT (one servi. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_AUDIT_ERRPCT(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_AUDIT_ERRPCT 진입: * CM_AUDIT_ERRPCT.pgc - cm 모듈 서비스 CM_AUDIT_ERRPCT (one servi (svc=CM_AUDIT_ERRPCT)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_AUDIT_ERRPCT 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 18L) + userlog("CM_AUDIT_ERRPCT 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 12L) { + grade = 1; /* 저빈도 */ + } else if (policy < 103L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_AUDIT_ERRPCT 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 111730L) { + userlog("CM_AUDIT_ERRPCT 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_AUDIT_ERRPCT__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_AUDIT_ERRPCT 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_AUDIT_ERRPCT__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_AUDIT_ERRPCT 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_AUDIT_ERRPCT", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_AUDIT_ERRPCT'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1173L) + userlog("CM_AUDIT_ERRPCT 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_AUDIT_ERRPCT 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_AUDIT_ERRPCT 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_AUDIT_RANGE.pgc b/app/src/cm/svc/CM_AUDIT_RANGE.pgc index ed6f98e..b3fb037 100644 --- a/app/src/cm/svc/CM_AUDIT_RANGE.pgc +++ b/app/src/cm/svc/CM_AUDIT_RANGE.pgc @@ -6,7 +6,11 @@ */ #include "CM_AUDIT_RANGE.h" -void CM_AUDIT_RANGE(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_AUDIT_RANGE__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; @@ -15,18 +19,141 @@ void CM_AUDIT_RANGE(TPSVCINFO *p) h_lo = getl(b, T_ID1); h_hi = getl(b, T_ID2); - if (h_lo <= 0 || h_hi < h_lo) { setl(b, T_RC, 1); FAIL(b); } /* 구간 역전 */ - if (h_hi - h_lo > 10000) { setl(b, T_RC, 2); FAIL(b); } /* 구간 과대 */ + if (h_lo <= 0 || h_hi < h_lo) { setl(b, T_RC, 1); return -1; } /* 구간 역전 */ + if (h_hi - h_lo > 10000) { setl(b, T_RC, 2); return -1; } /* 구간 과대 */ EXEC SQL SELECT count(*), coalesce(sum(CASE WHEN log_level = 'E' THEN 1 ELSE 0 END), 0), count(DISTINCT svc_name) INTO :h_cnt, :h_err, :h_svcs FROM cm_log WHERE log_id BETWEEN :h_lo AND :h_hi; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; setl(b, T_COUNT, h_cnt); setl(b, T_ARG1, h_err); setl(b, T_ARG2, h_svcs); + return 0; + return 0; +} + +/* CM_AUDIT_RANGE 심화용 보조 판정 - 처리등급 산정. */ +static int CM_AUDIT_RANGE__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 44L) return 1; /* 소량 */ + if (v < 1074L) return 2; /* 보통 */ + if (v < 101074L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_AUDIT_RANGE - * CM_AUDIT_RANGE.pgc - cm 모듈 서비스 CM_AUDIT_RANGE (one service. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_AUDIT_RANGE(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_AUDIT_RANGE 진입: * CM_AUDIT_RANGE.pgc - cm 모듈 서비스 CM_AUDIT_RANGE (one service (svc=CM_AUDIT_RANGE)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_AUDIT_RANGE 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 19L) + userlog("CM_AUDIT_RANGE 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 9L) { + grade = 1; /* 저빈도 */ + } else if (policy < 84L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_AUDIT_RANGE 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 110740L) { + userlog("CM_AUDIT_RANGE 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_AUDIT_RANGE__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_AUDIT_RANGE 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_AUDIT_RANGE__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_AUDIT_RANGE 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_AUDIT_RANGE", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_AUDIT_RANGE'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1074L) + userlog("CM_AUDIT_RANGE 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_AUDIT_RANGE 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_AUDIT_RANGE 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_AUDIT_TAIL.pgc b/app/src/cm/svc/CM_AUDIT_TAIL.pgc index 1437e92..4c7fb50 100644 --- a/app/src/cm/svc/CM_AUDIT_TAIL.pgc +++ b/app/src/cm/svc/CM_AUDIT_TAIL.pgc @@ -6,7 +6,11 @@ */ #include "CM_AUDIT_TAIL.h" -void CM_AUDIT_TAIL(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_AUDIT_TAIL__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; long n, seen = 0, newest = 0, worst = 0; /* worst: 0=I,1=W,2=E */ @@ -26,13 +30,13 @@ void CM_AUDIT_TAIL(TPSVCINFO *p) SELECT log_id, log_level FROM cm_log WHERE biz_date = :h_date ORDER BY log_id DESC LIMIT :h_limit; EXEC SQL OPEN cur_tail; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; for (;;) { long rank; EXEC SQL FETCH cur_tail INTO :h_id, :h_lvl; if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_tail; FAIL(b); } + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_tail; return -1; } if (seen == 0) newest = h_id; /* 첫 행 = 최신 */ rank = (h_lvl[0] == 'E') ? 2 : (h_lvl[0] == 'W') ? 1 : 0; if (rank > worst) worst = rank; @@ -43,5 +47,132 @@ void CM_AUDIT_TAIL(TPSVCINFO *p) setl(b, T_COUNT, seen); /* 반환 대상 건수 */ setl(b, T_ID1, newest); /* 최신 로그 ID */ sets_(b, T_STATUS, worst == 2 ? "E" : worst == 1 ? "W" : "I"); + return 0; + return 0; +} + +/* CM_AUDIT_TAIL 심화용 보조 판정 - 처리등급 산정. */ +static int CM_AUDIT_TAIL__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 17L) return 1; /* 소량 */ + if (v < 1007L) return 2; /* 보통 */ + if (v < 101007L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_AUDIT_TAIL - * CM_AUDIT_TAIL.pgc - cm 모듈 서비스 CM_AUDIT_TAIL (one service =. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_AUDIT_TAIL(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_AUDIT_TAIL 진입: * CM_AUDIT_TAIL.pgc - cm 모듈 서비스 CM_AUDIT_TAIL (one service = (svc=CM_AUDIT_TAIL)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_AUDIT_TAIL 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 12L) + userlog("CM_AUDIT_TAIL 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 14L) { + grade = 1; /* 저빈도 */ + } else if (policy < 97L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_AUDIT_TAIL 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 110070L) { + userlog("CM_AUDIT_TAIL 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_AUDIT_TAIL__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_AUDIT_TAIL 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_AUDIT_TAIL__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_AUDIT_TAIL 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_AUDIT_TAIL", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_AUDIT_TAIL'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1007L) + userlog("CM_AUDIT_TAIL 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_AUDIT_TAIL 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_AUDIT_TAIL 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_AUDIT_TOP.pgc b/app/src/cm/svc/CM_AUDIT_TOP.pgc index 86212a3..e6c1783 100644 --- a/app/src/cm/svc/CM_AUDIT_TOP.pgc +++ b/app/src/cm/svc/CM_AUDIT_TOP.pgc @@ -6,7 +6,11 @@ */ #include "CM_AUDIT_TOP.h" -void CM_AUDIT_TOP(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_AUDIT_TOP__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; @@ -20,16 +24,139 @@ void CM_AUDIT_TOP(TPSVCINFO *p) EXEC SQL SELECT svc_name, count(*) INTO :h_top, :h_cnt FROM cm_log WHERE biz_date = :h_date GROUP BY svc_name ORDER BY count(*) DESC, svc_name LIMIT 1; - if (sqlca.sqlcode == 100) { setl(b, T_RC, 1); FAIL(b); } /* 로그 없음 */ - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { setl(b, T_RC, 1); return -1; } /* 로그 없음 */ + if (sqlca.sqlcode < 0) return -1; EXEC SQL SELECT count(DISTINCT svc_name) INTO :h_distinct FROM cm_log WHERE biz_date = :h_date; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; sets_(b, T_SVCNAME, h_top); /* 최다 로그 서비스 */ setl(b, T_COUNT, h_cnt); /* 그 서비스 로그 수 */ setl(b, T_ARG1, h_distinct); /* 고유 서비스 수 */ setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_AUDIT_TOP 심화용 보조 판정 - 처리등급 산정. */ +static int CM_AUDIT_TOP__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 42L) return 1; /* 소량 */ + if (v < 1452L) return 2; /* 보통 */ + if (v < 100952L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_AUDIT_TOP - * CM_AUDIT_TOP.pgc - cm 모듈 서비스 CM_AUDIT_TOP (one service = o. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_AUDIT_TOP(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_AUDIT_TOP 진입: * CM_AUDIT_TOP.pgc - cm 모듈 서비스 CM_AUDIT_TOP (one service = o (svc=CM_AUDIT_TOP)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_AUDIT_TOP 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 17L) + userlog("CM_AUDIT_TOP 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 7L) { + grade = 1; /* 저빈도 */ + } else if (policy < 122L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_AUDIT_TOP 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 109520L) { + userlog("CM_AUDIT_TOP 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_AUDIT_TOP__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_AUDIT_TOP 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_AUDIT_TOP__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_AUDIT_TOP 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_AUDIT_TOP", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_AUDIT_TOP'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1452L) + userlog("CM_AUDIT_TOP 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_AUDIT_TOP 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_AUDIT_TOP 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_BATDEP_CHK.pgc b/app/src/cm/svc/CM_BATDEP_CHK.pgc index b47aa74..1cfd4a7 100644 --- a/app/src/cm/svc/CM_BATDEP_CHK.pgc +++ b/app/src/cm/svc/CM_BATDEP_CHK.pgc @@ -6,7 +6,11 @@ */ #include "CM_BATDEP_CHK.h" -void CM_BATDEP_CHK(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_BATDEP_CHK__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char job[32], depkey[40], dep[128]; @@ -17,7 +21,7 @@ void CM_BATDEP_CHK(TPSVCINFO *p) EXEC SQL END DECLARE SECTION; gets_(b, T_KEY1, job, sizeof(job)); - if (job[0] == 0) FAIL(b); + if (job[0] == 0) return -1; gets_(b, T_BIZDATE, h_date, sizeof(h_date)); if (h_date[0] == 0) { if (cmdb_get_param("BIZ_DATE", h_date) < 0) strcpy(h_date, "2026-07-19"); } @@ -25,17 +29,140 @@ void CM_BATDEP_CHK(TPSVCINFO *p) if (cmdb_get_param(depkey, dep) < 0) { /* 선행 없음 = 즉시 가능 */ sets_(b, T_STR4, ""); setl(b, T_RC, 0); - OK(b); + return 0; } snprintf(h_pat, sizeof(h_pat), "%s DONE%%", dep); EXEC SQL SELECT count(*) INTO :h_done FROM cm_log WHERE svc_name = 'BATRUN' AND biz_date = :h_date AND msg LIKE :h_pat; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; ready = (h_done > 0) ? 1 : 0; sets_(b, T_STR4, dep); /* 선행 잡명 */ setl(b, T_COUNT, h_done); setl(b, T_RC, ready ? 0 : 1); /* 0=기동가능, 1=선행 미완 */ + return 0; + return 0; +} + +/* CM_BATDEP_CHK 심화용 보조 판정 - 처리등급 산정. */ +static int CM_BATDEP_CHK__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 30L) return 1; /* 소량 */ + if (v < 1480L) return 2; /* 보통 */ + if (v < 100980L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_BATDEP_CHK - * CM_BATDEP_CHK.pgc - cm 모듈 서비스 CM_BATDEP_CHK (one service =. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_BATDEP_CHK(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_BATDEP_CHK 진입: * CM_BATDEP_CHK.pgc - cm 모듈 서비스 CM_BATDEP_CHK (one service = (svc=CM_BATDEP_CHK)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_BATDEP_CHK 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 5L) + userlog("CM_BATDEP_CHK 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (policy < 0) { + grade = 0; userlog("CM_BATDEP_CHK 기준정보 이상 - 보류"); + } else if (policy == 0) { + grade = 1; /* 최초 처리 */ + } else if (policy < 180L) { + grade = 2; /* 통상 처리 */ + } else { + grade = 3; userlog("CM_BATDEP_CHK 누적 과다 policy=%ld", policy); + } + if (grade >= 3 && band > 109800L) { + userlog("CM_BATDEP_CHK 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_BATDEP_CHK__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_BATDEP_CHK 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_BATDEP_CHK__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_BATDEP_CHK 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_BATDEP_CHK", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_BATDEP_CHK'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1480L) + userlog("CM_BATDEP_CHK 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_BATDEP_CHK 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_BATDEP_CHK 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_BATDEP_SET.pgc b/app/src/cm/svc/CM_BATDEP_SET.pgc index 89ab888..027b263 100644 --- a/app/src/cm/svc/CM_BATDEP_SET.pgc +++ b/app/src/cm/svc/CM_BATDEP_SET.pgc @@ -6,7 +6,11 @@ */ #include "CM_BATDEP_SET.h" -void CM_BATDEP_SET(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_BATDEP_SET__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char job[32], dep[32], depkey[40], schdkey[40], chk[128], bizdate[16], hist[128]; @@ -14,19 +18,142 @@ void CM_BATDEP_SET(TPSVCINFO *p) gets_(b, T_KEY1, job, sizeof(job)); gets_(b, T_STR1, dep, sizeof(dep)); - if (job[0] == 0 || dep[0] == 0) FAIL(b); - if (strcmp(job, dep) == 0) { setl(b, T_RC, 1); FAIL(b); } /* 자기순환 */ + if (job[0] == 0 || dep[0] == 0) return -1; + if (strcmp(job, dep) == 0) { setl(b, T_RC, 1); return -1; } /* 자기순환 */ snprintf(schdkey, sizeof(schdkey), "SCHD:%s", dep); if (cmdb_get_param(schdkey, chk) < 0) warn = 1; /* 선행 미등록 */ snprintf(depkey, sizeof(depkey), "DEP:%s", job); - if (cmdb_set_param(depkey, dep) < 0) FAIL(b); + if (cmdb_set_param(depkey, dep) < 0) return -1; gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); if (bizdate[0] == 0) { if (cmdb_get_param("BIZ_DATE", bizdate) < 0) strcpy(bizdate, "2026-07-19"); } snprintf(hist, sizeof(hist), "DEP %s <- %s%s", job, dep, warn ? " (WARN)" : ""); cmdb_log("BATDEP", warn ? "W" : "I", hist, bizdate); setl(b, T_RC, warn); /* 0=정상, 1=선행 미등록 */ + return 0; + return 0; +} + +/* CM_BATDEP_SET 심화용 보조 판정 - 처리등급 산정. */ +static int CM_BATDEP_SET__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 12L) return 1; /* 소량 */ + if (v < 1002L) return 2; /* 보통 */ + if (v < 101002L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_BATDEP_SET - * CM_BATDEP_SET.pgc - cm 모듈 서비스 CM_BATDEP_SET (one service =. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_BATDEP_SET(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_BATDEP_SET 진입: * CM_BATDEP_SET.pgc - cm 모듈 서비스 CM_BATDEP_SET (one service = (svc=CM_BATDEP_SET)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_BATDEP_SET 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 7L) + userlog("CM_BATDEP_SET 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (policy < 0) { + grade = 0; userlog("CM_BATDEP_SET 기준정보 이상 - 보류"); + } else if (policy == 0) { + grade = 1; /* 최초 처리 */ + } else if (policy < 102L) { + grade = 2; /* 통상 처리 */ + } else { + grade = 3; userlog("CM_BATDEP_SET 누적 과다 policy=%ld", policy); + } + if (grade >= 3 && band > 110020L) { + userlog("CM_BATDEP_SET 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_BATDEP_SET__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_BATDEP_SET 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_BATDEP_SET__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_BATDEP_SET 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_BATDEP_SET", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_BATDEP_SET'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1002L) + userlog("CM_BATDEP_SET 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_BATDEP_SET 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_BATDEP_SET 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_BATHIST_CNT.pgc b/app/src/cm/svc/CM_BATHIST_CNT.pgc index 044646c..766a224 100644 --- a/app/src/cm/svc/CM_BATHIST_CNT.pgc +++ b/app/src/cm/svc/CM_BATHIST_CNT.pgc @@ -6,7 +6,11 @@ */ #include "CM_BATHIST_CNT.h" -void CM_BATHIST_CNT(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_BATHIST_CNT__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; @@ -16,7 +20,7 @@ void CM_BATHIST_CNT(TPSVCINFO *p) gets_(b, T_KEY1, h_job, sizeof(h_job)); gets_(b, T_BIZDATE, h_date, sizeof(h_date)); - if (h_job[0] == 0) FAIL(b); + if (h_job[0] == 0) return -1; if (h_date[0] == 0) { if (cmdb_get_param("BIZ_DATE", h_date) < 0) strcpy(h_date, "2026-07-19"); } EXEC SQL SELECT count(*), @@ -25,12 +29,139 @@ void CM_BATHIST_CNT(TPSVCINFO *p) coalesce(max(log_id), 0) INTO :h_total, :h_err, :h_warn, :h_lastid FROM cm_log WHERE svc_name = :h_job AND biz_date = :h_date; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; setl(b, T_COUNT, h_total); setl(b, T_ARG1, h_err); setl(b, T_ARG2, h_warn); setl(b, T_ARG3, h_total - h_err - h_warn); /* 정상 건수 */ setl(b, T_ID1, h_lastid); + return 0; + return 0; +} + +/* CM_BATHIST_CNT 심화용 보조 판정 - 처리등급 산정. */ +static int CM_BATHIST_CNT__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 20L) return 1; /* 소량 */ + if (v < 1090L) return 2; /* 보통 */ + if (v < 101090L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_BATHIST_CNT - * CM_BATHIST_CNT.pgc - cm 모듈 서비스 CM_BATHIST_CNT (one service. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_BATHIST_CNT(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_BATHIST_CNT 진입: * CM_BATHIST_CNT.pgc - cm 모듈 서비스 CM_BATHIST_CNT (one service (svc=CM_BATHIST_CNT)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_BATHIST_CNT 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 15L) + userlog("CM_BATHIST_CNT 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (policy < 0) { + grade = 0; userlog("CM_BATHIST_CNT 기준정보 이상 - 보류"); + } else if (policy == 0) { + grade = 1; /* 최초 처리 */ + } else if (policy < 190L) { + grade = 2; /* 통상 처리 */ + } else { + grade = 3; userlog("CM_BATHIST_CNT 누적 과다 policy=%ld", policy); + } + if (grade >= 3 && band > 110900L) { + userlog("CM_BATHIST_CNT 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_BATHIST_CNT__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_BATHIST_CNT 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_BATHIST_CNT__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_BATHIST_CNT 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_BATHIST_CNT", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_BATHIST_CNT'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1090L) + userlog("CM_BATHIST_CNT 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_BATHIST_CNT 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_BATHIST_CNT 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_BATLAST.pgc b/app/src/cm/svc/CM_BATLAST.pgc index 81f312f..821fc3c 100644 --- a/app/src/cm/svc/CM_BATLAST.pgc +++ b/app/src/cm/svc/CM_BATLAST.pgc @@ -6,7 +6,11 @@ */ #include "CM_BATLAST.h" -void CM_BATLAST(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_BATLAST__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char job[32], lastmsg[256]; @@ -16,16 +20,16 @@ void CM_BATLAST(TPSVCINFO *p) EXEC SQL END DECLARE SECTION; gets_(b, T_KEY1, job, sizeof(job)); - if (job[0] == 0) FAIL(b); + if (job[0] == 0) return -1; snprintf(h_pat, sizeof(h_pat), "%s %%", job); EXEC SQL SELECT coalesce(max(log_id), 0) INTO :h_lastid FROM cm_log WHERE svc_name = 'BATRUN' AND msg LIKE :h_pat; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_lastid == 0) { setl(b, T_RC, 1); FAIL(b); } /* 실행 이력 없음 */ + if (sqlca.sqlcode < 0) return -1; + if (h_lastid == 0) { setl(b, T_RC, 1); return -1; } /* 실행 이력 없음 */ EXEC SQL SELECT biz_date::text INTO :h_lastdate FROM cm_log WHERE log_id = :h_lastid; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; lastmsg[0] = 0; cmdb_log_message(h_lastid, lastmsg); @@ -33,5 +37,128 @@ void CM_BATLAST(TPSVCINFO *p) setl(b, T_ID1, h_lastid); sets_(b, T_MSG, lastmsg); setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_BATLAST 심화용 보조 판정 - 처리등급 산정. */ +static int CM_BATLAST__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 12L) return 1; /* 소량 */ + if (v < 1262L) return 2; /* 보통 */ + if (v < 100762L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_BATLAST - * CM_BATLAST.pgc - cm 모듈 서비스 CM_BATLAST (one service = one f. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_BATLAST(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_BATLAST 진입: * CM_BATLAST.pgc - cm 모듈 서비스 CM_BATLAST (one service = one f (svc=CM_BATLAST)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_BATLAST 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 7L) + userlog("CM_BATLAST 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (policy < 0) { + grade = 0; userlog("CM_BATLAST 기준정보 이상 - 보류"); + } else if (policy == 0) { + grade = 1; /* 최초 처리 */ + } else if (policy < 162L) { + grade = 2; /* 통상 처리 */ + } else { + grade = 3; userlog("CM_BATLAST 누적 과다 policy=%ld", policy); + } + if (grade >= 3 && band > 107620L) { + userlog("CM_BATLAST 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_BATLAST__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_BATLAST 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_BATLAST__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_BATLAST 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_BATLAST", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_BATLAST'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1262L) + userlog("CM_BATLAST 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_BATLAST 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_BATLAST 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_BATRERUN_REQ.pgc b/app/src/cm/svc/CM_BATRERUN_REQ.pgc index 019789a..fc4a4d9 100644 --- a/app/src/cm/svc/CM_BATRERUN_REQ.pgc +++ b/app/src/cm/svc/CM_BATRERUN_REQ.pgc @@ -6,7 +6,11 @@ */ #include "CM_BATRERUN_REQ.h" -void CM_BATRERUN_REQ(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_BATRERUN_REQ__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char reason[96], ticket[224]; @@ -19,21 +23,144 @@ void CM_BATRERUN_REQ(TPSVCINFO *p) gets_(b, T_KEY1, h_job, sizeof(h_job)); gets_(b, T_BIZDATE, h_date, sizeof(h_date)); gets_(b, T_REASON, reason, sizeof(reason)); - if (h_job[0] == 0 || h_date[0] == 0) FAIL(b); + if (h_job[0] == 0 || h_date[0] == 0) return -1; if (reason[0] == 0) strcpy(reason, "MANUAL"); EXEC SQL SELECT count(*) INTO :h_ran FROM cm_log WHERE svc_name = :h_job AND biz_date = :h_date; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_ran == 0) { setl(b, T_RC, 1); FAIL(b); } /* 실행 이력 없음 */ + if (sqlca.sqlcode < 0) return -1; + if (h_ran == 0) { setl(b, T_RC, 1); return -1; } /* 실행 이력 없음 */ reqno = cmdb_next_seq("RERUN"); - if (reqno < 0) FAIL(b); + if (reqno < 0) return -1; snprintf(ticket, sizeof(ticket), "#%ld %s %s (%s)", reqno, h_job, h_date, reason); - if (cmdb_log("RERUN", "W", ticket, h_date) < 0) FAIL(b); + if (cmdb_log("RERUN", "W", ticket, h_date) < 0) return -1; setl(b, T_ID1, reqno); /* 재실행 요청번호 */ setl(b, T_COUNT, h_ran); /* 당일 기존 실행 수 */ setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_BATRERUN_REQ 심화용 보조 판정 - 처리등급 산정. */ +static int CM_BATRERUN_REQ__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 27L) return 1; /* 소량 */ + if (v < 1177L) return 2; /* 보통 */ + if (v < 101177L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_BATRERUN_REQ - * CM_BATRERUN_REQ.pgc - cm 모듈 서비스 CM_BATRERUN_REQ (one servi. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_BATRERUN_REQ(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_BATRERUN_REQ 진입: * CM_BATRERUN_REQ.pgc - cm 모듈 서비스 CM_BATRERUN_REQ (one servi (svc=CM_BATRERUN_REQ)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_BATRERUN_REQ 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 22L) + userlog("CM_BATRERUN_REQ 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (policy < 0) { + grade = 0; userlog("CM_BATRERUN_REQ 기준정보 이상 - 보류"); + } else if (policy == 0) { + grade = 1; /* 최초 처리 */ + } else if (policy < 177L) { + grade = 2; /* 통상 처리 */ + } else { + grade = 3; userlog("CM_BATRERUN_REQ 누적 과다 policy=%ld", policy); + } + if (grade >= 3 && band > 111770L) { + userlog("CM_BATRERUN_REQ 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_BATRERUN_REQ__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_BATRERUN_REQ 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_BATRERUN_REQ__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_BATRERUN_REQ 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_BATRERUN_REQ", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_BATRERUN_REQ'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1177L) + userlog("CM_BATRERUN_REQ 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_BATRERUN_REQ 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_BATRERUN_REQ 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_BATSCHD_DEL.pgc b/app/src/cm/svc/CM_BATSCHD_DEL.pgc index 32ef6a8..1e87d7a 100644 --- a/app/src/cm/svc/CM_BATSCHD_DEL.pgc +++ b/app/src/cm/svc/CM_BATSCHD_DEL.pgc @@ -6,7 +6,11 @@ */ #include "CM_BATSCHD_DEL.h" -void CM_BATSCHD_DEL(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_BATSCHD_DEL__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char job[32], chk[128], bizdate[16], hist[96]; @@ -15,14 +19,14 @@ void CM_BATSCHD_DEL(TPSVCINFO *p) EXEC SQL END DECLARE SECTION; gets_(b, T_KEY1, job, sizeof(job)); - if (job[0] == 0) FAIL(b); + if (job[0] == 0) return -1; snprintf(h_schd, sizeof(h_schd), "SCHD:%s", job); snprintf(h_dep, sizeof(h_dep), "DEP:%s", job); - if (cmdb_get_param(h_schd, chk) < 0) { setl(b, T_RC, 1); FAIL(b); } /* 미등록 */ + if (cmdb_get_param(h_schd, chk) < 0) { setl(b, T_RC, 1); return -1; } /* 미등록 */ EXEC SQL DELETE FROM cm_param WHERE param_key IN (:h_schd, :h_dep); - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); if (bizdate[0] == 0) { if (cmdb_get_param("BIZ_DATE", bizdate) < 0) strcpy(bizdate, "2026-07-19"); } @@ -30,5 +34,128 @@ void CM_BATSCHD_DEL(TPSVCINFO *p) cmdb_log("BATSCHD", "I", hist, bizdate); setl(b, T_COUNT, sqlca.sqlerrd[2]); /* 삭제된 파라미터 수 */ setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_BATSCHD_DEL 심화용 보조 판정 - 처리등급 산정. */ +static int CM_BATSCHD_DEL__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 22L) return 1; /* 소량 */ + if (v < 1052L) return 2; /* 보통 */ + if (v < 101052L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_BATSCHD_DEL - * CM_BATSCHD_DEL.pgc - cm 모듈 서비스 CM_BATSCHD_DEL (one service. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_BATSCHD_DEL(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_BATSCHD_DEL 진입: * CM_BATSCHD_DEL.pgc - cm 모듈 서비스 CM_BATSCHD_DEL (one service (svc=CM_BATSCHD_DEL)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_BATSCHD_DEL 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 17L) + userlog("CM_BATSCHD_DEL 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (policy < 0) { + grade = 0; userlog("CM_BATSCHD_DEL 기준정보 이상 - 보류"); + } else if (policy == 0) { + grade = 1; /* 최초 처리 */ + } else if (policy < 152L) { + grade = 2; /* 통상 처리 */ + } else { + grade = 3; userlog("CM_BATSCHD_DEL 누적 과다 policy=%ld", policy); + } + if (grade >= 3 && band > 110520L) { + userlog("CM_BATSCHD_DEL 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_BATSCHD_DEL__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_BATSCHD_DEL 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_BATSCHD_DEL__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_BATSCHD_DEL 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_BATSCHD_DEL", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_BATSCHD_DEL'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1052L) + userlog("CM_BATSCHD_DEL 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_BATSCHD_DEL 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_BATSCHD_DEL 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_BATSCHD_GET.pgc b/app/src/cm/svc/CM_BATSCHD_GET.pgc index 82080ec..df32067 100644 --- a/app/src/cm/svc/CM_BATSCHD_GET.pgc +++ b/app/src/cm/svc/CM_BATSCHD_GET.pgc @@ -6,22 +6,149 @@ */ #include "CM_BATSCHD_GET.h" -void CM_BATSCHD_GET(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_BATSCHD_GET__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char job[32], key[48], stored[128], cycle[16]; long hhmm = -1; gets_(b, T_KEY1, job, sizeof(job)); - if (job[0] == 0) FAIL(b); + if (job[0] == 0) return -1; snprintf(key, sizeof(key), "SCHD:%s", job); - if (cmdb_get_param(key, stored) < 0) { setl(b, T_RC, 1); FAIL(b); } /* 미등록 */ + if (cmdb_get_param(key, stored) < 0) { setl(b, T_RC, 1); return -1; } /* 미등록 */ if (sscanf(stored, "%15s %ld", cycle, &hhmm) != 2 || hhmm < 0 || hhmm > 2359) - { setl(b, T_RC, 2); FAIL(b); } /* 형식 오류 */ + { setl(b, T_RC, 2); return -1; } /* 형식 오류 */ sets_(b, T_STR4, cycle); setl(b, T_ARG1, hhmm); setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_BATSCHD_GET 심화용 보조 판정 - 처리등급 산정. */ +static int CM_BATSCHD_GET__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 33L) return 1; /* 소량 */ + if (v < 1063L) return 2; /* 보통 */ + if (v < 101063L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_BATSCHD_GET - 공통 처리. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_BATSCHD_GET(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_BATSCHD_GET 진입: 공통 처리 (svc=CM_BATSCHD_GET)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_BATSCHD_GET 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 8L) + userlog("CM_BATSCHD_GET 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (policy < 0) { + grade = 0; userlog("CM_BATSCHD_GET 기준정보 이상 - 보류"); + } else if (policy == 0) { + grade = 1; /* 최초 처리 */ + } else if (policy < 163L) { + grade = 2; /* 통상 처리 */ + } else { + grade = 3; userlog("CM_BATSCHD_GET 누적 과다 policy=%ld", policy); + } + if (grade >= 3 && band > 110630L) { + userlog("CM_BATSCHD_GET 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_BATSCHD_GET__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_BATSCHD_GET 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_BATSCHD_GET__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_BATSCHD_GET 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_BATSCHD_GET", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_BATSCHD_GET'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1063L) + userlog("CM_BATSCHD_GET 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_BATSCHD_GET 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_BATSCHD_GET 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_BATSCHD_LIST.pgc b/app/src/cm/svc/CM_BATSCHD_LIST.pgc index d022022..295aed1 100644 --- a/app/src/cm/svc/CM_BATSCHD_LIST.pgc +++ b/app/src/cm/svc/CM_BATSCHD_LIST.pgc @@ -6,7 +6,11 @@ */ #include "CM_BATSCHD_LIST.h" -void CM_BATSCHD_LIST(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_BATSCHD_LIST__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; long daily = 0, weekly = 0, monthly = 0, broken = 0, total = 0; @@ -17,12 +21,12 @@ void CM_BATSCHD_LIST(TPSVCINFO *p) EXEC SQL DECLARE cur_schd CURSOR FOR SELECT param_val FROM cm_param WHERE param_key LIKE 'SCHD:%'; EXEC SQL OPEN cur_schd; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; for (;;) { EXEC SQL FETCH cur_schd INTO :h_val; if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_schd; FAIL(b); } + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_schd; return -1; } total++; if (strncmp(h_val, "DAILY", 5) == 0) daily++; else if (strncmp(h_val, "WEEKLY", 6) == 0) weekly++; @@ -36,5 +40,132 @@ void CM_BATSCHD_LIST(TPSVCINFO *p) setl(b, T_ARG2, weekly); setl(b, T_ARG3, monthly); setl(b, T_ARG4, broken); + return 0; + return 0; +} + +/* CM_BATSCHD_LIST 심화용 보조 판정 - 처리등급 산정. */ +static int CM_BATSCHD_LIST__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 45L) return 1; /* 소량 */ + if (v < 1155L) return 2; /* 보통 */ + if (v < 101155L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_BATSCHD_LIST - * CM_BATSCHD_LIST.pgc - cm 모듈 서비스 CM_BATSCHD_LIST (one servi. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_BATSCHD_LIST(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_BATSCHD_LIST 진입: * CM_BATSCHD_LIST.pgc - cm 모듈 서비스 CM_BATSCHD_LIST (one servi (svc=CM_BATSCHD_LIST)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_BATSCHD_LIST 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 20L) + userlog("CM_BATSCHD_LIST 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (policy < 0) { + grade = 0; userlog("CM_BATSCHD_LIST 기준정보 이상 - 보류"); + } else if (policy == 0) { + grade = 1; /* 최초 처리 */ + } else if (policy < 155L) { + grade = 2; /* 통상 처리 */ + } else { + grade = 3; userlog("CM_BATSCHD_LIST 누적 과다 policy=%ld", policy); + } + if (grade >= 3 && band > 111550L) { + userlog("CM_BATSCHD_LIST 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_BATSCHD_LIST__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_BATSCHD_LIST 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_BATSCHD_LIST__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_BATSCHD_LIST 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_BATSCHD_LIST", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_BATSCHD_LIST'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1155L) + userlog("CM_BATSCHD_LIST 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_BATSCHD_LIST 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_BATSCHD_LIST 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_BATSCHD_REG.pgc b/app/src/cm/svc/CM_BATSCHD_REG.pgc index c187ba5..577889e 100644 --- a/app/src/cm/svc/CM_BATSCHD_REG.pgc +++ b/app/src/cm/svc/CM_BATSCHD_REG.pgc @@ -6,7 +6,11 @@ */ #include "CM_BATSCHD_REG.h" -void CM_BATSCHD_REG(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_BATSCHD_REG__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char job[32], cycle[12], key[48], val[32], bizdate[16], hist[128]; @@ -16,21 +20,144 @@ void CM_BATSCHD_REG(TPSVCINFO *p) gets_(b, T_KEY1, job, sizeof(job)); gets_(b, T_STR1, cycle, sizeof(cycle)); hhmm = getl(b, T_ARG1); - if (job[0] == 0) FAIL(b); + if (job[0] == 0) return -1; if (strcmp(cycle, "DAILY") == 0) cyc_ok = 1; else if (strcmp(cycle, "WEEKLY") == 0) cyc_ok = 1; else if (strcmp(cycle, "MONTHLY") == 0) cyc_ok = 1; - if (!cyc_ok) { setl(b, T_RC, 1); FAIL(b); } /* 주기 오류 */ - if (hhmm < 0 || hhmm > 2359 || (hhmm % 100) > 59) { setl(b, T_RC, 2); FAIL(b); } + if (!cyc_ok) { setl(b, T_RC, 1); return -1; } /* 주기 오류 */ + if (hhmm < 0 || hhmm > 2359 || (hhmm % 100) > 59) { setl(b, T_RC, 2); return -1; } snprintf(key, sizeof(key), "SCHD:%s", job); snprintf(val, sizeof(val), "%s %04ld", cycle, hhmm); - if (cmdb_set_param(key, val) < 0) FAIL(b); + if (cmdb_set_param(key, val) < 0) return -1; gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); if (bizdate[0] == 0) { if (cmdb_get_param("BIZ_DATE", bizdate) < 0) strcpy(bizdate, "2026-07-19"); } snprintf(hist, sizeof(hist), "SCHD %s = %s", job, val); cmdb_log("BATSCHD", "I", hist, bizdate); setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_BATSCHD_REG 심화용 보조 판정 - 처리등급 산정. */ +static int CM_BATSCHD_REG__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 31L) return 1; /* 소량 */ + if (v < 1061L) return 2; /* 보통 */ + if (v < 101061L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_BATSCHD_REG - 공통 처리. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_BATSCHD_REG(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_BATSCHD_REG 진입: 공통 처리 (svc=CM_BATSCHD_REG)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_BATSCHD_REG 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 6L) + userlog("CM_BATSCHD_REG 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (policy < 0) { + grade = 0; userlog("CM_BATSCHD_REG 기준정보 이상 - 보류"); + } else if (policy == 0) { + grade = 1; /* 최초 처리 */ + } else if (policy < 161L) { + grade = 2; /* 통상 처리 */ + } else { + grade = 3; userlog("CM_BATSCHD_REG 누적 과다 policy=%ld", policy); + } + if (grade >= 3 && band > 110610L) { + userlog("CM_BATSCHD_REG 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_BATSCHD_REG__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_BATSCHD_REG 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_BATSCHD_REG__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_BATSCHD_REG 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_BATSCHD_REG", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_BATSCHD_REG'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1061L) + userlog("CM_BATSCHD_REG 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_BATSCHD_REG 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_BATSCHD_REG 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_BIZDAY.pgc b/app/src/cm/svc/CM_BIZDAY.pgc index fb4992f..228b713 100644 --- a/app/src/cm/svc/CM_BIZDAY.pgc +++ b/app/src/cm/svc/CM_BIZDAY.pgc @@ -6,11 +6,141 @@ #include "CM_BIZDAY.h" /* 3. CM_BIZDAY - 현재 영업일 조회 (cm_param BIZ_DATE). */ -void CM_BIZDAY(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_BIZDAY__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char val[128]; - if (cmdb_get_param("BIZ_DATE", val) < 0) FAIL(b); + if (cmdb_get_param("BIZ_DATE", val) < 0) return -1; sets_(b, T_BIZDATE, val); + return 0; + return 0; +} + +/* CM_BIZDAY 심화용 보조 판정 - 처리등급 산정. */ +static int CM_BIZDAY__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 20L) return 1; /* 소량 */ + if (v < 1190L) return 2; /* 보통 */ + if (v < 100690L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_BIZDAY - 현재 영업일 조회 (cm_param BIZ_DATE).. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_BIZDAY(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_BIZDAY 진입: 현재 영업일 조회 (cm_param BIZ_DATE). (svc=CM_BIZDAY)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + rc = cmdb_is_holiday(hz_bizdate); + if (rc < 0) { userlog("CM_BIZDAY 휴일 조회 오류"); } + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_holiday + WHERE holi_date >= :hz_bizdate; + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 잔여 휴일수 */ + if (rc == 1) { userlog("CM_BIZDAY 기준일이 휴일 bizdate=%s", hz_bizdate); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (policy < 0) { + grade = 0; userlog("CM_BIZDAY 기준정보 이상 - 보류"); + } else if (policy == 0) { + grade = 1; /* 최초 처리 */ + } else if (policy < 190L) { + grade = 2; /* 통상 처리 */ + } else { + grade = 3; userlog("CM_BIZDAY 누적 과다 policy=%ld", policy); + } + if (grade >= 3 && band > 106900L) { + userlog("CM_BIZDAY 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_BIZDAY__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_BIZDAY 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_BIZDAY__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_BIZDAY 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_BIZDAY", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_holiday + WHERE holi_date BETWEEN :hz_bizdate AND '2026-12-31'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 연말까지 휴일수 */ + if (agg > 1190L) + userlog("CM_BIZDAY 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_BIZDAY 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_BIZDAY 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_BIZDAY_ADD.pgc b/app/src/cm/svc/CM_BIZDAY_ADD.pgc index aa16dcf..00f0f30 100644 --- a/app/src/cm/svc/CM_BIZDAY_ADD.pgc +++ b/app/src/cm/svc/CM_BIZDAY_ADD.pgc @@ -16,7 +16,11 @@ static void cm_tomorrow(int *y, int *m, int *d) if (++(*d) > last) { *d = 1; if (++(*m) > 12) { *m = 1; (*y)++; } } } -void CM_BIZDAY_ADD(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_BIZDAY_ADD__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char cur[16], iso[16]; @@ -27,8 +31,8 @@ void CM_BIZDAY_ADD(TPSVCINFO *p) gets_(b, T_BIZDATE, cur, sizeof(cur)); n = getl(b, T_ARG1); if (cur[0] == 0) { if (cmdb_get_param("BIZ_DATE", cur) < 0) strcpy(cur, "2026-07-19"); } - if (sscanf(cur, "%d-%d-%d", &y, &m, &d) != 3) FAIL(b); - if (n < 0 || n > 365) { setl(b, T_RC, 1); FAIL(b); } + if (sscanf(cur, "%d-%d-%d", &y, &m, &d) != 3) return -1; + if (n < 0 || n > 365) { setl(b, T_RC, 1); return -1; } /* Zeller 없이: 임의 기준(1=일요일) dow 는 cmdb 휴일과 요일 병행 점검 */ while (moved < n) { @@ -44,5 +48,127 @@ void CM_BIZDAY_ADD(TPSVCINFO *p) sets_(b, T_STR4, iso); /* N영업일 뒤 날짜 */ setl(b, T_ARG2, skipped); /* 건너뛴 비영업일 */ setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_BIZDAY_ADD 심화용 보조 판정 - 처리등급 산정. */ +static int CM_BIZDAY_ADD__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 36L) return 1; /* 소량 */ + if (v < 1486L) return 2; /* 보통 */ + if (v < 100986L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_BIZDAY_ADD - * CM_BIZDAY_ADD.pgc - cm 모듈 서비스 CM_BIZDAY_ADD (one service =. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_BIZDAY_ADD(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_BIZDAY_ADD 진입: * CM_BIZDAY_ADD.pgc - cm 모듈 서비스 CM_BIZDAY_ADD (one service = (svc=CM_BIZDAY_ADD)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + rc = cmdb_is_holiday(hz_bizdate); + if (rc < 0) { userlog("CM_BIZDAY_ADD 휴일 조회 오류"); } + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_holiday + WHERE holi_date >= :hz_bizdate; + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 잔여 휴일수 */ + if (rc == 1) { userlog("CM_BIZDAY_ADD 기준일이 휴일 bizdate=%s", hz_bizdate); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (policy < 0) { + grade = 0; userlog("CM_BIZDAY_ADD 기준정보 이상 - 보류"); + } else if (policy == 0) { + grade = 1; /* 최초 처리 */ + } else if (policy < 186L) { + grade = 2; /* 통상 처리 */ + } else { + grade = 3; userlog("CM_BIZDAY_ADD 누적 과다 policy=%ld", policy); + } + if (grade >= 3 && band > 109860L) { + userlog("CM_BIZDAY_ADD 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_BIZDAY_ADD__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_BIZDAY_ADD 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_BIZDAY_ADD__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_BIZDAY_ADD 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_BIZDAY_ADD", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_holiday + WHERE holi_date BETWEEN :hz_bizdate AND '2026-12-31'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 연말까지 휴일수 */ + if (agg > 1486L) + userlog("CM_BIZDAY_ADD 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_BIZDAY_ADD 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_BIZDAY_ADD 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_BIZDAY_CNT.pgc b/app/src/cm/svc/CM_BIZDAY_CNT.pgc index 27b4982..3512806 100644 --- a/app/src/cm/svc/CM_BIZDAY_CNT.pgc +++ b/app/src/cm/svc/CM_BIZDAY_CNT.pgc @@ -23,7 +23,11 @@ static void cm_step_day(int *y, int *m, int *d) if (++*d > lim) { *d = 1; if (++*m > 12) { *m = 1; ++*y; } } } -void CM_BIZDAY_CNT(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_BIZDAY_CNT__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char from[16], to[16], cur[16]; @@ -32,18 +36,18 @@ void CM_BIZDAY_CNT(TPSVCINFO *p) gets_(b, T_STR1, from, sizeof(from)); gets_(b, T_STR2, to, sizeof(to)); - if (sscanf(from, "%d-%d-%d", &y, &m, &d) != 3) FAIL(b); - if (to[0] == 0 || strcmp(from, to) > 0) FAIL(b); + if (sscanf(from, "%d-%d-%d", &y, &m, &d) != 3) return -1; + if (to[0] == 0 || strcmp(from, to) > 0) return -1; sprintf(cur, "%04d-%02d-%02d", y, m, d); while (strcmp(cur, to) <= 0) { - if (++guard > 400) { setl(b, T_RC, 2); FAIL(b); } /* 구간 과대 */ + if (++guard > 400) { setl(b, T_RC, 2); return -1; } /* 구간 과대 */ dow = cm_dow_of(y, m, d); if (dow == 0 || dow == 6) { weekend++; } else { hol = cmdb_is_holiday(cur); - if (hol < 0) FAIL(b); + if (hol < 0) return -1; if (hol == 1) holidays++; else biz++; } cm_step_day(&y, &m, &d); @@ -52,5 +56,127 @@ void CM_BIZDAY_CNT(TPSVCINFO *p) setl(b, T_COUNT, biz); setl(b, T_ARG1, weekend); setl(b, T_ARG2, holidays); + return 0; + return 0; +} + +/* CM_BIZDAY_CNT 심화용 보조 판정 - 처리등급 산정. */ +static int CM_BIZDAY_CNT__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 24L) return 1; /* 소량 */ + if (v < 1014L) return 2; /* 보통 */ + if (v < 101014L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_BIZDAY_CNT - * CM_BIZDAY_CNT.pgc - cm 모듈 서비스 CM_BIZDAY_CNT (one service =. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_BIZDAY_CNT(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_BIZDAY_CNT 진입: * CM_BIZDAY_CNT.pgc - cm 모듈 서비스 CM_BIZDAY_CNT (one service = (svc=CM_BIZDAY_CNT)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + rc = cmdb_is_holiday(hz_bizdate); + if (rc < 0) { userlog("CM_BIZDAY_CNT 휴일 조회 오류"); } + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_holiday + WHERE holi_date >= :hz_bizdate; + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 잔여 휴일수 */ + if (rc == 1) { userlog("CM_BIZDAY_CNT 기준일이 휴일 bizdate=%s", hz_bizdate); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (policy < 0) { + grade = 0; userlog("CM_BIZDAY_CNT 기준정보 이상 - 보류"); + } else if (policy == 0) { + grade = 1; /* 최초 처리 */ + } else if (policy < 114L) { + grade = 2; /* 통상 처리 */ + } else { + grade = 3; userlog("CM_BIZDAY_CNT 누적 과다 policy=%ld", policy); + } + if (grade >= 3 && band > 110140L) { + userlog("CM_BIZDAY_CNT 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_BIZDAY_CNT__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_BIZDAY_CNT 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_BIZDAY_CNT__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_BIZDAY_CNT 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_BIZDAY_CNT", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_holiday + WHERE holi_date BETWEEN :hz_bizdate AND '2026-12-31'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 연말까지 휴일수 */ + if (agg > 1014L) + userlog("CM_BIZDAY_CNT 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_BIZDAY_CNT 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_BIZDAY_CNT 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_CACHE_INV.pgc b/app/src/cm/svc/CM_CACHE_INV.pgc index a69c159..6ed29ee 100644 --- a/app/src/cm/svc/CM_CACHE_INV.pgc +++ b/app/src/cm/svc/CM_CACHE_INV.pgc @@ -6,7 +6,11 @@ #include "CM_CACHE_INV.h" /* 21. CM_CACHE_INV - 캐시 무효화 (순수 처리: 버전 증가 표시). */ -void CM_CACHE_INV(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_CACHE_INV__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char grp[32]; @@ -14,5 +18,128 @@ void CM_CACHE_INV(TPSVCINFO *p) setl(b, T_RC, 0); sets_(b, T_MSG, "캐시 무효화 완료"); userlog("CM_CACHE_INV grp=%s", grp); + return 0; + return 0; +} + +/* CM_CACHE_INV 심화용 보조 판정 - 처리등급 산정. */ +static int CM_CACHE_INV__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 41L) return 1; /* 소량 */ + if (v < 1411L) return 2; /* 보통 */ + if (v < 100911L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_CACHE_INV - 캐시 무효화 (순수 처리: 버전 증가 표시).. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_CACHE_INV(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_CACHE_INV 진입: 캐시 무효화 (순수 처리: 버전 증가 표시). (svc=CM_CACHE_INV)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_CACHE_INV 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 16L) + userlog("CM_CACHE_INV 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 14L) { + grade = 1; /* 저빈도 */ + } else if (policy < 81L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_CACHE_INV 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 109110L) { + userlog("CM_CACHE_INV 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_CACHE_INV__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_CACHE_INV 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_CACHE_INV__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_CACHE_INV 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_CACHE_INV", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_CACHE_INV'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1411L) + userlog("CM_CACHE_INV 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_CACHE_INV 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_CACHE_INV 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_CACHE_LOAD.pgc b/app/src/cm/svc/CM_CACHE_LOAD.pgc index 35936f7..13fe7ff 100644 --- a/app/src/cm/svc/CM_CACHE_LOAD.pgc +++ b/app/src/cm/svc/CM_CACHE_LOAD.pgc @@ -6,15 +6,146 @@ #include "CM_CACHE_LOAD.h" /* 20. CM_CACHE_LOAD - 코드 그룹 캐시 로드 (건수 조회로 로딩 시뮬레이션). */ -void CM_CACHE_LOAD(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_CACHE_LOAD__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char grp[32]; long cnt; gets_(b, T_STR1, grp, sizeof(grp)); if (grp[0] == 0) strcpy(grp, "CCY"); cnt = cmdb_count_codes(grp); - if (cnt < 0) FAIL(b); + if (cnt < 0) return -1; setl(b, T_COUNT, cnt); userlog("CM_CACHE_LOAD grp=%s codes=%ld", grp, cnt); + return 0; + return 0; +} + +/* CM_CACHE_LOAD 심화용 보조 판정 - 처리등급 산정. */ +static int CM_CACHE_LOAD__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 12L) return 1; /* 소량 */ + if (v < 1462L) return 2; /* 보통 */ + if (v < 100962L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_CACHE_LOAD - 코드 그룹 캐시 로드 (건수 조회로 로딩 시뮬레이션).. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_CACHE_LOAD(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_CACHE_LOAD 진입: 코드 그룹 캐시 로드 (건수 조회로 로딩 시뮬레이션). (svc=CM_CACHE_LOAD)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_CACHE_LOAD 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 7L) + userlog("CM_CACHE_LOAD 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 5L) { + grade = 1; /* 저빈도 */ + } else if (policy < 52L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_CACHE_LOAD 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 109620L) { + userlog("CM_CACHE_LOAD 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_CACHE_LOAD__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_CACHE_LOAD 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_CACHE_LOAD__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_CACHE_LOAD 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_CACHE_LOAD", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_CACHE_LOAD'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1462L) + userlog("CM_CACHE_LOAD 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_CACHE_LOAD 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_CACHE_LOAD 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_CHECKDIGIT.pgc b/app/src/cm/svc/CM_CHECKDIGIT.pgc index b701906..08b3766 100644 --- a/app/src/cm/svc/CM_CHECKDIGIT.pgc +++ b/app/src/cm/svc/CM_CHECKDIGIT.pgc @@ -6,22 +6,152 @@ #include "CM_CHECKDIGIT.h" /* 25. CM_CHECKDIGIT - Luhn 체크디지트 생성 (순수 계산). */ -void CM_CHECKDIGIT(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_CHECKDIGIT__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char base[32], cand[34]; int d, found = -1, n; gets_(b, T_STR2, base, sizeof(base)); n = (int)strlen(base); - if (n == 0 || n >= (int)sizeof(cand) - 1) FAIL(b); + if (n == 0 || n >= (int)sizeof(cand) - 1) return -1; for (d = 0; d <= 9; d++) { strcpy(cand, base); cand[n] = (char)('0' + d); cand[n + 1] = 0; if (acq_luhn_ok(cand)) { found = d; break; } } - if (found < 0) FAIL(b); + if (found < 0) return -1; setl(b, T_ARG1, found); sets_(b, T_STR4, cand); /* 체크디지트 포함 완성번호 */ + return 0; + return 0; +} + +/* CM_CHECKDIGIT 심화용 보조 판정 - 처리등급 산정. */ +static int CM_CHECKDIGIT__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 48L) return 1; /* 소량 */ + if (v < 1458L) return 2; /* 보통 */ + if (v < 100958L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_CHECKDIGIT - Luhn 체크디지트 생성 (순수 계산).. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_CHECKDIGIT(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_CHECKDIGIT 진입: Luhn 체크디지트 생성 (순수 계산). (svc=CM_CHECKDIGIT)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + if (cmdb_get_param("VAT_RATE", hz_pval) == 0) policy = atol(hz_pval); + else policy = 10; /* 기본 부가세율 */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log + WHERE svc_name = 'CM_CHECKDIGIT' AND biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_CHECKDIGIT 처리이력 조회 오류"); } + else { policy += (hz_cnt % 7); } /* 반복호출 보정 */ + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 13L) { + grade = 1; /* 저빈도 */ + } else if (policy < 128L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_CHECKDIGIT 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 109580L) { + userlog("CM_CHECKDIGIT 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_CHECKDIGIT__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_CHECKDIGIT 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_CHECKDIGIT__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_CHECKDIGIT 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_CHECKDIGIT", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_CHECKDIGIT'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1458L) + userlog("CM_CHECKDIGIT 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_CHECKDIGIT 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_CHECKDIGIT 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_CODE.pgc b/app/src/cm/svc/CM_CODE.pgc index dd12a33..eb2e644 100644 --- a/app/src/cm/svc/CM_CODE.pgc +++ b/app/src/cm/svc/CM_CODE.pgc @@ -6,7 +6,11 @@ #include "CM_CODE.h" /* ========================================================================= */ -void CM_CODE(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_CODE__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char grp[32], code[32]; @@ -14,7 +18,129 @@ void CM_CODE(TPSVCINFO *p) gets_(b, T_STR1, grp, sizeof(grp)); if (grp[0] == 0) strcpy(grp, "CCY"); gets_(b, T_STR2, code, sizeof(code)); ex = cmdb_code_exists(grp, code); - if (ex < 0) FAIL(b); + if (ex < 0) return -1; setl(b, T_RC, ex); + return 0; + return 0; +} + +/* CM_CODE 심화용 보조 판정 - 처리등급 산정. */ +static int CM_CODE__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 12L) return 1; /* 소량 */ + if (v < 1022L) return 2; /* 보통 */ + if (v < 100522L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_CODE - 공통 처리. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_CODE(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_CODE 진입: 공통 처리 (svc=CM_CODE)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_STR1, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "CCY"); + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_code + WHERE code_grp = :hz_key AND use_yn = 'Y'; + if (sqlca.sqlcode < 0) { userlog("CM_CODE 코드그룹 조회 오류"); } + else if (hz_cnt == 0) { userlog("CM_CODE 미등록 코드그룹 key=%s", hz_key); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + gets_(b, T_STATUS, ins, sizeof(ins)); + if (ins[0] == 'C' || ins[0] == 'X') { + grade = 0; userlog("CM_CODE 폐기/취소 상태 - 갱신 금지"); + } else if (policy == 0) { + grade = 1; userlog("CM_CODE 신규 등록 경로"); + } else if (policy < 7L) { + grade = 2; /* 소규모 그룹 - 단순 반영 */ + } else { + grade = 3; /* 대규모 그룹 - 검증 강화 */ + } + if (grade >= 3 && band > 105220L) { + userlog("CM_CODE 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_CODE__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_CODE 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_CODE__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_CODE 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_CODE", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(DISTINCT code_grp) INTO :hz_v1 FROM cm_code; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 코드그룹 수 */ + if (agg > 1022L) + userlog("CM_CODE 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_CODE 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_CODE 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_CODEGRP_CLOSE.pgc b/app/src/cm/svc/CM_CODEGRP_CLOSE.pgc index 710658a..f7bbcab 100644 --- a/app/src/cm/svc/CM_CODEGRP_CLOSE.pgc +++ b/app/src/cm/svc/CM_CODEGRP_CLOSE.pgc @@ -6,7 +6,11 @@ */ #include "CM_CODEGRP_CLOSE.h" -void CM_CODEGRP_CLOSE(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_CODEGRP_CLOSE__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char hist[160], reason[80], bizdate[16]; @@ -17,18 +21,140 @@ void CM_CODEGRP_CLOSE(TPSVCINFO *p) gets_(b, T_KEY1, h_grp, sizeof(h_grp)); gets_(b, T_KEY2, h_code, sizeof(h_code)); gets_(b, T_REASON, reason, sizeof(reason)); - if (h_grp[0] == 0 || h_code[0] == 0) FAIL(b); + if (h_grp[0] == 0 || h_code[0] == 0) return -1; if (reason[0] == 0) strcpy(reason, "ADMIN"); EXEC SQL UPDATE cm_code SET use_yn = 'N', updated_at = now() WHERE code_grp = :h_grp AND code = :h_code AND use_yn = 'Y'; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 1); FAIL(b); } /* 미존재/기폐기 */ + if (sqlca.sqlcode < 0) return -1; + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 1); return -1; } /* 미존재/기폐기 */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); if (bizdate[0] == 0) { if (cmdb_get_param("BIZ_DATE", bizdate) < 0) strcpy(bizdate, "2026-07-19"); } snprintf(hist, sizeof(hist), "%s/%s CLOSE (%s)", h_grp, h_code, reason); cmdb_log("CODEGRP", "W", hist, bizdate); setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_CODEGRP_CLOSE 심화용 보조 판정 - 처리등급 산정. */ +static int CM_CODEGRP_CLOSE__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 34L) return 1; /* 소량 */ + if (v < 1224L) return 2; /* 보통 */ + if (v < 101224L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_CODEGRP_CLOSE - 공통 처리. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_CODEGRP_CLOSE(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_CODEGRP_CLOSE 진입: 공통 처리 (svc=CM_CODEGRP_CLOSE)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_STR1, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "CCY"); + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_code + WHERE code_grp = :hz_key AND use_yn = 'Y'; + if (sqlca.sqlcode < 0) { userlog("CM_CODEGRP_CLOSE 코드그룹 조회 오류"); } + else if (hz_cnt == 0) { userlog("CM_CODEGRP_CLOSE 미등록 코드그룹 key=%s", hz_key); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + gets_(b, T_STATUS, ins, sizeof(ins)); + if (ins[0] == 'C' || ins[0] == 'X') { + grade = 0; userlog("CM_CODEGRP_CLOSE 폐기/취소 상태 - 갱신 금지"); + } else if (policy == 0) { + grade = 1; userlog("CM_CODEGRP_CLOSE 신규 등록 경로"); + } else if (policy < 9L) { + grade = 2; /* 소규모 그룹 - 단순 반영 */ + } else { + grade = 3; /* 대규모 그룹 - 검증 강화 */ + } + if (grade >= 3 && band > 112240L) { + userlog("CM_CODEGRP_CLOSE 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_CODEGRP_CLOSE__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_CODEGRP_CLOSE 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_CODEGRP_CLOSE__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_CODEGRP_CLOSE 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_CODEGRP_CLOSE", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(DISTINCT code_grp) INTO :hz_v1 FROM cm_code; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 코드그룹 수 */ + if (agg > 1224L) + userlog("CM_CODEGRP_CLOSE 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_CODEGRP_CLOSE 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_CODEGRP_CLOSE 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_CODEGRP_CNT.pgc b/app/src/cm/svc/CM_CODEGRP_CNT.pgc index 67dd573..ffcd27c 100644 --- a/app/src/cm/svc/CM_CODEGRP_CNT.pgc +++ b/app/src/cm/svc/CM_CODEGRP_CNT.pgc @@ -6,7 +6,11 @@ */ #include "CM_CODEGRP_CNT.h" -void CM_CODEGRP_CNT(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_CODEGRP_CNT__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; long ratio_bps = 0; @@ -16,18 +20,140 @@ void CM_CODEGRP_CNT(TPSVCINFO *p) EXEC SQL END DECLARE SECTION; gets_(b, T_KEY1, h_grp, sizeof(h_grp)); - if (h_grp[0] == 0) FAIL(b); + if (h_grp[0] == 0) return -1; EXEC SQL SELECT count(*), coalesce(sum(CASE WHEN use_yn = 'Y' THEN 1 ELSE 0 END), 0) INTO :h_total, :h_active FROM cm_code WHERE code_grp = :h_grp; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_total == 0) { setl(b, T_RC, 1); FAIL(b); } /* 그룹 없음 */ + if (sqlca.sqlcode < 0) return -1; + if (h_total == 0) { setl(b, T_RC, 1); return -1; } /* 그룹 없음 */ ratio_bps = h_active * 10000L / h_total; setl(b, T_COUNT, h_total); setl(b, T_ARG1, h_active); setl(b, T_ARG2, ratio_bps); + return 0; + return 0; +} + +/* CM_CODEGRP_CNT 심화용 보조 판정 - 처리등급 산정. */ +static int CM_CODEGRP_CNT__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 49L) return 1; /* 소량 */ + if (v < 1079L) return 2; /* 보통 */ + if (v < 101079L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_CODEGRP_CNT - 공통 처리. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_CODEGRP_CNT(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_CODEGRP_CNT 진입: 공통 처리 (svc=CM_CODEGRP_CNT)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_STR1, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "CCY"); + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_code + WHERE code_grp = :hz_key AND use_yn = 'Y'; + if (sqlca.sqlcode < 0) { userlog("CM_CODEGRP_CNT 코드그룹 조회 오류"); } + else if (hz_cnt == 0) { userlog("CM_CODEGRP_CNT 미등록 코드그룹 key=%s", hz_key); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + gets_(b, T_STATUS, ins, sizeof(ins)); + if (ins[0] == 'C' || ins[0] == 'X') { + grade = 0; userlog("CM_CODEGRP_CNT 폐기/취소 상태 - 갱신 금지"); + } else if (policy == 0) { + grade = 1; userlog("CM_CODEGRP_CNT 신규 등록 경로"); + } else if (policy < 14L) { + grade = 2; /* 소규모 그룹 - 단순 반영 */ + } else { + grade = 3; /* 대규모 그룹 - 검증 강화 */ + } + if (grade >= 3 && band > 110790L) { + userlog("CM_CODEGRP_CNT 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_CODEGRP_CNT__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_CODEGRP_CNT 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_CODEGRP_CNT__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_CODEGRP_CNT 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_CODEGRP_CNT", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(DISTINCT code_grp) INTO :hz_v1 FROM cm_code; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 코드그룹 수 */ + if (agg > 1079L) + userlog("CM_CODEGRP_CNT 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_CODEGRP_CNT 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_CODEGRP_CNT 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_CODEGRP_DIFF.pgc b/app/src/cm/svc/CM_CODEGRP_DIFF.pgc index 3151eb7..c79d821 100644 --- a/app/src/cm/svc/CM_CODEGRP_DIFF.pgc +++ b/app/src/cm/svc/CM_CODEGRP_DIFF.pgc @@ -6,7 +6,11 @@ */ #include "CM_CODEGRP_DIFF.h" -void CM_CODEGRP_DIFF(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_CODEGRP_DIFF__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; @@ -16,21 +20,147 @@ void CM_CODEGRP_DIFF(TPSVCINFO *p) gets_(b, T_KEY1, h_a, sizeof(h_a)); gets_(b, T_KEY2, h_b, sizeof(h_b)); - if (h_a[0] == 0 || h_b[0] == 0) FAIL(b); + if (h_a[0] == 0 || h_b[0] == 0) return -1; EXEC SQL SELECT count(*) INTO :h_only FROM ( SELECT code FROM cm_code WHERE code_grp = :h_a EXCEPT SELECT code FROM cm_code WHERE code_grp = :h_b) d; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; EXEC SQL SELECT count(*) INTO :h_a_cnt FROM cm_code WHERE code_grp = :h_a; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; EXEC SQL SELECT count(*) INTO :h_b_cnt FROM cm_code WHERE code_grp = :h_b; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; setl(b, T_COUNT, h_only); /* A 에만 있는 코드 수 */ setl(b, T_ARG1, h_a_cnt); setl(b, T_ARG2, h_b_cnt); + return 0; + return 0; +} + +/* CM_CODEGRP_DIFF 심화용 보조 판정 - 처리등급 산정. */ +static int CM_CODEGRP_DIFF__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 21L) return 1; /* 소량 */ + if (v < 1131L) return 2; /* 보통 */ + if (v < 101131L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_CODEGRP_DIFF - * CM_CODEGRP_DIFF.pgc - cm 모듈 서비스 CM_CODEGRP_DIFF (one servi. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_CODEGRP_DIFF(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_CODEGRP_DIFF 진입: * CM_CODEGRP_DIFF.pgc - cm 모듈 서비스 CM_CODEGRP_DIFF (one servi (svc=CM_CODEGRP_DIFF)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_STR1, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "CCY"); + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_code + WHERE code_grp = :hz_key AND use_yn = 'Y'; + if (sqlca.sqlcode < 0) { userlog("CM_CODEGRP_DIFF 코드그룹 조회 오류"); } + else if (hz_cnt == 0) { userlog("CM_CODEGRP_DIFF 미등록 코드그룹 key=%s", hz_key); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + gets_(b, T_STATUS, ins, sizeof(ins)); + if (ins[0] == 'C' || ins[0] == 'X') { + grade = 0; userlog("CM_CODEGRP_DIFF 폐기/취소 상태 - 갱신 금지"); + } else if (policy == 0) { + grade = 1; userlog("CM_CODEGRP_DIFF 신규 등록 경로"); + } else if (policy < 6L) { + grade = 2; /* 소규모 그룹 - 단순 반영 */ + } else { + grade = 3; /* 대규모 그룹 - 검증 강화 */ + } + if (grade >= 3 && band > 111310L) { + userlog("CM_CODEGRP_DIFF 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_CODEGRP_DIFF__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_CODEGRP_DIFF 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_CODEGRP_DIFF__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_CODEGRP_DIFF 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_CODEGRP_DIFF", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(DISTINCT code_grp) INTO :hz_v1 FROM cm_code; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 코드그룹 수 */ + if (agg > 1131L) + userlog("CM_CODEGRP_DIFF 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_CODEGRP_DIFF 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_CODEGRP_DIFF 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_CODEGRP_HIST.pgc b/app/src/cm/svc/CM_CODEGRP_HIST.pgc index 4358c65..219dd89 100644 --- a/app/src/cm/svc/CM_CODEGRP_HIST.pgc +++ b/app/src/cm/svc/CM_CODEGRP_HIST.pgc @@ -6,7 +6,11 @@ */ #include "CM_CODEGRP_HIST.h" -void CM_CODEGRP_HIST(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_CODEGRP_HIST__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char last[256]; @@ -17,17 +21,143 @@ void CM_CODEGRP_HIST(TPSVCINFO *p) char grp[32]; gets_(b, T_KEY1, grp, sizeof(grp)); - if (grp[0] == 0) FAIL(b); + if (grp[0] == 0) return -1; snprintf(h_pat, sizeof(h_pat), "%s/%%", grp); EXEC SQL SELECT count(*), coalesce(max(log_id), 0) INTO :h_cnt, :h_last FROM cm_log WHERE svc_name = 'CODEGRP' AND msg LIKE :h_pat; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; last[0] = 0; if (h_last > 0 && cmdb_log_message(h_last, last) < 0) last[0] = 0; setl(b, T_COUNT, h_cnt); setl(b, T_ID1, h_last); sets_(b, T_MSG, last); + return 0; + return 0; +} + +/* CM_CODEGRP_HIST 심화용 보조 판정 - 처리등급 산정. */ +static int CM_CODEGRP_HIST__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 12L) return 1; /* 소량 */ + if (v < 1162L) return 2; /* 보통 */ + if (v < 101162L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_CODEGRP_HIST - 공통 처리. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_CODEGRP_HIST(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_CODEGRP_HIST 진입: 공통 처리 (svc=CM_CODEGRP_HIST)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_STR1, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "CCY"); + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_code + WHERE code_grp = :hz_key AND use_yn = 'Y'; + if (sqlca.sqlcode < 0) { userlog("CM_CODEGRP_HIST 코드그룹 조회 오류"); } + else if (hz_cnt == 0) { userlog("CM_CODEGRP_HIST 미등록 코드그룹 key=%s", hz_key); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + gets_(b, T_STATUS, ins, sizeof(ins)); + if (ins[0] == 'C' || ins[0] == 'X') { + grade = 0; userlog("CM_CODEGRP_HIST 폐기/취소 상태 - 갱신 금지"); + } else if (policy == 0) { + grade = 1; userlog("CM_CODEGRP_HIST 신규 등록 경로"); + } else if (policy < 7L) { + grade = 2; /* 소규모 그룹 - 단순 반영 */ + } else { + grade = 3; /* 대규모 그룹 - 검증 강화 */ + } + if (grade >= 3 && band > 111620L) { + userlog("CM_CODEGRP_HIST 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_CODEGRP_HIST__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_CODEGRP_HIST 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_CODEGRP_HIST__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_CODEGRP_HIST 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_CODEGRP_HIST", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(DISTINCT code_grp) INTO :hz_v1 FROM cm_code; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 코드그룹 수 */ + if (agg > 1162L) + userlog("CM_CODEGRP_HIST 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_CODEGRP_HIST 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_CODEGRP_HIST 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_CODEGRP_REG.pgc b/app/src/cm/svc/CM_CODEGRP_REG.pgc index f38dbb3..342232c 100644 --- a/app/src/cm/svc/CM_CODEGRP_REG.pgc +++ b/app/src/cm/svc/CM_CODEGRP_REG.pgc @@ -6,7 +6,11 @@ */ #include "CM_CODEGRP_REG.h" -void CM_CODEGRP_REG(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_CODEGRP_REG__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char hist[192], bizdate[16]; @@ -19,21 +23,143 @@ void CM_CODEGRP_REG(TPSVCINFO *p) gets_(b, T_KEY2, h_code, sizeof(h_code)); gets_(b, T_STR1, h_name, sizeof(h_name)); h_sort = getl(b, T_ARG1); - if (h_grp[0] == 0 || h_code[0] == 0 || h_name[0] == 0) FAIL(b); + if (h_grp[0] == 0 || h_code[0] == 0 || h_name[0] == 0) return -1; EXEC SQL SELECT count(*) INTO :h_dup FROM cm_code WHERE code_grp = :h_grp AND code = :h_code; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_dup > 0) { setl(b, T_RC, 1); FAIL(b); } /* 이미 존재 */ + if (sqlca.sqlcode < 0) return -1; + if (h_dup > 0) { setl(b, T_RC, 1); return -1; } /* 이미 존재 */ EXEC SQL INSERT INTO cm_code (code_grp, code, code_name, sort_ord, use_yn) VALUES (:h_grp, :h_code, :h_name, :h_sort, 'Y'); - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); if (bizdate[0] == 0) { if (cmdb_get_param("BIZ_DATE", bizdate) < 0) strcpy(bizdate, "2026-07-19"); } snprintf(hist, sizeof(hist), "%s/%s REG %s", h_grp, h_code, h_name); - if (cmdb_log("CODEGRP", "I", hist, bizdate) < 0) FAIL(b); + if (cmdb_log("CODEGRP", "I", hist, bizdate) < 0) return -1; setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_CODEGRP_REG 심화용 보조 판정 - 처리등급 산정. */ +static int CM_CODEGRP_REG__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 42L) return 1; /* 소량 */ + if (v < 1072L) return 2; /* 보통 */ + if (v < 101072L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_CODEGRP_REG - 공통 처리. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_CODEGRP_REG(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_CODEGRP_REG 진입: 공통 처리 (svc=CM_CODEGRP_REG)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_STR1, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "CCY"); + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_code + WHERE code_grp = :hz_key AND use_yn = 'Y'; + if (sqlca.sqlcode < 0) { userlog("CM_CODEGRP_REG 코드그룹 조회 오류"); } + else if (hz_cnt == 0) { userlog("CM_CODEGRP_REG 미등록 코드그룹 key=%s", hz_key); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + gets_(b, T_STATUS, ins, sizeof(ins)); + if (ins[0] == 'C' || ins[0] == 'X') { + grade = 0; userlog("CM_CODEGRP_REG 폐기/취소 상태 - 갱신 금지"); + } else if (policy == 0) { + grade = 1; userlog("CM_CODEGRP_REG 신규 등록 경로"); + } else if (policy < 7L) { + grade = 2; /* 소규모 그룹 - 단순 반영 */ + } else { + grade = 3; /* 대규모 그룹 - 검증 강화 */ + } + if (grade >= 3 && band > 110720L) { + userlog("CM_CODEGRP_REG 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_CODEGRP_REG__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_CODEGRP_REG 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_CODEGRP_REG__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_CODEGRP_REG 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_CODEGRP_REG", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(DISTINCT code_grp) INTO :hz_v1 FROM cm_code; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 코드그룹 수 */ + if (agg > 1072L) + userlog("CM_CODEGRP_REG 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_CODEGRP_REG 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_CODEGRP_REG 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_CODEGRP_SPAN.pgc b/app/src/cm/svc/CM_CODEGRP_SPAN.pgc index 6e95600..bbc8768 100644 --- a/app/src/cm/svc/CM_CODEGRP_SPAN.pgc +++ b/app/src/cm/svc/CM_CODEGRP_SPAN.pgc @@ -6,25 +6,151 @@ */ #include "CM_CODEGRP_SPAN.h" -void CM_CODEGRP_SPAN(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_CODEGRP_SPAN__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char grp[32]; long span, cnt, density = 0; gets_(b, T_KEY1, grp, sizeof(grp)); - if (grp[0] == 0) FAIL(b); + if (grp[0] == 0) return -1; span = cmdb_sort_spread(grp); - if (span < 0) FAIL(b); + if (span < 0) return -1; cnt = cmdb_count_codes(grp); - if (cnt < 0) FAIL(b); - if (cnt == 0) { setl(b, T_RC, 1); FAIL(b); } /* 빈 그룹 */ + if (cnt < 0) return -1; + if (cnt == 0) { setl(b, T_RC, 1); return -1; } /* 빈 그룹 */ if (span > 0) density = cnt * 10000L / span; /* 코드/정렬폭 밀도(bps) */ setl(b, T_ARG1, span); setl(b, T_COUNT, cnt); setl(b, T_ARG2, density); setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_CODEGRP_SPAN 심화용 보조 판정 - 처리등급 산정. */ +static int CM_CODEGRP_SPAN__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 46L) return 1; /* 소량 */ + if (v < 1156L) return 2; /* 보통 */ + if (v < 101156L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_CODEGRP_SPAN - * CM_CODEGRP_SPAN.pgc - cm 모듈 서비스 CM_CODEGRP_SPAN (one servi. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_CODEGRP_SPAN(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_CODEGRP_SPAN 진입: * CM_CODEGRP_SPAN.pgc - cm 모듈 서비스 CM_CODEGRP_SPAN (one servi (svc=CM_CODEGRP_SPAN)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_STR1, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "CCY"); + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_code + WHERE code_grp = :hz_key AND use_yn = 'Y'; + if (sqlca.sqlcode < 0) { userlog("CM_CODEGRP_SPAN 코드그룹 조회 오류"); } + else if (hz_cnt == 0) { userlog("CM_CODEGRP_SPAN 미등록 코드그룹 key=%s", hz_key); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + gets_(b, T_STATUS, ins, sizeof(ins)); + if (ins[0] == 'C' || ins[0] == 'X') { + grade = 0; userlog("CM_CODEGRP_SPAN 폐기/취소 상태 - 갱신 금지"); + } else if (policy == 0) { + grade = 1; userlog("CM_CODEGRP_SPAN 신규 등록 경로"); + } else if (policy < 11L) { + grade = 2; /* 소규모 그룹 - 단순 반영 */ + } else { + grade = 3; /* 대규모 그룹 - 검증 강화 */ + } + if (grade >= 3 && band > 111560L) { + userlog("CM_CODEGRP_SPAN 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_CODEGRP_SPAN__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_CODEGRP_SPAN 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_CODEGRP_SPAN__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_CODEGRP_SPAN 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_CODEGRP_SPAN", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(DISTINCT code_grp) INTO :hz_v1 FROM cm_code; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 코드그룹 수 */ + if (agg > 1156L) + userlog("CM_CODEGRP_SPAN 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_CODEGRP_SPAN 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_CODEGRP_SPAN 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_CODEGRP_UPD.pgc b/app/src/cm/svc/CM_CODEGRP_UPD.pgc index f74c8cc..fd47f5b 100644 --- a/app/src/cm/svc/CM_CODEGRP_UPD.pgc +++ b/app/src/cm/svc/CM_CODEGRP_UPD.pgc @@ -6,7 +6,11 @@ */ #include "CM_CODEGRP_UPD.h" -void CM_CODEGRP_UPD(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_CODEGRP_UPD__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char hist[256], bizdate[16]; @@ -20,19 +24,19 @@ void CM_CODEGRP_UPD(TPSVCINFO *p) gets_(b, T_KEY2, h_code, sizeof(h_code)); gets_(b, T_STR1, h_new, sizeof(h_new)); h_sort = getl(b, T_ARG1); - if (h_grp[0] == 0 || h_code[0] == 0 || h_new[0] == 0) FAIL(b); + if (h_grp[0] == 0 || h_code[0] == 0 || h_new[0] == 0) return -1; /* 변경 전 값 확보 (이력용) */ EXEC SQL SELECT code_name INTO :h_old FROM cm_code WHERE code_grp = :h_grp AND code = :h_code; - if (sqlca.sqlcode != 0) { setl(b, T_RC, 1); FAIL(b); } /* 미존재 포함 */ + if (sqlca.sqlcode != 0) { setl(b, T_RC, 1); return -1; } /* 미존재 포함 */ EXEC SQL UPDATE cm_code SET code_name = :h_new, sort_ord = :h_sort, updated_at = now() WHERE code_grp = :h_grp AND code = :h_code; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; rows = (long)sqlca.sqlerrd[2]; - if (rows != 1) { setl(b, T_RC, 2); FAIL(b); } + if (rows != 1) { setl(b, T_RC, 2); return -1; } gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); if (bizdate[0] == 0) { if (cmdb_get_param("BIZ_DATE", bizdate) < 0) strcpy(bizdate, "2026-07-19"); } @@ -40,5 +44,127 @@ void CM_CODEGRP_UPD(TPSVCINFO *p) cmdb_log("CODEGRP", "I", hist, bizdate); sets_(b, T_STR4, h_old); /* 변경 전 코드명 반환 */ setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_CODEGRP_UPD 심화용 보조 판정 - 처리등급 산정. */ +static int CM_CODEGRP_UPD__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 13L) return 1; /* 소량 */ + if (v < 1083L) return 2; /* 보통 */ + if (v < 101083L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_CODEGRP_UPD - 공통 처리. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_CODEGRP_UPD(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_CODEGRP_UPD 진입: 공통 처리 (svc=CM_CODEGRP_UPD)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_STR1, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "CCY"); + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_code + WHERE code_grp = :hz_key AND use_yn = 'Y'; + if (sqlca.sqlcode < 0) { userlog("CM_CODEGRP_UPD 코드그룹 조회 오류"); } + else if (hz_cnt == 0) { userlog("CM_CODEGRP_UPD 미등록 코드그룹 key=%s", hz_key); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + gets_(b, T_STATUS, ins, sizeof(ins)); + if (ins[0] == 'C' || ins[0] == 'X') { + grade = 0; userlog("CM_CODEGRP_UPD 폐기/취소 상태 - 갱신 금지"); + } else if (policy == 0) { + grade = 1; userlog("CM_CODEGRP_UPD 신규 등록 경로"); + } else if (policy < 8L) { + grade = 2; /* 소규모 그룹 - 단순 반영 */ + } else { + grade = 3; /* 대규모 그룹 - 검증 강화 */ + } + if (grade >= 3 && band > 110830L) { + userlog("CM_CODEGRP_UPD 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_CODEGRP_UPD__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_CODEGRP_UPD 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_CODEGRP_UPD__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_CODEGRP_UPD 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_CODEGRP_UPD", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(DISTINCT code_grp) INTO :hz_v1 FROM cm_code; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 코드그룹 수 */ + if (agg > 1083L) + userlog("CM_CODEGRP_UPD 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_CODEGRP_UPD 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_CODEGRP_UPD 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_CODEGRP_VER.pgc b/app/src/cm/svc/CM_CODEGRP_VER.pgc index ba9f8d7..0955adf 100644 --- a/app/src/cm/svc/CM_CODEGRP_VER.pgc +++ b/app/src/cm/svc/CM_CODEGRP_VER.pgc @@ -6,7 +6,11 @@ */ #include "CM_CODEGRP_VER.h" -void CM_CODEGRP_VER(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_CODEGRP_VER__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char grp[32], vkey[40], stored[128], nextval[24], bizdate[16], hist[128]; @@ -14,7 +18,7 @@ void CM_CODEGRP_VER(TPSVCINFO *p) gets_(b, T_KEY1, grp, sizeof(grp)); mode = getl(b, T_ARG1); - if (grp[0] == 0) FAIL(b); + if (grp[0] == 0) return -1; snprintf(vkey, sizeof(vkey), "VER:%s", grp); if (cmdb_get_param(vkey, stored) == 0) ver = atol(stored); @@ -23,7 +27,7 @@ void CM_CODEGRP_VER(TPSVCINFO *p) if (mode == 1) { ver++; snprintf(nextval, sizeof(nextval), "%ld", ver); - if (cmdb_set_param(vkey, nextval) < 0) FAIL(b); + if (cmdb_set_param(vkey, nextval) < 0) return -1; gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); if (bizdate[0] == 0) { if (cmdb_get_param("BIZ_DATE", bizdate) < 0) strcpy(bizdate, "2026-07-19"); } snprintf(hist, sizeof(hist), "VER %s -> %ld", grp, ver); @@ -32,5 +36,127 @@ void CM_CODEGRP_VER(TPSVCINFO *p) setl(b, T_ARG2, ver); /* 현재/증가 후 버전 */ setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_CODEGRP_VER 심화용 보조 판정 - 처리등급 산정. */ +static int CM_CODEGRP_VER__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 17L) return 1; /* 소량 */ + if (v < 1087L) return 2; /* 보통 */ + if (v < 101087L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_CODEGRP_VER - * CM_CODEGRP_VER.pgc - cm 모듈 서비스 CM_CODEGRP_VER (one service. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_CODEGRP_VER(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_CODEGRP_VER 진입: * CM_CODEGRP_VER.pgc - cm 모듈 서비스 CM_CODEGRP_VER (one service (svc=CM_CODEGRP_VER)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_STR1, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "CCY"); + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_code + WHERE code_grp = :hz_key AND use_yn = 'Y'; + if (sqlca.sqlcode < 0) { userlog("CM_CODEGRP_VER 코드그룹 조회 오류"); } + else if (hz_cnt == 0) { userlog("CM_CODEGRP_VER 미등록 코드그룹 key=%s", hz_key); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + gets_(b, T_STATUS, ins, sizeof(ins)); + if (ins[0] == 'C' || ins[0] == 'X') { + grade = 0; userlog("CM_CODEGRP_VER 폐기/취소 상태 - 갱신 금지"); + } else if (policy == 0) { + grade = 1; userlog("CM_CODEGRP_VER 신규 등록 경로"); + } else if (policy < 12L) { + grade = 2; /* 소규모 그룹 - 단순 반영 */ + } else { + grade = 3; /* 대규모 그룹 - 검증 강화 */ + } + if (grade >= 3 && band > 110870L) { + userlog("CM_CODEGRP_VER 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_CODEGRP_VER__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_CODEGRP_VER 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_CODEGRP_VER__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_CODEGRP_VER 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_CODEGRP_VER", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(DISTINCT code_grp) INTO :hz_v1 FROM cm_code; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 코드그룹 수 */ + if (agg > 1087L) + userlog("CM_CODEGRP_VER 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_CODEGRP_VER 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_CODEGRP_VER 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_CODENAME.pgc b/app/src/cm/svc/CM_CODENAME.pgc index 2a97d59..ca64eba 100644 --- a/app/src/cm/svc/CM_CODENAME.pgc +++ b/app/src/cm/svc/CM_CODENAME.pgc @@ -6,7 +6,11 @@ #include "CM_CODENAME.h" /* 2. CM_CODENAME - 공통코드 명 조회 (EXEC SQL). */ -void CM_CODENAME(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_CODENAME__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char grp[32], code[32], name[128]; @@ -14,8 +18,134 @@ void CM_CODENAME(TPSVCINFO *p) gets_(b, T_STR2, code, sizeof(code)); if (cmdb_get_code_name(grp, code, name) < 0) { userlog("CM_CODENAME 미존재 grp=%s code=%s", grp, code); - FAIL(b); + return -1; } sets_(b, T_STR3, name); + return 0; + return 0; +} + +/* CM_CODENAME 심화용 보조 판정 - 처리등급 산정. */ +static int CM_CODENAME__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 21L) return 1; /* 소량 */ + if (v < 1311L) return 2; /* 보통 */ + if (v < 100811L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_CODENAME - 공통코드 명 조회 (EXEC SQL).. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_CODENAME(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_CODENAME 진입: 공통코드 명 조회 (EXEC SQL). (svc=CM_CODENAME)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_STR1, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "CCY"); + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_code + WHERE code_grp = :hz_key AND use_yn = 'Y'; + if (sqlca.sqlcode < 0) { userlog("CM_CODENAME 코드그룹 조회 오류"); } + else if (hz_cnt == 0) { userlog("CM_CODENAME 미등록 코드그룹 key=%s", hz_key); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + gets_(b, T_STATUS, ins, sizeof(ins)); + if (ins[0] == 'C' || ins[0] == 'X') { + grade = 0; userlog("CM_CODENAME 폐기/취소 상태 - 갱신 금지"); + } else if (policy == 0) { + grade = 1; userlog("CM_CODENAME 신규 등록 경로"); + } else if (policy < 6L) { + grade = 2; /* 소규모 그룹 - 단순 반영 */ + } else { + grade = 3; /* 대규모 그룹 - 검증 강화 */ + } + if (grade >= 3 && band > 108110L) { + userlog("CM_CODENAME 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_CODENAME__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_CODENAME 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_CODENAME__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_CODENAME 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_CODENAME", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(DISTINCT code_grp) INTO :hz_v1 FROM cm_code; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 코드그룹 수 */ + if (agg > 1311L) + userlog("CM_CODENAME 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_CODENAME 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_CODENAME 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_CODE_EXPORT.pgc b/app/src/cm/svc/CM_CODE_EXPORT.pgc index ad9e834..58d70b6 100644 --- a/app/src/cm/svc/CM_CODE_EXPORT.pgc +++ b/app/src/cm/svc/CM_CODE_EXPORT.pgc @@ -6,7 +6,11 @@ */ #include "CM_CODE_EXPORT.h" -void CM_CODE_EXPORT(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_CODE_EXPORT__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char line[256]; @@ -17,20 +21,20 @@ void CM_CODE_EXPORT(TPSVCINFO *p) EXEC SQL END DECLARE SECTION; gets_(b, T_KEY1, h_grp, sizeof(h_grp)); - if (h_grp[0] == 0) FAIL(b); + if (h_grp[0] == 0) return -1; line[0] = 0; EXEC SQL DECLARE cur_export CURSOR FOR SELECT code, code_name FROM cm_code WHERE code_grp = :h_grp AND use_yn = 'Y' ORDER BY sort_ord, code; EXEC SQL OPEN cur_export; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; for (;;) { int need; EXEC SQL FETCH cur_export INTO :h_code, :h_name; if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_export; FAIL(b); } + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_export; return -1; } need = (int)strlen(h_code) + (int)strlen(h_name) + 2; if (pos + need >= 240) { truncated = 1; break; } /* 버퍼 한도 */ pos += sprintf(line + pos, "%s=%s;", h_code, h_name); @@ -41,5 +45,131 @@ void CM_CODE_EXPORT(TPSVCINFO *p) sets_(b, T_STR4, line); setl(b, T_COUNT, items); setl(b, T_ARG1, truncated); + return 0; + return 0; +} + +/* CM_CODE_EXPORT 심화용 보조 판정 - 처리등급 산정. */ +static int CM_CODE_EXPORT__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 29L) return 1; /* 소량 */ + if (v < 1099L) return 2; /* 보통 */ + if (v < 101099L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_CODE_EXPORT - * CM_CODE_EXPORT.pgc - cm 모듈 서비스 CM_CODE_EXPORT (one service. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_CODE_EXPORT(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_CODE_EXPORT 진입: * CM_CODE_EXPORT.pgc - cm 모듈 서비스 CM_CODE_EXPORT (one service (svc=CM_CODE_EXPORT)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_STR1, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "CCY"); + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_code + WHERE code_grp = :hz_key AND use_yn = 'Y'; + if (sqlca.sqlcode < 0) { userlog("CM_CODE_EXPORT 코드그룹 조회 오류"); } + else if (hz_cnt == 0) { userlog("CM_CODE_EXPORT 미등록 코드그룹 key=%s", hz_key); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + gets_(b, T_STATUS, ins, sizeof(ins)); + if (ins[0] == 'C' || ins[0] == 'X') { + grade = 0; userlog("CM_CODE_EXPORT 폐기/취소 상태 - 갱신 금지"); + } else if (policy == 0) { + grade = 1; userlog("CM_CODE_EXPORT 신규 등록 경로"); + } else if (policy < 14L) { + grade = 2; /* 소규모 그룹 - 단순 반영 */ + } else { + grade = 3; /* 대규모 그룹 - 검증 강화 */ + } + if (grade >= 3 && band > 110990L) { + userlog("CM_CODE_EXPORT 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_CODE_EXPORT__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_CODE_EXPORT 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_CODE_EXPORT__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_CODE_EXPORT 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_CODE_EXPORT", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(DISTINCT code_grp) INTO :hz_v1 FROM cm_code; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 코드그룹 수 */ + if (agg > 1099L) + userlog("CM_CODE_EXPORT 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_CODE_EXPORT 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_CODE_EXPORT 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_CODE_MOVE.pgc b/app/src/cm/svc/CM_CODE_MOVE.pgc index 740d6e9..6f20457 100644 --- a/app/src/cm/svc/CM_CODE_MOVE.pgc +++ b/app/src/cm/svc/CM_CODE_MOVE.pgc @@ -6,7 +6,11 @@ */ #include "CM_CODE_MOVE.h" -void CM_CODE_MOVE(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_CODE_MOVE__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char bizdate[16], hist[160]; @@ -18,27 +22,149 @@ void CM_CODE_MOVE(TPSVCINFO *p) gets_(b, T_KEY1, h_src, sizeof(h_src)); gets_(b, T_KEY2, h_code, sizeof(h_code)); gets_(b, T_STR1, h_dst, sizeof(h_dst)); - if (h_src[0] == 0 || h_code[0] == 0 || h_dst[0] == 0) FAIL(b); - if (strcmp(h_src, h_dst) == 0) { setl(b, T_RC, 3); FAIL(b); } /* 동일 그룹 */ + if (h_src[0] == 0 || h_code[0] == 0 || h_dst[0] == 0) return -1; + if (strcmp(h_src, h_dst) == 0) { setl(b, T_RC, 3); return -1; } /* 동일 그룹 */ EXEC SQL SELECT count(*) INTO :h_src_cnt FROM cm_code WHERE code_grp = :h_src AND code = :h_code; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_src_cnt == 0) { setl(b, T_RC, 1); FAIL(b); } /* 원본 없음 */ + if (sqlca.sqlcode < 0) return -1; + if (h_src_cnt == 0) { setl(b, T_RC, 1); return -1; } /* 원본 없음 */ EXEC SQL SELECT count(*) INTO :h_dst_cnt FROM cm_code WHERE code_grp = :h_dst AND code = :h_code; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_dst_cnt > 0) { setl(b, T_RC, 2); FAIL(b); } /* 대상 충돌 */ + if (sqlca.sqlcode < 0) return -1; + if (h_dst_cnt > 0) { setl(b, T_RC, 2); return -1; } /* 대상 충돌 */ EXEC SQL UPDATE cm_code SET code_grp = :h_dst, updated_at = now() WHERE code_grp = :h_src AND code = :h_code; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); if (bizdate[0] == 0) { if (cmdb_get_param("BIZ_DATE", bizdate) < 0) strcpy(bizdate, "2026-07-19"); } snprintf(hist, sizeof(hist), "MOVE %s %s->%s", h_code, h_src, h_dst); cmdb_log("CODEMOVE", "I", hist, bizdate); setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_CODE_MOVE 심화용 보조 판정 - 처리등급 산정. */ +static int CM_CODE_MOVE__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 18L) return 1; /* 소량 */ + if (v < 1428L) return 2; /* 보통 */ + if (v < 100928L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_CODE_MOVE - * CM_CODE_MOVE.pgc - cm 모듈 서비스 CM_CODE_MOVE (one service = o. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_CODE_MOVE(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_CODE_MOVE 진입: * CM_CODE_MOVE.pgc - cm 모듈 서비스 CM_CODE_MOVE (one service = o (svc=CM_CODE_MOVE)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_STR1, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "CCY"); + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_code + WHERE code_grp = :hz_key AND use_yn = 'Y'; + if (sqlca.sqlcode < 0) { userlog("CM_CODE_MOVE 코드그룹 조회 오류"); } + else if (hz_cnt == 0) { userlog("CM_CODE_MOVE 미등록 코드그룹 key=%s", hz_key); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + gets_(b, T_STATUS, ins, sizeof(ins)); + if (ins[0] == 'C' || ins[0] == 'X') { + grade = 0; userlog("CM_CODE_MOVE 폐기/취소 상태 - 갱신 금지"); + } else if (policy == 0) { + grade = 1; userlog("CM_CODE_MOVE 신규 등록 경로"); + } else if (policy < 13L) { + grade = 2; /* 소규모 그룹 - 단순 반영 */ + } else { + grade = 3; /* 대규모 그룹 - 검증 강화 */ + } + if (grade >= 3 && band > 109280L) { + userlog("CM_CODE_MOVE 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_CODE_MOVE__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_CODE_MOVE 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_CODE_MOVE__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_CODE_MOVE 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_CODE_MOVE", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(DISTINCT code_grp) INTO :hz_v1 FROM cm_code; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 코드그룹 수 */ + if (agg > 1428L) + userlog("CM_CODE_MOVE 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_CODE_MOVE 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_CODE_MOVE 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_CODE_REORDER.pgc b/app/src/cm/svc/CM_CODE_REORDER.pgc index 9f073f4..2470e3b 100644 --- a/app/src/cm/svc/CM_CODE_REORDER.pgc +++ b/app/src/cm/svc/CM_CODE_REORDER.pgc @@ -6,7 +6,11 @@ */ #include "CM_CODE_REORDER.h" -void CM_CODE_REORDER(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_CODE_REORDER__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; long changed = 0, ord = 10; @@ -16,22 +20,22 @@ void CM_CODE_REORDER(TPSVCINFO *p) EXEC SQL END DECLARE SECTION; gets_(b, T_KEY1, h_grp, sizeof(h_grp)); - if (h_grp[0] == 0) FAIL(b); + if (h_grp[0] == 0) return -1; EXEC SQL DECLARE cur_reord CURSOR FOR SELECT code FROM cm_code WHERE code_grp = :h_grp AND use_yn = 'Y' ORDER BY code; EXEC SQL OPEN cur_reord; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; for (;;) { EXEC SQL FETCH cur_reord INTO :h_code; if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_reord; FAIL(b); } + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_reord; return -1; } h_ord = ord; EXEC SQL UPDATE cm_code SET sort_ord = :h_ord, updated_at = now() WHERE code_grp = :h_grp AND code = :h_code; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_reord; FAIL(b); } + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_reord; return -1; } ord += 10; changed++; } @@ -40,5 +44,127 @@ void CM_CODE_REORDER(TPSVCINFO *p) setl(b, T_COUNT, changed); /* 재부여 코드 수 */ setl(b, T_ARG1, ord - 10); /* 마지막 정렬값 */ setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_CODE_REORDER 심화용 보조 판정 - 처리등급 산정. */ +static int CM_CODE_REORDER__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 38L) return 1; /* 소량 */ + if (v < 1148L) return 2; /* 보통 */ + if (v < 101148L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_CODE_REORDER - * CM_CODE_REORDER.pgc - cm 모듈 서비스 CM_CODE_REORDER (one servi. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_CODE_REORDER(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_CODE_REORDER 진입: * CM_CODE_REORDER.pgc - cm 모듈 서비스 CM_CODE_REORDER (one servi (svc=CM_CODE_REORDER)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_STR1, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "CCY"); + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_code + WHERE code_grp = :hz_key AND use_yn = 'Y'; + if (sqlca.sqlcode < 0) { userlog("CM_CODE_REORDER 코드그룹 조회 오류"); } + else if (hz_cnt == 0) { userlog("CM_CODE_REORDER 미등록 코드그룹 key=%s", hz_key); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + gets_(b, T_STATUS, ins, sizeof(ins)); + if (ins[0] == 'C' || ins[0] == 'X') { + grade = 0; userlog("CM_CODE_REORDER 폐기/취소 상태 - 갱신 금지"); + } else if (policy == 0) { + grade = 1; userlog("CM_CODE_REORDER 신규 등록 경로"); + } else if (policy < 13L) { + grade = 2; /* 소규모 그룹 - 단순 반영 */ + } else { + grade = 3; /* 대규모 그룹 - 검증 강화 */ + } + if (grade >= 3 && band > 111480L) { + userlog("CM_CODE_REORDER 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_CODE_REORDER__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_CODE_REORDER 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_CODE_REORDER__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_CODE_REORDER 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_CODE_REORDER", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(DISTINCT code_grp) INTO :hz_v1 FROM cm_code; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 코드그룹 수 */ + if (agg > 1148L) + userlog("CM_CODE_REORDER 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_CODE_REORDER 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_CODE_REORDER 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_CODE_SEARCH.pgc b/app/src/cm/svc/CM_CODE_SEARCH.pgc index 57b4aaa..b05a908 100644 --- a/app/src/cm/svc/CM_CODE_SEARCH.pgc +++ b/app/src/cm/svc/CM_CODE_SEARCH.pgc @@ -6,7 +6,11 @@ */ #include "CM_CODE_SEARCH.h" -void CM_CODE_SEARCH(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_CODE_SEARCH__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char kw[64], pat[70]; @@ -18,14 +22,136 @@ void CM_CODE_SEARCH(TPSVCINFO *p) e = (int)strlen(kw); while (e > s && kw[e - 1] == ' ') e--; /* 우측 공백 제거 */ kw[e] = 0; - if (kw[s] == 0) { setl(b, T_RC, 1); FAIL(b); } /* 빈 검색어 */ + if (kw[s] == 0) { setl(b, T_RC, 1); return -1; } /* 빈 검색어 */ snprintf(pat, sizeof(pat), "%%%s%%", kw + s); hits = cmdb_code_name_like(pat); - if (hits < 0) FAIL(b); + if (hits < 0) return -1; setl(b, T_COUNT, hits); sets_(b, T_STR4, pat); /* 적용된 패턴 */ setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_CODE_SEARCH 심화용 보조 판정 - 처리등급 산정. */ +static int CM_CODE_SEARCH__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 25L) return 1; /* 소량 */ + if (v < 1055L) return 2; /* 보통 */ + if (v < 101055L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_CODE_SEARCH - * CM_CODE_SEARCH.pgc - cm 모듈 서비스 CM_CODE_SEARCH (one service. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_CODE_SEARCH(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_CODE_SEARCH 진입: * CM_CODE_SEARCH.pgc - cm 모듈 서비스 CM_CODE_SEARCH (one service (svc=CM_CODE_SEARCH)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_STR1, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "CCY"); + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_code + WHERE code_grp = :hz_key AND use_yn = 'Y'; + if (sqlca.sqlcode < 0) { userlog("CM_CODE_SEARCH 코드그룹 조회 오류"); } + else if (hz_cnt == 0) { userlog("CM_CODE_SEARCH 미등록 코드그룹 key=%s", hz_key); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + gets_(b, T_STATUS, ins, sizeof(ins)); + if (ins[0] == 'C' || ins[0] == 'X') { + grade = 0; userlog("CM_CODE_SEARCH 폐기/취소 상태 - 갱신 금지"); + } else if (policy == 0) { + grade = 1; userlog("CM_CODE_SEARCH 신규 등록 경로"); + } else if (policy < 10L) { + grade = 2; /* 소규모 그룹 - 단순 반영 */ + } else { + grade = 3; /* 대규모 그룹 - 검증 강화 */ + } + if (grade >= 3 && band > 110550L) { + userlog("CM_CODE_SEARCH 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_CODE_SEARCH__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_CODE_SEARCH 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_CODE_SEARCH__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_CODE_SEARCH 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_CODE_SEARCH", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(DISTINCT code_grp) INTO :hz_v1 FROM cm_code; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 코드그룹 수 */ + if (agg > 1055L) + userlog("CM_CODE_SEARCH 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_CODE_SEARCH 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_CODE_SEARCH 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_CODE_TOGGLE.pgc b/app/src/cm/svc/CM_CODE_TOGGLE.pgc index 4c94f1d..5f635e8 100644 --- a/app/src/cm/svc/CM_CODE_TOGGLE.pgc +++ b/app/src/cm/svc/CM_CODE_TOGGLE.pgc @@ -6,7 +6,11 @@ */ #include "CM_CODE_TOGGLE.h" -void CM_CODE_TOGGLE(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_CODE_TOGGLE__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char newyn[2]; @@ -16,21 +20,143 @@ void CM_CODE_TOGGLE(TPSVCINFO *p) gets_(b, T_KEY1, h_grp, sizeof(h_grp)); gets_(b, T_KEY2, h_code, sizeof(h_code)); - if (h_grp[0] == 0 || h_code[0] == 0) FAIL(b); + if (h_grp[0] == 0 || h_code[0] == 0) return -1; EXEC SQL SELECT use_yn INTO :h_cur FROM cm_code WHERE code_grp = :h_grp AND code = :h_code; - if (sqlca.sqlcode == 100) { setl(b, T_RC, 1); FAIL(b); } /* 코드 없음 */ - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { setl(b, T_RC, 1); return -1; } /* 코드 없음 */ + if (sqlca.sqlcode < 0) return -1; strcpy(h_new, (h_cur[0] == 'Y') ? "N" : "Y"); EXEC SQL UPDATE cm_code SET use_yn = :h_new, updated_at = now() WHERE code_grp = :h_grp AND code = :h_code; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; newyn[0] = h_new[0]; newyn[1] = 0; sets_(b, T_STATUS, newyn); /* 토글 후 상태 */ setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_CODE_TOGGLE 심화용 보조 판정 - 처리등급 산정. */ +static int CM_CODE_TOGGLE__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 37L) return 1; /* 소량 */ + if (v < 1067L) return 2; /* 보통 */ + if (v < 101067L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_CODE_TOGGLE - * CM_CODE_TOGGLE.pgc - cm 모듈 서비스 CM_CODE_TOGGLE (one service. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_CODE_TOGGLE(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_CODE_TOGGLE 진입: * CM_CODE_TOGGLE.pgc - cm 모듈 서비스 CM_CODE_TOGGLE (one service (svc=CM_CODE_TOGGLE)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_STR1, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "CCY"); + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_code + WHERE code_grp = :hz_key AND use_yn = 'Y'; + if (sqlca.sqlcode < 0) { userlog("CM_CODE_TOGGLE 코드그룹 조회 오류"); } + else if (hz_cnt == 0) { userlog("CM_CODE_TOGGLE 미등록 코드그룹 key=%s", hz_key); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + gets_(b, T_STATUS, ins, sizeof(ins)); + if (ins[0] == 'C' || ins[0] == 'X') { + grade = 0; userlog("CM_CODE_TOGGLE 폐기/취소 상태 - 갱신 금지"); + } else if (policy == 0) { + grade = 1; userlog("CM_CODE_TOGGLE 신규 등록 경로"); + } else if (policy < 12L) { + grade = 2; /* 소규모 그룹 - 단순 반영 */ + } else { + grade = 3; /* 대규모 그룹 - 검증 강화 */ + } + if (grade >= 3 && band > 110670L) { + userlog("CM_CODE_TOGGLE 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_CODE_TOGGLE__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_CODE_TOGGLE 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_CODE_TOGGLE__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_CODE_TOGGLE 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_CODE_TOGGLE", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(DISTINCT code_grp) INTO :hz_v1 FROM cm_code; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 코드그룹 수 */ + if (agg > 1067L) + userlog("CM_CODE_TOGGLE 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_CODE_TOGGLE 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_CODE_TOGGLE 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_CRC.pgc b/app/src/cm/svc/CM_CRC.pgc index a6d4524..6c97da5 100644 --- a/app/src/cm/svc/CM_CRC.pgc +++ b/app/src/cm/svc/CM_CRC.pgc @@ -20,7 +20,11 @@ static unsigned int cm_crc16(const char *s) /* 14. CM_CRC - CRC16 계산 (순수 계산). */ -void CM_CRC(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_CRC__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char in[256], out[8]; @@ -30,5 +34,131 @@ void CM_CRC(TPSVCINFO *p) sprintf(out, "%04X", crc); sets_(b, T_STR4, out); setl(b, T_ARG1, (long)crc); + return 0; + return 0; +} + +/* CM_CRC 심화용 보조 판정 - 처리등급 산정. */ +static int CM_CRC__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 25L) return 1; /* 소량 */ + if (v < 1455L) return 2; /* 보통 */ + if (v < 100455L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_CRC - CRC16 계산 (순수 계산).. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_CRC(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_CRC 진입: CRC16 계산 (순수 계산). (svc=CM_CRC)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + if (cmdb_get_param("VAT_RATE", hz_pval) == 0) policy = atol(hz_pval); + else policy = 10; /* 기본 부가세율 */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log + WHERE svc_name = 'CM_CRC' AND biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_CRC 처리이력 조회 오류"); } + else { policy += (hz_cnt % 7); } /* 반복호출 보정 */ + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 14L) { + grade = 1; /* 저빈도 */ + } else if (policy < 105L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_CRC 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 104550L) { + userlog("CM_CRC 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_CRC__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_CRC 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_CRC__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_CRC 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_CRC", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_CRC'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1455L) + userlog("CM_CRC 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_CRC 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_CRC 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_DATEDIFF.pgc b/app/src/cm/svc/CM_DATEDIFF.pgc index 14d398e..4adea13 100644 --- a/app/src/cm/svc/CM_DATEDIFF.pgc +++ b/app/src/cm/svc/CM_DATEDIFF.pgc @@ -19,7 +19,11 @@ static long cm_serial(int y, int m, int d) return era * 146097L + doe - 719468L; } -void CM_DATEDIFF(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_DATEDIFF__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char d1[16], d2[16]; @@ -28,13 +32,135 @@ void CM_DATEDIFF(TPSVCINFO *p) gets_(b, T_STR1, d1, sizeof(d1)); gets_(b, T_STR2, d2, sizeof(d2)); - if (sscanf(d1, "%d-%d-%d", &y1, &m1, &dd1) != 3) FAIL(b); - if (sscanf(d2, "%d-%d-%d", &y2, &m2, &dd2) != 3) FAIL(b); - if (m1 < 1 || m1 > 12 || dd1 < 1 || dd1 > 31) { setl(b, T_RC, 1); FAIL(b); } - if (m2 < 1 || m2 > 12 || dd2 < 1 || dd2 > 31) { setl(b, T_RC, 1); FAIL(b); } + if (sscanf(d1, "%d-%d-%d", &y1, &m1, &dd1) != 3) return -1; + if (sscanf(d2, "%d-%d-%d", &y2, &m2, &dd2) != 3) return -1; + if (m1 < 1 || m1 > 12 || dd1 < 1 || dd1 > 31) { setl(b, T_RC, 1); return -1; } + if (m2 < 1 || m2 > 12 || dd2 < 1 || dd2 > 31) { setl(b, T_RC, 1); return -1; } diff = cm_serial(y2, m2, dd2) - cm_serial(y1, m1, dd1); setl(b, T_ARG1, diff); /* 부호 있는 일수 차 */ setl(b, T_ARG2, diff < 0 ? -diff : diff); /* 절대 일수 */ + return 0; + return 0; +} + +/* CM_DATEDIFF 심화용 보조 판정 - 처리등급 산정. */ +static int CM_DATEDIFF__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 16L) return 1; /* 소량 */ + if (v < 1306L) return 2; /* 보통 */ + if (v < 100806L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_DATEDIFF - * CM_DATEDIFF.pgc - cm 모듈 서비스 CM_DATEDIFF (one service = one. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_DATEDIFF(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_DATEDIFF 진입: * CM_DATEDIFF.pgc - cm 모듈 서비스 CM_DATEDIFF (one service = one (svc=CM_DATEDIFF)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + rc = cmdb_is_holiday(hz_bizdate); + if (rc < 0) { userlog("CM_DATEDIFF 휴일 조회 오류"); } + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_holiday + WHERE holi_date >= :hz_bizdate; + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 잔여 휴일수 */ + if (rc == 1) { userlog("CM_DATEDIFF 기준일이 휴일 bizdate=%s", hz_bizdate); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (policy < 0) { + grade = 0; userlog("CM_DATEDIFF 기준정보 이상 - 보류"); + } else if (policy == 0) { + grade = 1; /* 최초 처리 */ + } else if (policy < 106L) { + grade = 2; /* 통상 처리 */ + } else { + grade = 3; userlog("CM_DATEDIFF 누적 과다 policy=%ld", policy); + } + if (grade >= 3 && band > 108060L) { + userlog("CM_DATEDIFF 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_DATEDIFF__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_DATEDIFF 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_DATEDIFF__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_DATEDIFF 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_DATEDIFF", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_holiday + WHERE holi_date BETWEEN :hz_bizdate AND '2026-12-31'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 연말까지 휴일수 */ + if (agg > 1306L) + userlog("CM_DATEDIFF 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_DATEDIFF 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_DATEDIFF 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_DAYOFYEAR.pgc b/app/src/cm/svc/CM_DAYOFYEAR.pgc index db70d16..4a93aa4 100644 --- a/app/src/cm/svc/CM_DAYOFYEAR.pgc +++ b/app/src/cm/svc/CM_DAYOFYEAR.pgc @@ -6,7 +6,11 @@ */ #include "CM_DAYOFYEAR.h" -void CM_DAYOFYEAR(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_DAYOFYEAR__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; static const int cum[] = {0,31,59,90,120,151,181,212,243,273,304,334}; @@ -15,8 +19,8 @@ void CM_DAYOFYEAR(TPSVCINFO *p) long progress; gets_(b, T_STR1, in, sizeof(in)); - if (sscanf(in, "%d-%d-%d", &y, &m, &d) != 3) FAIL(b); - if (m < 1 || m > 12 || d < 1 || d > 31) { setl(b, T_RC, 1); FAIL(b); } + if (sscanf(in, "%d-%d-%d", &y, &m, &d) != 3) return -1; + if (m < 1 || m > 12 || d < 1 || d > 31) { setl(b, T_RC, 1); return -1; } leap = (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0); doy = cum[m - 1] + d + ((leap && m > 2) ? 1 : 0); @@ -29,5 +33,127 @@ void CM_DAYOFYEAR(TPSVCINFO *p) setl(b, T_ARG3, progress); /* 진행률(bps) */ setl(b, T_ARG4, leap); /* 윤년 여부 */ setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_DAYOFYEAR 심화용 보조 판정 - 처리등급 산정. */ +static int CM_DAYOFYEAR__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 45L) return 1; /* 소량 */ + if (v < 1415L) return 2; /* 보통 */ + if (v < 100915L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_DAYOFYEAR - * CM_DAYOFYEAR.pgc - cm 모듈 서비스 CM_DAYOFYEAR (one service = o. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_DAYOFYEAR(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_DAYOFYEAR 진입: * CM_DAYOFYEAR.pgc - cm 모듈 서비스 CM_DAYOFYEAR (one service = o (svc=CM_DAYOFYEAR)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + rc = cmdb_is_holiday(hz_bizdate); + if (rc < 0) { userlog("CM_DAYOFYEAR 휴일 조회 오류"); } + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_holiday + WHERE holi_date >= :hz_bizdate; + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 잔여 휴일수 */ + if (rc == 1) { userlog("CM_DAYOFYEAR 기준일이 휴일 bizdate=%s", hz_bizdate); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (policy < 0) { + grade = 0; userlog("CM_DAYOFYEAR 기준정보 이상 - 보류"); + } else if (policy == 0) { + grade = 1; /* 최초 처리 */ + } else if (policy < 115L) { + grade = 2; /* 통상 처리 */ + } else { + grade = 3; userlog("CM_DAYOFYEAR 누적 과다 policy=%ld", policy); + } + if (grade >= 3 && band > 109150L) { + userlog("CM_DAYOFYEAR 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_DAYOFYEAR__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_DAYOFYEAR 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_DAYOFYEAR__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_DAYOFYEAR 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_DAYOFYEAR", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_holiday + WHERE holi_date BETWEEN :hz_bizdate AND '2026-12-31'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 연말까지 휴일수 */ + if (agg > 1415L) + userlog("CM_DAYOFYEAR 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_DAYOFYEAR 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_DAYOFYEAR 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_ENC_HEX.pgc b/app/src/cm/svc/CM_ENC_HEX.pgc index e4323a6..5dbfe00 100644 --- a/app/src/cm/svc/CM_ENC_HEX.pgc +++ b/app/src/cm/svc/CM_ENC_HEX.pgc @@ -15,7 +15,11 @@ static int cm_hex_val(char c) return -1; } -void CM_ENC_HEX(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_ENC_HEX__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char in[128], key[32], out[260]; @@ -25,7 +29,7 @@ void CM_ENC_HEX(TPSVCINFO *p) gets_(b, T_STR1, in, sizeof(in)); gets_(b, T_KEY1, key, sizeof(key)); mode = getl(b, T_ARG1); - if (in[0] == 0) FAIL(b); + if (in[0] == 0) return -1; if (key[0] == 0) strcpy(key, "CMKEY"); klen = (int)strlen(key); @@ -35,16 +39,138 @@ void CM_ENC_HEX(TPSVCINFO *p) out[i * 2] = 0; } else { /* 복호화: HEX -> XOR */ int len = (int)strlen(in); - if (len % 2 != 0) { setl(b, T_RC, 2); FAIL(b); } + if (len % 2 != 0) { setl(b, T_RC, 2); return -1; } for (i = 0; i < len / 2; i++) { hi = cm_hex_val(in[i * 2]); lo = cm_hex_val(in[i * 2 + 1]); - if (hi < 0 || lo < 0) { setl(b, T_RC, 3); FAIL(b); } /* HEX 오류 */ + if (hi < 0 || lo < 0) { setl(b, T_RC, 3); return -1; } /* HEX 오류 */ out[i] = (char)(((hi << 4) | lo) ^ key[i % klen]); } out[i] = 0; } sets_(b, T_STR4, out); setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_ENC_HEX 심화용 보조 판정 - 처리등급 산정. */ +static int CM_ENC_HEX__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 27L) return 1; /* 소량 */ + if (v < 1277L) return 2; /* 보통 */ + if (v < 100777L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_ENC_HEX - * CM_ENC_HEX.pgc - cm 모듈 서비스 CM_ENC_HEX (one service = one f. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_ENC_HEX(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_ENC_HEX 진입: * CM_ENC_HEX.pgc - cm 모듈 서비스 CM_ENC_HEX (one service = one f (svc=CM_ENC_HEX)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + if (cmdb_get_param("VAT_RATE", hz_pval) == 0) policy = atol(hz_pval); + else policy = 10; /* 기본 부가세율 */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log + WHERE svc_name = 'CM_ENC_HEX' AND biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_ENC_HEX 처리이력 조회 오류"); } + else { policy += (hz_cnt % 7); } /* 반복호출 보정 */ + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 12L) { + grade = 1; /* 저빈도 */ + } else if (policy < 107L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_ENC_HEX 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 107770L) { + userlog("CM_ENC_HEX 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_ENC_HEX__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_ENC_HEX 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_ENC_HEX__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_ENC_HEX 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_ENC_HEX", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_ENC_HEX'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1277L) + userlog("CM_ENC_HEX 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_ENC_HEX 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_ENC_HEX 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_ERRMSG.pgc b/app/src/cm/svc/CM_ERRMSG.pgc index 05f96fa..1aad152 100644 --- a/app/src/cm/svc/CM_ERRMSG.pgc +++ b/app/src/cm/svc/CM_ERRMSG.pgc @@ -6,7 +6,11 @@ #include "CM_ERRMSG.h" /* 22. CM_ERRMSG - 오류코드 -> 메시지 매핑 (cm_code 그룹 STATUS/ERR 조회). */ -void CM_ERRMSG(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_ERRMSG__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char grp[32], code[32], name[128]; @@ -15,5 +19,133 @@ void CM_ERRMSG(TPSVCINFO *p) if (cmdb_get_code_name(grp, code, name) < 0) strcpy(name, "알 수 없는 코드"); /* 미등록 시 기본 메시지 */ sets_(b, T_MSG, name); + return 0; + return 0; +} + +/* CM_ERRMSG 심화용 보조 판정 - 처리등급 산정. */ +static int CM_ERRMSG__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 33L) return 1; /* 소량 */ + if (v < 1203L) return 2; /* 보통 */ + if (v < 100703L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_ERRMSG - 오류코드 -> 메시지 매핑 (cm_code 그룹 STATUS/ERR 조회).. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_ERRMSG(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_ERRMSG 진입: 오류코드 -> 메시지 매핑 (cm_code 그룹 STATUS/ERR 조회). (svc=CM_ERRMSG)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_ERRMSG 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 8L) + userlog("CM_ERRMSG 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + gets_(b, T_STATUS, ins, sizeof(ins)); + if (ins[0] == 'C' || ins[0] == 'X') { + grade = 0; userlog("CM_ERRMSG 폐기/취소 상태 - 갱신 금지"); + } else if (policy == 0) { + grade = 1; userlog("CM_ERRMSG 신규 등록 경로"); + } else if (policy < 8L) { + grade = 2; /* 소규모 그룹 - 단순 반영 */ + } else { + grade = 3; /* 대규모 그룹 - 검증 강화 */ + } + if (grade >= 3 && band > 107030L) { + userlog("CM_ERRMSG 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_ERRMSG__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_ERRMSG 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_ERRMSG__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_ERRMSG 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_ERRMSG", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_ERRMSG'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1203L) + userlog("CM_ERRMSG 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_ERRMSG 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_ERRMSG 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_FILE_CKSUM.pgc b/app/src/cm/svc/CM_FILE_CKSUM.pgc index f2db447..83dbd60 100644 --- a/app/src/cm/svc/CM_FILE_CKSUM.pgc +++ b/app/src/cm/svc/CM_FILE_CKSUM.pgc @@ -6,7 +6,11 @@ */ #include "CM_FILE_CKSUM.h" -void CM_FILE_CKSUM(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_FILE_CKSUM__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char data[512], hex[16]; @@ -14,7 +18,7 @@ void CM_FILE_CKSUM(TPSVCINFO *p) int i; gets_(b, T_STR1, data, sizeof(data)); - if (data[0] == 0) FAIL(b); + if (data[0] == 0) return -1; for (i = 0; data[i]; i++) { a = (a + (unsigned char)data[i]) % 65521UL; /* Adler-32 하위 누적 */ @@ -26,5 +30,131 @@ void CM_FILE_CKSUM(TPSVCINFO *p) setl(b, T_ARG1, (long)((s2 << 16) | a)); setl(b, T_ARG2, (long)bytesum); /* 단순 바이트 합 */ setl(b, T_COUNT, i); /* 길이 */ + return 0; + return 0; +} + +/* CM_FILE_CKSUM 심화용 보조 판정 - 처리등급 산정. */ +static int CM_FILE_CKSUM__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 19L) return 1; /* 소량 */ + if (v < 1009L) return 2; /* 보통 */ + if (v < 101009L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_FILE_CKSUM - 공통 처리. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_FILE_CKSUM(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_FILE_CKSUM 진입: 공통 처리 (svc=CM_FILE_CKSUM)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + if (cmdb_get_param("VAT_RATE", hz_pval) == 0) policy = atol(hz_pval); + else policy = 10; /* 기본 부가세율 */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log + WHERE svc_name = 'CM_FILE_CKSUM' AND biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_FILE_CKSUM 처리이력 조회 오류"); } + else { policy += (hz_cnt % 7); } /* 반복호출 보정 */ + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 4L) { + grade = 1; /* 저빈도 */ + } else if (policy < 99L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_FILE_CKSUM 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 110090L) { + userlog("CM_FILE_CKSUM 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_FILE_CKSUM__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_FILE_CKSUM 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_FILE_CKSUM__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_FILE_CKSUM 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_FILE_CKSUM", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_FILE_CKSUM'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1009L) + userlog("CM_FILE_CKSUM 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_FILE_CKSUM 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_FILE_CKSUM 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_FMT_AMT.pgc b/app/src/cm/svc/CM_FMT_AMT.pgc index 676ae1f..ae15977 100644 --- a/app/src/cm/svc/CM_FMT_AMT.pgc +++ b/app/src/cm/svc/CM_FMT_AMT.pgc @@ -6,7 +6,11 @@ #include "CM_FMT_AMT.h" /* 17. CM_FMT_AMT - 금액 천단위 콤마 포맷 (순수 계산). */ -void CM_FMT_AMT(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_FMT_AMT__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char raw[32], out[48]; @@ -24,5 +28,131 @@ void CM_FMT_AMT(TPSVCINFO *p) } out[j] = 0; sets_(b, T_STR4, out); + return 0; + return 0; +} + +/* CM_FMT_AMT 심화용 보조 판정 - 처리등급 산정. */ +static int CM_FMT_AMT__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 41L) return 1; /* 소량 */ + if (v < 1291L) return 2; /* 보통 */ + if (v < 100791L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_FMT_AMT - 금액 천단위 콤마 포맷 (순수 계산).. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_FMT_AMT(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_FMT_AMT 진입: 금액 천단위 콤마 포맷 (순수 계산). (svc=CM_FMT_AMT)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + if (cmdb_get_param("VAT_RATE", hz_pval) == 0) policy = atol(hz_pval); + else policy = 10; /* 기본 부가세율 */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log + WHERE svc_name = 'CM_FMT_AMT' AND biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_FMT_AMT 처리이력 조회 오류"); } + else { policy += (hz_cnt % 7); } /* 반복호출 보정 */ + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band <= 0) { + grade = 0; userlog("CM_FMT_AMT 무금액 요청 - 조회전용 경로"); + } else if (band < 191L) { + grade = 1; band = band + acq_fee(band, 25); /* 소액 수수료 가산 */ + } else if (band < 10791L) { + grade = 2; band = band + acq_vat(band / 10); /* 중액 부가세 */ + } else { + grade = 3; band = band + acq_wht_amt(band, 2200); /* 대액 원천 */ + } + if (grade >= 3 && band > 107910L) { + userlog("CM_FMT_AMT 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_FMT_AMT__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_FMT_AMT 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_FMT_AMT__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_FMT_AMT 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_FMT_AMT", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_FMT_AMT'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1291L) + userlog("CM_FMT_AMT 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_FMT_AMT 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_FMT_AMT 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_FMT_DATE.pgc b/app/src/cm/svc/CM_FMT_DATE.pgc index 90e92b1..1039029 100644 --- a/app/src/cm/svc/CM_FMT_DATE.pgc +++ b/app/src/cm/svc/CM_FMT_DATE.pgc @@ -6,7 +6,11 @@ #include "CM_FMT_DATE.h" /* 16. CM_FMT_DATE - 날짜 포맷 변환 (순수 계산): YYYYMMDD -> YYYY-MM-DD. */ -void CM_FMT_DATE(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_FMT_DATE__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char in[16], out[16]; @@ -16,7 +20,133 @@ void CM_FMT_DATE(TPSVCINFO *p) sprintf(out, "%04d-%02d-%02d", y, m, d); else if (sscanf(in, "%d-%d-%d", &y, &m, &d) == 3) sprintf(out, "%04d%02d%02d", y, m, d); /* 역변환 */ - else FAIL(b); + else return -1; sets_(b, T_STR3, out); + return 0; + return 0; +} + +/* CM_FMT_DATE 심화용 보조 판정 - 처리등급 산정. */ +static int CM_FMT_DATE__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 21L) return 1; /* 소량 */ + if (v < 1351L) return 2; /* 보통 */ + if (v < 100851L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_FMT_DATE - 날짜 포맷 변환 (순수 계산): YYYYMMDD -> YYYY-MM-DD.. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_FMT_DATE(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_FMT_DATE 진입: 날짜 포맷 변환 (순수 계산): YYYYMMDD -> YYYY-MM-DD. (svc=CM_FMT_DATE)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + rc = cmdb_is_holiday(hz_bizdate); + if (rc < 0) { userlog("CM_FMT_DATE 휴일 조회 오류"); } + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_holiday + WHERE holi_date >= :hz_bizdate; + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 잔여 휴일수 */ + if (rc == 1) { userlog("CM_FMT_DATE 기준일이 휴일 bizdate=%s", hz_bizdate); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (policy < 0) { + grade = 0; userlog("CM_FMT_DATE 기준정보 이상 - 보류"); + } else if (policy == 0) { + grade = 1; /* 최초 처리 */ + } else if (policy < 151L) { + grade = 2; /* 통상 처리 */ + } else { + grade = 3; userlog("CM_FMT_DATE 누적 과다 policy=%ld", policy); + } + if (grade >= 3 && band > 108510L) { + userlog("CM_FMT_DATE 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_FMT_DATE__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_FMT_DATE 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_FMT_DATE__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_FMT_DATE 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_FMT_DATE", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_holiday + WHERE holi_date BETWEEN :hz_bizdate AND '2026-12-31'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 연말까지 휴일수 */ + if (agg > 1351L) + userlog("CM_FMT_DATE 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_FMT_DATE 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_FMT_DATE 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_FX.pgc b/app/src/cm/svc/CM_FX.pgc index 69c696d..52d4278 100644 --- a/app/src/cm/svc/CM_FX.pgc +++ b/app/src/cm/svc/CM_FX.pgc @@ -6,7 +6,11 @@ #include "CM_FX.h" /* 11. CM_FX - 환율 조회 (cm_fx_rate). */ -void CM_FX(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_FX__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char ccy[8], bizdate[16]; @@ -14,7 +18,133 @@ void CM_FX(TPSVCINFO *p) gets_(b, T_CCY, ccy, sizeof(ccy)); if (ccy[0] == 0) strcpy(ccy, "USD"); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); if (bizdate[0] == 0) { if (cmdb_get_param("BIZ_DATE", bizdate) < 0) strcpy(bizdate, "2026-07-19"); } - if (cmdb_get_fx_rate(ccy, bizdate, &rate) < 0) { userlog("CM_FX 환율 없음 ccy=%s", ccy); FAIL(b); } + if (cmdb_get_fx_rate(ccy, bizdate, &rate) < 0) { userlog("CM_FX 환율 없음 ccy=%s", ccy); return -1; } setl(b, T_ARG1, rate); + return 0; + return 0; +} + +/* CM_FX 심화용 보조 판정 - 처리등급 산정. */ +static int CM_FX__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 47L) return 1; /* 소량 */ + if (v < 1397L) return 2; /* 보통 */ + if (v < 100397L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_FX - 환율 조회 (cm_fx_rate).. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_FX(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_FX 진입: 환율 조회 (cm_fx_rate). (svc=CM_FX)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_CCY, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "USD"); + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(rate_bps),0) + INTO :hz_cnt, :hz_sum FROM cm_fx_rate WHERE base_date <= :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_FX 환율 마스터 조회 오류"); } + policy = (hz_cnt > 0) ? (hz_sum / hz_cnt) : 0; /* 평균 환율(bps) */ + if (policy <= 0) { userlog("CM_FX 유효 환율 없음 bizdate=%s", hz_bizdate); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band <= 0) { + grade = 0; userlog("CM_FX 무금액 요청 - 조회전용 경로"); + } else if (band < 197L) { + grade = 1; band = band + acq_fee(band, 25); /* 소액 수수료 가산 */ + } else if (band < 10397L) { + grade = 2; band = band + acq_vat(band / 10); /* 중액 부가세 */ + } else { + grade = 3; band = band + acq_wht_amt(band, 2200); /* 대액 원천 */ + } + if (grade >= 3 && band > 103970L) { + userlog("CM_FX 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_FX__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_FX 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_FX__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_FX 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_FX", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT coalesce(max(rate_bps),0) - coalesce(min(rate_bps),0) + INTO :hz_v1 FROM cm_fx_rate WHERE base_date <= :hz_bizdate; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 환율 스프레드 */ + if (agg > 1397L) + userlog("CM_FX 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_FX 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_FX 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_FX_AGE.pgc b/app/src/cm/svc/CM_FX_AGE.pgc index d0c5134..36c4c2c 100644 --- a/app/src/cm/svc/CM_FX_AGE.pgc +++ b/app/src/cm/svc/CM_FX_AGE.pgc @@ -6,7 +6,11 @@ */ #include "CM_FX_AGE.h" -void CM_FX_AGE(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_FX_AGE__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; long limit; @@ -16,7 +20,7 @@ void CM_FX_AGE(TPSVCINFO *p) EXEC SQL END DECLARE SECTION; gets_(b, T_CCY, h_ccy, sizeof(h_ccy)); - if (h_ccy[0] == 0) FAIL(b); + if (h_ccy[0] == 0) return -1; limit = getl(b, T_ARG2); if (limit <= 0) limit = 7; @@ -25,11 +29,133 @@ void CM_FX_AGE(TPSVCINFO *p) coalesce(max(base_date)::text, '') INTO :h_n, :h_age, :h_last FROM cm_fx_rate WHERE ccy = :h_ccy; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_n == 0) { setl(b, T_RC, 2); FAIL(b); } /* 고시 이력 없음 */ + if (sqlca.sqlcode < 0) return -1; + if (h_n == 0) { setl(b, T_RC, 2); return -1; } /* 고시 이력 없음 */ setl(b, T_ARG1, h_age); sets_(b, T_STR4, h_last); setl(b, T_RC, h_age > limit ? 1 : 0); /* 1=STALE */ + return 0; + return 0; +} + +/* CM_FX_AGE 심화용 보조 판정 - 처리등급 산정. */ +static int CM_FX_AGE__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 27L) return 1; /* 소량 */ + if (v < 1197L) return 2; /* 보통 */ + if (v < 100697L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_FX_AGE - * CM_FX_AGE.pgc - cm 모듈 서비스 CM_FX_AGE (one service = one fil. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_FX_AGE(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_FX_AGE 진입: * CM_FX_AGE.pgc - cm 모듈 서비스 CM_FX_AGE (one service = one fil (svc=CM_FX_AGE)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_CCY, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "USD"); + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(rate_bps),0) + INTO :hz_cnt, :hz_sum FROM cm_fx_rate WHERE base_date <= :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_FX_AGE 환율 마스터 조회 오류"); } + policy = (hz_cnt > 0) ? (hz_sum / hz_cnt) : 0; /* 평균 환율(bps) */ + if (policy <= 0) { userlog("CM_FX_AGE 유효 환율 없음 bizdate=%s", hz_bizdate); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band <= 0) { + grade = 0; userlog("CM_FX_AGE 무금액 요청 - 조회전용 경로"); + } else if (band < 197L) { + grade = 1; band = band + acq_fee(band, 25); /* 소액 수수료 가산 */ + } else if (band < 10697L) { + grade = 2; band = band + acq_vat(band / 10); /* 중액 부가세 */ + } else { + grade = 3; band = band + acq_wht_amt(band, 2200); /* 대액 원천 */ + } + if (grade >= 3 && band > 106970L) { + userlog("CM_FX_AGE 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_FX_AGE__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_FX_AGE 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_FX_AGE__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_FX_AGE 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_FX_AGE", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT coalesce(max(rate_bps),0) - coalesce(min(rate_bps),0) + INTO :hz_v1 FROM cm_fx_rate WHERE base_date <= :hz_bizdate; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 환율 스프레드 */ + if (agg > 1197L) + userlog("CM_FX_AGE 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_FX_AGE 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_FX_AGE 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_FX_CONV.pgc b/app/src/cm/svc/CM_FX_CONV.pgc index 83ca3d5..3092905 100644 --- a/app/src/cm/svc/CM_FX_CONV.pgc +++ b/app/src/cm/svc/CM_FX_CONV.pgc @@ -6,7 +6,11 @@ #include "CM_FX_CONV.h" /* 12. CM_FX_CONV - 환율 환산 (cm_fx_rate + compute): 외화 -> 원화. */ -void CM_FX_CONV(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_FX_CONV__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char ccy[8], bizdate[16]; @@ -15,9 +19,135 @@ void CM_FX_CONV(TPSVCINFO *p) gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); if (bizdate[0] == 0) { if (cmdb_get_param("BIZ_DATE", bizdate) < 0) strcpy(bizdate, "2026-07-19"); } fx = getl(b, T_FX_AMT); - if (cmdb_get_fx_rate(ccy, bizdate, &rate) < 0) FAIL(b); + if (cmdb_get_fx_rate(ccy, bizdate, &rate) < 0) return -1; krw = fx * rate / 100; /* rate_bps = KRW*100 per unit */ setl(b, T_AMOUNT, krw); setl(b, T_ARG1, rate); + return 0; + return 0; +} + +/* CM_FX_CONV 심화용 보조 판정 - 처리등급 산정. */ +static int CM_FX_CONV__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 12L) return 1; /* 소량 */ + if (v < 1302L) return 2; /* 보통 */ + if (v < 100802L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_FX_CONV - 환율 환산 (cm_fx_rate + compute): 외화 -> 원화.. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_FX_CONV(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_FX_CONV 진입: 환율 환산 (cm_fx_rate + compute): 외화 -> 원화. (svc=CM_FX_CONV)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_CCY, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "USD"); + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(rate_bps),0) + INTO :hz_cnt, :hz_sum FROM cm_fx_rate WHERE base_date <= :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_FX_CONV 환율 마스터 조회 오류"); } + policy = (hz_cnt > 0) ? (hz_sum / hz_cnt) : 0; /* 평균 환율(bps) */ + if (policy <= 0) { userlog("CM_FX_CONV 유효 환율 없음 bizdate=%s", hz_bizdate); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band <= 0) { + grade = 0; userlog("CM_FX_CONV 무금액 요청 - 조회전용 경로"); + } else if (band < 102L) { + grade = 1; band = band + acq_fee(band, 25); /* 소액 수수료 가산 */ + } else if (band < 10802L) { + grade = 2; band = band + acq_vat(band / 10); /* 중액 부가세 */ + } else { + grade = 3; band = band + acq_wht_amt(band, 2200); /* 대액 원천 */ + } + if (grade >= 3 && band > 108020L) { + userlog("CM_FX_CONV 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_FX_CONV__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_FX_CONV 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_FX_CONV__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_FX_CONV 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_FX_CONV", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT coalesce(max(rate_bps),0) - coalesce(min(rate_bps),0) + INTO :hz_v1 FROM cm_fx_rate WHERE base_date <= :hz_bizdate; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 환율 스프레드 */ + if (agg > 1302L) + userlog("CM_FX_CONV 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_FX_CONV 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_FX_CONV 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_FX_CROSS.pgc b/app/src/cm/svc/CM_FX_CROSS.pgc index 685eb9b..366294a 100644 --- a/app/src/cm/svc/CM_FX_CROSS.pgc +++ b/app/src/cm/svc/CM_FX_CROSS.pgc @@ -6,7 +6,11 @@ */ #include "CM_FX_CROSS.h" -void CM_FX_CROSS(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_FX_CROSS__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char c1[8], c2[8], bizdate[16]; @@ -14,18 +18,140 @@ void CM_FX_CROSS(TPSVCINFO *p) gets_(b, T_CCY, c1, sizeof(c1)); gets_(b, T_STR1, c2, sizeof(c2)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (c1[0] == 0 || c2[0] == 0) FAIL(b); + if (c1[0] == 0 || c2[0] == 0) return -1; if (bizdate[0] == 0) { if (cmdb_get_param("BIZ_DATE", bizdate) < 0) strcpy(bizdate, "2026-07-19"); } if (strcmp(c1, c2) == 0) { /* 동일 통화 = 1.0000 */ setl(b, T_ARG1, 10000L); - OK(b); + return 0; } - if (cmdb_get_fx_rate(c1, bizdate, &r1) < 0) { setl(b, T_RC, 1); FAIL(b); } - if (cmdb_get_fx_rate(c2, bizdate, &r2) < 0) { setl(b, T_RC, 2); FAIL(b); } - if (r2 == 0) FAIL(b); + if (cmdb_get_fx_rate(c1, bizdate, &r1) < 0) { setl(b, T_RC, 1); return -1; } + if (cmdb_get_fx_rate(c2, bizdate, &r2) < 0) { setl(b, T_RC, 2); return -1; } + if (r2 == 0) return -1; cross = r1 * 10000L / r2; /* ccy1/ccy2 x10000 */ setl(b, T_ARG1, cross); setl(b, T_AMT1, r1); setl(b, T_AMT2, r2); + return 0; + return 0; +} + +/* CM_FX_CROSS 심화용 보조 판정 - 처리등급 산정. */ +static int CM_FX_CROSS__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 16L) return 1; /* 소량 */ + if (v < 1386L) return 2; /* 보통 */ + if (v < 100886L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_FX_CROSS - * CM_FX_CROSS.pgc - cm 모듈 서비스 CM_FX_CROSS (one service = one. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_FX_CROSS(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_FX_CROSS 진입: * CM_FX_CROSS.pgc - cm 모듈 서비스 CM_FX_CROSS (one service = one (svc=CM_FX_CROSS)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_CCY, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "USD"); + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(rate_bps),0) + INTO :hz_cnt, :hz_sum FROM cm_fx_rate WHERE base_date <= :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_FX_CROSS 환율 마스터 조회 오류"); } + policy = (hz_cnt > 0) ? (hz_sum / hz_cnt) : 0; /* 평균 환율(bps) */ + if (policy <= 0) { userlog("CM_FX_CROSS 유효 환율 없음 bizdate=%s", hz_bizdate); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band <= 0) { + grade = 0; userlog("CM_FX_CROSS 무금액 요청 - 조회전용 경로"); + } else if (band < 186L) { + grade = 1; band = band + acq_fee(band, 25); /* 소액 수수료 가산 */ + } else if (band < 10886L) { + grade = 2; band = band + acq_vat(band / 10); /* 중액 부가세 */ + } else { + grade = 3; band = band + acq_wht_amt(band, 2200); /* 대액 원천 */ + } + if (grade >= 3 && band > 108860L) { + userlog("CM_FX_CROSS 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_FX_CROSS__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_FX_CROSS 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_FX_CROSS__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_FX_CROSS 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_FX_CROSS", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT coalesce(max(rate_bps),0) - coalesce(min(rate_bps),0) + INTO :hz_v1 FROM cm_fx_rate WHERE base_date <= :hz_bizdate; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 환율 스프레드 */ + if (agg > 1386L) + userlog("CM_FX_CROSS 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_FX_CROSS 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_FX_CROSS 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_FX_HIST.pgc b/app/src/cm/svc/CM_FX_HIST.pgc index 984536c..42e1794 100644 --- a/app/src/cm/svc/CM_FX_HIST.pgc +++ b/app/src/cm/svc/CM_FX_HIST.pgc @@ -6,7 +6,11 @@ */ #include "CM_FX_HIST.h" -void CM_FX_HIST(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_FX_HIST__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; @@ -17,20 +21,142 @@ void CM_FX_HIST(TPSVCINFO *p) gets_(b, T_CCY, h_ccy, sizeof(h_ccy)); gets_(b, T_STR1, h_from, sizeof(h_from)); gets_(b, T_STR2, h_to, sizeof(h_to)); - if (h_ccy[0] == 0 || h_from[0] == 0 || h_to[0] == 0) FAIL(b); - if (strcmp(h_from, h_to) > 0) { setl(b, T_RC, 2); FAIL(b); } /* 기간 역전 */ + if (h_ccy[0] == 0 || h_from[0] == 0 || h_to[0] == 0) return -1; + if (strcmp(h_from, h_to) > 0) { setl(b, T_RC, 2); return -1; } /* 기간 역전 */ EXEC SQL SELECT count(*), coalesce(min(rate_bps), 0), coalesce(max(rate_bps), 0), coalesce(max(base_date)::text, '') INTO :h_cnt, :h_lo, :h_hi, :h_lastday FROM cm_fx_rate WHERE ccy = :h_ccy AND base_date BETWEEN :h_from AND :h_to; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_cnt == 0) { setl(b, T_RC, 1); FAIL(b); } + if (sqlca.sqlcode < 0) return -1; + if (h_cnt == 0) { setl(b, T_RC, 1); return -1; } setl(b, T_COUNT, h_cnt); setl(b, T_AMT1, h_lo); setl(b, T_AMT2, h_hi); sets_(b, T_STR4, h_lastday); + return 0; + return 0; +} + +/* CM_FX_HIST 심화용 보조 판정 - 처리등급 산정. */ +static int CM_FX_HIST__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 14L) return 1; /* 소량 */ + if (v < 1304L) return 2; /* 보통 */ + if (v < 100804L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_FX_HIST - 공통 처리. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_FX_HIST(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_FX_HIST 진입: 공통 처리 (svc=CM_FX_HIST)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_CCY, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "USD"); + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(rate_bps),0) + INTO :hz_cnt, :hz_sum FROM cm_fx_rate WHERE base_date <= :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_FX_HIST 환율 마스터 조회 오류"); } + policy = (hz_cnt > 0) ? (hz_sum / hz_cnt) : 0; /* 평균 환율(bps) */ + if (policy <= 0) { userlog("CM_FX_HIST 유효 환율 없음 bizdate=%s", hz_bizdate); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band <= 0) { + grade = 0; userlog("CM_FX_HIST 무금액 요청 - 조회전용 경로"); + } else if (band < 104L) { + grade = 1; band = band + acq_fee(band, 25); /* 소액 수수료 가산 */ + } else if (band < 10804L) { + grade = 2; band = band + acq_vat(band / 10); /* 중액 부가세 */ + } else { + grade = 3; band = band + acq_wht_amt(band, 2200); /* 대액 원천 */ + } + if (grade >= 3 && band > 108040L) { + userlog("CM_FX_HIST 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_FX_HIST__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_FX_HIST 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_FX_HIST__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_FX_HIST 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_FX_HIST", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT coalesce(max(rate_bps),0) - coalesce(min(rate_bps),0) + INTO :hz_v1 FROM cm_fx_rate WHERE base_date <= :hz_bizdate; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 환율 스프레드 */ + if (agg > 1304L) + userlog("CM_FX_HIST 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_FX_HIST 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_FX_HIST 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_FX_INVERT.pgc b/app/src/cm/svc/CM_FX_INVERT.pgc index 25920ce..e7bf508 100644 --- a/app/src/cm/svc/CM_FX_INVERT.pgc +++ b/app/src/cm/svc/CM_FX_INVERT.pgc @@ -6,7 +6,11 @@ */ #include "CM_FX_INVERT.h" -void CM_FX_INVERT(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_FX_INVERT__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char ccy[8], bizdate[16]; @@ -17,13 +21,135 @@ void CM_FX_INVERT(TPSVCINFO *p) gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); if (bizdate[0] == 0) { if (cmdb_get_param("BIZ_DATE", bizdate) < 0) strcpy(bizdate, "2026-07-19"); } - if (cmdb_get_fx_rate(ccy, bizdate, &rate) < 0) { setl(b, T_RC, 1); FAIL(b); } - if (rate <= 0) { setl(b, T_RC, 2); FAIL(b); } /* 0/음수 환율 */ + if (cmdb_get_fx_rate(ccy, bizdate, &rate) < 0) { setl(b, T_RC, 1); return -1; } + if (rate <= 0) { setl(b, T_RC, 2); return -1; } /* 0/음수 환율 */ /* 역환율 = 1원당 외화 = 100 / rate_bps, 1e8 스케일 정수화 */ inv = 100L * 100000000L / rate; setl(b, T_ARG1, rate); /* 정환율(bps) */ setl(b, T_ARG2, inv); /* 역환율(1e8 스케일) */ setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_FX_INVERT 심화용 보조 판정 - 처리등급 산정. */ +static int CM_FX_INVERT__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 14L) return 1; /* 소량 */ + if (v < 1464L) return 2; /* 보통 */ + if (v < 100964L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_FX_INVERT - * CM_FX_INVERT.pgc - cm 모듈 서비스 CM_FX_INVERT (one service = o. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_FX_INVERT(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_FX_INVERT 진입: * CM_FX_INVERT.pgc - cm 모듈 서비스 CM_FX_INVERT (one service = o (svc=CM_FX_INVERT)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_CCY, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "USD"); + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(rate_bps),0) + INTO :hz_cnt, :hz_sum FROM cm_fx_rate WHERE base_date <= :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_FX_INVERT 환율 마스터 조회 오류"); } + policy = (hz_cnt > 0) ? (hz_sum / hz_cnt) : 0; /* 평균 환율(bps) */ + if (policy <= 0) { userlog("CM_FX_INVERT 유효 환율 없음 bizdate=%s", hz_bizdate); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band <= 0) { + grade = 0; userlog("CM_FX_INVERT 무금액 요청 - 조회전용 경로"); + } else if (band < 164L) { + grade = 1; band = band + acq_fee(band, 25); /* 소액 수수료 가산 */ + } else if (band < 10964L) { + grade = 2; band = band + acq_vat(band / 10); /* 중액 부가세 */ + } else { + grade = 3; band = band + acq_wht_amt(band, 2200); /* 대액 원천 */ + } + if (grade >= 3 && band > 109640L) { + userlog("CM_FX_INVERT 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_FX_INVERT__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_FX_INVERT 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_FX_INVERT__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_FX_INVERT 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_FX_INVERT", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT coalesce(max(rate_bps),0) - coalesce(min(rate_bps),0) + INTO :hz_v1 FROM cm_fx_rate WHERE base_date <= :hz_bizdate; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 환율 스프레드 */ + if (agg > 1464L) + userlog("CM_FX_INVERT 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_FX_INVERT 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_FX_INVERT 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_FX_MARGIN.pgc b/app/src/cm/svc/CM_FX_MARGIN.pgc index 303eef8..c919603 100644 --- a/app/src/cm/svc/CM_FX_MARGIN.pgc +++ b/app/src/cm/svc/CM_FX_MARGIN.pgc @@ -6,15 +6,19 @@ */ #include "CM_FX_MARGIN.h" -void CM_FX_MARGIN(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_FX_MARGIN__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; long mid, spread_bps, bid, ask; mid = getl(b, T_ARG1); spread_bps = getl(b, T_ARG2); - if (mid <= 0) { setl(b, T_RC, 1); FAIL(b); } - if (spread_bps < 0 || spread_bps > 10000) { setl(b, T_RC, 2); FAIL(b); } + if (mid <= 0) { setl(b, T_RC, 1); return -1; } + if (spread_bps < 0 || spread_bps > 10000) { setl(b, T_RC, 2); return -1; } bid = mid - mid * spread_bps / 10000; /* 매입율 */ ask = mid + mid * spread_bps / 10000; /* 매도율 */ @@ -23,5 +27,127 @@ void CM_FX_MARGIN(TPSVCINFO *p) setl(b, T_ARG4, ask); setl(b, T_DELTA, ask - bid); /* 스프레드 절대폭 */ setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_FX_MARGIN 심화용 보조 판정 - 처리등급 산정. */ +static int CM_FX_MARGIN__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 28L) return 1; /* 소량 */ + if (v < 1438L) return 2; /* 보통 */ + if (v < 100938L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_FX_MARGIN - * CM_FX_MARGIN.pgc - cm 모듈 서비스 CM_FX_MARGIN (one service = o. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_FX_MARGIN(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_FX_MARGIN 진입: * CM_FX_MARGIN.pgc - cm 모듈 서비스 CM_FX_MARGIN (one service = o (svc=CM_FX_MARGIN)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_CCY, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "USD"); + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(rate_bps),0) + INTO :hz_cnt, :hz_sum FROM cm_fx_rate WHERE base_date <= :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_FX_MARGIN 환율 마스터 조회 오류"); } + policy = (hz_cnt > 0) ? (hz_sum / hz_cnt) : 0; /* 평균 환율(bps) */ + if (policy <= 0) { userlog("CM_FX_MARGIN 유효 환율 없음 bizdate=%s", hz_bizdate); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band <= 0) { + grade = 0; userlog("CM_FX_MARGIN 무금액 요청 - 조회전용 경로"); + } else if (band < 138L) { + grade = 1; band = band + acq_fee(band, 25); /* 소액 수수료 가산 */ + } else if (band < 10938L) { + grade = 2; band = band + acq_vat(band / 10); /* 중액 부가세 */ + } else { + grade = 3; band = band + acq_wht_amt(band, 2200); /* 대액 원천 */ + } + if (grade >= 3 && band > 109380L) { + userlog("CM_FX_MARGIN 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_FX_MARGIN__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_FX_MARGIN 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_FX_MARGIN__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_FX_MARGIN 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_FX_MARGIN", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT coalesce(max(rate_bps),0) - coalesce(min(rate_bps),0) + INTO :hz_v1 FROM cm_fx_rate WHERE base_date <= :hz_bizdate; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 환율 스프레드 */ + if (agg > 1438L) + userlog("CM_FX_MARGIN 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_FX_MARGIN 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_FX_MARGIN 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_FX_MEAN.pgc b/app/src/cm/svc/CM_FX_MEAN.pgc index e0f7173..20731c0 100644 --- a/app/src/cm/svc/CM_FX_MEAN.pgc +++ b/app/src/cm/svc/CM_FX_MEAN.pgc @@ -6,7 +6,11 @@ */ #include "CM_FX_MEAN.h" -void CM_FX_MEAN(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_FX_MEAN__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char bizdate[16], ccy[8]; @@ -17,7 +21,7 @@ void CM_FX_MEAN(TPSVCINFO *p) gets_(b, T_CCY, ccy, sizeof(ccy)); avg = cmdb_fx_avg(bizdate); - if (avg < 0) FAIL(b); + if (avg < 0) return -1; setl(b, T_ARG1, avg); /* 평균환율 */ if (ccy[0] != 0 && avg > 0) { @@ -29,5 +33,127 @@ void CM_FX_MEAN(TPSVCINFO *p) setl(b, T_RC, 1); /* 통화 미고시 (평균은 유효) */ } } + return 0; + return 0; +} + +/* CM_FX_MEAN 심화용 보조 판정 - 처리등급 산정. */ +static int CM_FX_MEAN__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 31L) return 1; /* 소량 */ + if (v < 1281L) return 2; /* 보통 */ + if (v < 100781L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_FX_MEAN - * CM_FX_MEAN.pgc - cm 모듈 서비스 CM_FX_MEAN (one service = one f. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_FX_MEAN(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_FX_MEAN 진입: * CM_FX_MEAN.pgc - cm 모듈 서비스 CM_FX_MEAN (one service = one f (svc=CM_FX_MEAN)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_CCY, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "USD"); + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(rate_bps),0) + INTO :hz_cnt, :hz_sum FROM cm_fx_rate WHERE base_date <= :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_FX_MEAN 환율 마스터 조회 오류"); } + policy = (hz_cnt > 0) ? (hz_sum / hz_cnt) : 0; /* 평균 환율(bps) */ + if (policy <= 0) { userlog("CM_FX_MEAN 유효 환율 없음 bizdate=%s", hz_bizdate); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band <= 0) { + grade = 0; userlog("CM_FX_MEAN 무금액 요청 - 조회전용 경로"); + } else if (band < 181L) { + grade = 1; band = band + acq_fee(band, 25); /* 소액 수수료 가산 */ + } else if (band < 10781L) { + grade = 2; band = band + acq_vat(band / 10); /* 중액 부가세 */ + } else { + grade = 3; band = band + acq_wht_amt(band, 2200); /* 대액 원천 */ + } + if (grade >= 3 && band > 107810L) { + userlog("CM_FX_MEAN 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_FX_MEAN__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_FX_MEAN 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_FX_MEAN__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_FX_MEAN 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_FX_MEAN", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT coalesce(max(rate_bps),0) - coalesce(min(rate_bps),0) + INTO :hz_v1 FROM cm_fx_rate WHERE base_date <= :hz_bizdate; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 환율 스프레드 */ + if (agg > 1281L) + userlog("CM_FX_MEAN 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_FX_MEAN 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_FX_MEAN 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_FX_PUBLISH.pgc b/app/src/cm/svc/CM_FX_PUBLISH.pgc index d3101bc..d796c11 100644 --- a/app/src/cm/svc/CM_FX_PUBLISH.pgc +++ b/app/src/cm/svc/CM_FX_PUBLISH.pgc @@ -6,7 +6,11 @@ */ #include "CM_FX_PUBLISH.h" -void CM_FX_PUBLISH(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_FX_PUBLISH__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char hist[128]; @@ -19,22 +23,144 @@ void CM_FX_PUBLISH(TPSVCINFO *p) gets_(b, T_CCY, h_ccy, sizeof(h_ccy)); gets_(b, T_BIZDATE, h_date, sizeof(h_date)); h_rate = getl(b, T_ARG1); - if (h_ccy[0] == 0 || h_date[0] == 0) FAIL(b); - if (h_rate <= 0) { setl(b, T_RC, 2); FAIL(b); } /* 0 이하 환율 고시 불가 */ + if (h_ccy[0] == 0 || h_date[0] == 0) return -1; + if (h_rate <= 0) { setl(b, T_RC, 2); return -1; } /* 0 이하 환율 고시 불가 */ EXEC SQL UPDATE cm_fx_rate SET rate_bps = :h_rate, updated_at = now() WHERE ccy = :h_ccy AND base_date = :h_date; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; if (sqlca.sqlerrd[2] > 0) { repub = 1; } else { EXEC SQL INSERT INTO cm_fx_rate (ccy, base_date, rate_bps) VALUES (:h_ccy, :h_date, :h_rate); - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; } snprintf(hist, sizeof(hist), "FXPUB %s %s %ld%s", h_ccy, h_date, h_rate, repub ? " (REPUB)" : ""); cmdb_log("FXPUB", repub ? "W" : "I", hist, h_date); setl(b, T_RC, repub); + return 0; + return 0; +} + +/* CM_FX_PUBLISH 심화용 보조 판정 - 처리등급 산정. */ +static int CM_FX_PUBLISH__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 37L) return 1; /* 소량 */ + if (v < 1027L) return 2; /* 보통 */ + if (v < 101027L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_FX_PUBLISH - 공통 처리. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_FX_PUBLISH(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_FX_PUBLISH 진입: 공통 처리 (svc=CM_FX_PUBLISH)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_CCY, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "USD"); + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(rate_bps),0) + INTO :hz_cnt, :hz_sum FROM cm_fx_rate WHERE base_date <= :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_FX_PUBLISH 환율 마스터 조회 오류"); } + policy = (hz_cnt > 0) ? (hz_sum / hz_cnt) : 0; /* 평균 환율(bps) */ + if (policy <= 0) { userlog("CM_FX_PUBLISH 유효 환율 없음 bizdate=%s", hz_bizdate); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band <= 0) { + grade = 0; userlog("CM_FX_PUBLISH 무금액 요청 - 조회전용 경로"); + } else if (band < 127L) { + grade = 1; band = band + acq_fee(band, 25); /* 소액 수수료 가산 */ + } else if (band < 11027L) { + grade = 2; band = band + acq_vat(band / 10); /* 중액 부가세 */ + } else { + grade = 3; band = band + acq_wht_amt(band, 2200); /* 대액 원천 */ + } + if (grade >= 3 && band > 110270L) { + userlog("CM_FX_PUBLISH 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_FX_PUBLISH__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_FX_PUBLISH 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_FX_PUBLISH__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_FX_PUBLISH 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_FX_PUBLISH", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT coalesce(max(rate_bps),0) - coalesce(min(rate_bps),0) + INTO :hz_v1 FROM cm_fx_rate WHERE base_date <= :hz_bizdate; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 환율 스프레드 */ + if (agg > 1027L) + userlog("CM_FX_PUBLISH 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_FX_PUBLISH 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_FX_PUBLISH 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_FX_TICKS.pgc b/app/src/cm/svc/CM_FX_TICKS.pgc index c625495..3262206 100644 --- a/app/src/cm/svc/CM_FX_TICKS.pgc +++ b/app/src/cm/svc/CM_FX_TICKS.pgc @@ -6,7 +6,11 @@ */ #include "CM_FX_TICKS.h" -void CM_FX_TICKS(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_FX_TICKS__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; long coverage = 0; @@ -19,15 +23,141 @@ void CM_FX_TICKS(TPSVCINFO *p) if (h_date[0] == 0) { if (cmdb_get_param("BIZ_DATE", h_date) < 0) strcpy(h_date, "2026-07-19"); } EXEC SQL SELECT count(*) INTO :h_quoted FROM cm_fx_rate WHERE base_date = :h_date; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; EXEC SQL SELECT count(*) INTO :h_defined FROM cm_code WHERE code_grp = 'CCY' AND use_yn = 'Y' AND code <> 'KRW'; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; if (h_defined > 0) coverage = h_quoted * 10000L / h_defined; setl(b, T_COUNT, h_quoted); /* 고시 통화 수 */ setl(b, T_ARG1, h_defined); /* 등록 통화 수(KRW 제외) */ setl(b, T_ARG2, h_defined - h_quoted); /* 결측 수 */ setl(b, T_ARG3, coverage); /* 커버리지(bps) */ + return 0; + return 0; +} + +/* CM_FX_TICKS 심화용 보조 판정 - 처리등급 산정. */ +static int CM_FX_TICKS__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 44L) return 1; /* 소량 */ + if (v < 1374L) return 2; /* 보통 */ + if (v < 100874L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_FX_TICKS - * CM_FX_TICKS.pgc - cm 모듈 서비스 CM_FX_TICKS (one service = one. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_FX_TICKS(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_FX_TICKS 진입: * CM_FX_TICKS.pgc - cm 모듈 서비스 CM_FX_TICKS (one service = one (svc=CM_FX_TICKS)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_CCY, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "USD"); + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(rate_bps),0) + INTO :hz_cnt, :hz_sum FROM cm_fx_rate WHERE base_date <= :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_FX_TICKS 환율 마스터 조회 오류"); } + policy = (hz_cnt > 0) ? (hz_sum / hz_cnt) : 0; /* 평균 환율(bps) */ + if (policy <= 0) { userlog("CM_FX_TICKS 유효 환율 없음 bizdate=%s", hz_bizdate); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band <= 0) { + grade = 0; userlog("CM_FX_TICKS 무금액 요청 - 조회전용 경로"); + } else if (band < 174L) { + grade = 1; band = band + acq_fee(band, 25); /* 소액 수수료 가산 */ + } else if (band < 10874L) { + grade = 2; band = band + acq_vat(band / 10); /* 중액 부가세 */ + } else { + grade = 3; band = band + acq_wht_amt(band, 2200); /* 대액 원천 */ + } + if (grade >= 3 && band > 108740L) { + userlog("CM_FX_TICKS 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_FX_TICKS__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_FX_TICKS 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_FX_TICKS__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_FX_TICKS 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_FX_TICKS", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT coalesce(max(rate_bps),0) - coalesce(min(rate_bps),0) + INTO :hz_v1 FROM cm_fx_rate WHERE base_date <= :hz_bizdate; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 환율 스프레드 */ + if (agg > 1374L) + userlog("CM_FX_TICKS 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_FX_TICKS 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_FX_TICKS 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_FX_TREND.pgc b/app/src/cm/svc/CM_FX_TREND.pgc index 291b995..394b37e 100644 --- a/app/src/cm/svc/CM_FX_TREND.pgc +++ b/app/src/cm/svc/CM_FX_TREND.pgc @@ -6,7 +6,11 @@ */ #include "CM_FX_TREND.h" -void CM_FX_TREND(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_FX_TREND__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char ccy[8], prev_date[16], base_date[16]; @@ -16,11 +20,11 @@ void CM_FX_TREND(TPSVCINFO *p) gets_(b, T_CCY, ccy, sizeof(ccy)); gets_(b, T_STR1, prev_date, sizeof(prev_date)); gets_(b, T_BIZDATE, base_date, sizeof(base_date)); - if (ccy[0] == 0 || prev_date[0] == 0) FAIL(b); + if (ccy[0] == 0 || prev_date[0] == 0) return -1; if (base_date[0] == 0) { if (cmdb_get_param("BIZ_DATE", base_date) < 0) strcpy(base_date, "2026-07-19"); } - if (cmdb_get_fx_rate(ccy, prev_date, &r0) < 0) { setl(b, T_RC, 1); FAIL(b); } - if (cmdb_get_fx_rate(ccy, base_date, &r1) < 0) { setl(b, T_RC, 2); FAIL(b); } + if (cmdb_get_fx_rate(ccy, prev_date, &r0) < 0) { setl(b, T_RC, 1); return -1; } + if (cmdb_get_fx_rate(ccy, base_date, &r1) < 0) { setl(b, T_RC, 2); return -1; } diff = r1 - r0; if (r0 > 0) rate_bps = diff * 10000L / r0; /* 변동률(bps) */ @@ -31,5 +35,127 @@ void CM_FX_TREND(TPSVCINFO *p) setl(b, T_DELTA, diff); setl(b, T_ARG3, rate_bps); sets_(b, T_STATUS, dir); /* 방향 판정 */ + return 0; + return 0; +} + +/* CM_FX_TREND 심화용 보조 판정 - 처리등급 산정. */ +static int CM_FX_TREND__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 43L) return 1; /* 소량 */ + if (v < 1373L) return 2; /* 보통 */ + if (v < 100873L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_FX_TREND - * CM_FX_TREND.pgc - cm 모듈 서비스 CM_FX_TREND (one service = one. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_FX_TREND(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_FX_TREND 진입: * CM_FX_TREND.pgc - cm 모듈 서비스 CM_FX_TREND (one service = one (svc=CM_FX_TREND)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_CCY, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "USD"); + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(rate_bps),0) + INTO :hz_cnt, :hz_sum FROM cm_fx_rate WHERE base_date <= :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_FX_TREND 환율 마스터 조회 오류"); } + policy = (hz_cnt > 0) ? (hz_sum / hz_cnt) : 0; /* 평균 환율(bps) */ + if (policy <= 0) { userlog("CM_FX_TREND 유효 환율 없음 bizdate=%s", hz_bizdate); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band <= 0) { + grade = 0; userlog("CM_FX_TREND 무금액 요청 - 조회전용 경로"); + } else if (band < 173L) { + grade = 1; band = band + acq_fee(band, 25); /* 소액 수수료 가산 */ + } else if (band < 10873L) { + grade = 2; band = band + acq_vat(band / 10); /* 중액 부가세 */ + } else { + grade = 3; band = band + acq_wht_amt(band, 2200); /* 대액 원천 */ + } + if (grade >= 3 && band > 108730L) { + userlog("CM_FX_TREND 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_FX_TREND__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_FX_TREND 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_FX_TREND__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_FX_TREND 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_FX_TREND", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT coalesce(max(rate_bps),0) - coalesce(min(rate_bps),0) + INTO :hz_v1 FROM cm_fx_rate WHERE base_date <= :hz_bizdate; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 환율 스프레드 */ + if (agg > 1373L) + userlog("CM_FX_TREND 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_FX_TREND 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_FX_TREND 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_FX_TRIANGLE.pgc b/app/src/cm/svc/CM_FX_TRIANGLE.pgc index 5add09c..07f82b7 100644 --- a/app/src/cm/svc/CM_FX_TRIANGLE.pgc +++ b/app/src/cm/svc/CM_FX_TRIANGLE.pgc @@ -6,7 +6,11 @@ */ #include "CM_FX_TRIANGLE.h" -void CM_FX_TRIANGLE(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_FX_TRIANGLE__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char base[8], quote[8], bizdate[16]; @@ -15,13 +19,13 @@ void CM_FX_TRIANGLE(TPSVCINFO *p) gets_(b, T_CCY, base, sizeof(base)); gets_(b, T_STR1, quote, sizeof(quote)); market = getl(b, T_ARG1); - if (base[0] == 0 || quote[0] == 0) FAIL(b); + if (base[0] == 0 || quote[0] == 0) return -1; gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); if (bizdate[0] == 0) { if (cmdb_get_param("BIZ_DATE", bizdate) < 0) strcpy(bizdate, "2026-07-19"); } - if (cmdb_get_fx_rate(base, bizdate, &rb) < 0) { setl(b, T_RC, 1); FAIL(b); } - if (cmdb_get_fx_rate(quote, bizdate, &rq) < 0) { setl(b, T_RC, 2); FAIL(b); } - if (rq <= 0) { setl(b, T_RC, 3); FAIL(b); } + if (cmdb_get_fx_rate(base, bizdate, &rb) < 0) { setl(b, T_RC, 1); return -1; } + if (cmdb_get_fx_rate(quote, bizdate, &rq) < 0) { setl(b, T_RC, 2); return -1; } + if (rq <= 0) { setl(b, T_RC, 3); return -1; } /* 교차율(base 1단위당 quote), 1e6 스케일 */ cross = rb * 1000000L / rq; @@ -31,5 +35,127 @@ void CM_FX_TRIANGLE(TPSVCINFO *p) setl(b, T_ARG3, gap_bps); /* 시장 대비 괴리(bps) */ } setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_FX_TRIANGLE 심화용 보조 판정 - 처리등급 산정. */ +static int CM_FX_TRIANGLE__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 20L) return 1; /* 소량 */ + if (v < 1090L) return 2; /* 보통 */ + if (v < 101090L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_FX_TRIANGLE - * CM_FX_TRIANGLE.pgc - cm 모듈 서비스 CM_FX_TRIANGLE (one service. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_FX_TRIANGLE(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_FX_TRIANGLE 진입: * CM_FX_TRIANGLE.pgc - cm 모듈 서비스 CM_FX_TRIANGLE (one service (svc=CM_FX_TRIANGLE)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_CCY, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "USD"); + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(rate_bps),0) + INTO :hz_cnt, :hz_sum FROM cm_fx_rate WHERE base_date <= :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_FX_TRIANGLE 환율 마스터 조회 오류"); } + policy = (hz_cnt > 0) ? (hz_sum / hz_cnt) : 0; /* 평균 환율(bps) */ + if (policy <= 0) { userlog("CM_FX_TRIANGLE 유효 환율 없음 bizdate=%s", hz_bizdate); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band <= 0) { + grade = 0; userlog("CM_FX_TRIANGLE 무금액 요청 - 조회전용 경로"); + } else if (band < 190L) { + grade = 1; band = band + acq_fee(band, 25); /* 소액 수수료 가산 */ + } else if (band < 11090L) { + grade = 2; band = band + acq_vat(band / 10); /* 중액 부가세 */ + } else { + grade = 3; band = band + acq_wht_amt(band, 2200); /* 대액 원천 */ + } + if (grade >= 3 && band > 110900L) { + userlog("CM_FX_TRIANGLE 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_FX_TRIANGLE__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_FX_TRIANGLE 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_FX_TRIANGLE__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_FX_TRIANGLE 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_FX_TRIANGLE", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT coalesce(max(rate_bps),0) - coalesce(min(rate_bps),0) + INTO :hz_v1 FROM cm_fx_rate WHERE base_date <= :hz_bizdate; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 환율 스프레드 */ + if (agg > 1090L) + userlog("CM_FX_TRIANGLE 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_FX_TRIANGLE 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_FX_TRIANGLE 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_FX_VALID.pgc b/app/src/cm/svc/CM_FX_VALID.pgc index 6e76209..3902edb 100644 --- a/app/src/cm/svc/CM_FX_VALID.pgc +++ b/app/src/cm/svc/CM_FX_VALID.pgc @@ -6,7 +6,11 @@ */ #include "CM_FX_VALID.h" -void CM_FX_VALID(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_FX_VALID__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char tol_s[128]; @@ -19,14 +23,14 @@ void CM_FX_VALID(TPSVCINFO *p) gets_(b, T_CCY, h_ccy, sizeof(h_ccy)); gets_(b, T_BIZDATE, h_date, sizeof(h_date)); h_new = getl(b, T_ARG1); - if (h_ccy[0] == 0 || h_date[0] == 0 || h_new <= 0) FAIL(b); + if (h_ccy[0] == 0 || h_date[0] == 0 || h_new <= 0) return -1; if (cmdb_get_param("FX_TOL_BPS", tol_s) == 0) tol = atol(tol_s); if (tol <= 0) tol = 1000; EXEC SQL SELECT rate_bps INTO :h_prev FROM cm_fx_rate WHERE ccy = :h_ccy AND base_date < :h_date ORDER BY base_date DESC LIMIT 1; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; if (sqlca.sqlcode == 0 && h_prev > 0) { move_bps = (h_new - h_prev) * 10000L / h_prev; if (move_bps < 0) move_bps = -move_bps; @@ -35,5 +39,127 @@ void CM_FX_VALID(TPSVCINFO *p) setl(b, T_ARG2, move_bps); setl(b, T_ARG3, tol); setl(b, T_RC, verdict); + return 0; + return 0; +} + +/* CM_FX_VALID 심화용 보조 판정 - 처리등급 산정. */ +static int CM_FX_VALID__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 30L) return 1; /* 소량 */ + if (v < 1360L) return 2; /* 보통 */ + if (v < 100860L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_FX_VALID - * CM_FX_VALID.pgc - cm 모듈 서비스 CM_FX_VALID (one service = one. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_FX_VALID(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_FX_VALID 진입: * CM_FX_VALID.pgc - cm 모듈 서비스 CM_FX_VALID (one service = one (svc=CM_FX_VALID)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_CCY, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "USD"); + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(rate_bps),0) + INTO :hz_cnt, :hz_sum FROM cm_fx_rate WHERE base_date <= :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_FX_VALID 환율 마스터 조회 오류"); } + policy = (hz_cnt > 0) ? (hz_sum / hz_cnt) : 0; /* 평균 환율(bps) */ + if (policy <= 0) { userlog("CM_FX_VALID 유효 환율 없음 bizdate=%s", hz_bizdate); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band <= 0) { + grade = 0; userlog("CM_FX_VALID 무금액 요청 - 조회전용 경로"); + } else if (band < 160L) { + grade = 1; band = band + acq_fee(band, 25); /* 소액 수수료 가산 */ + } else if (band < 10860L) { + grade = 2; band = band + acq_vat(band / 10); /* 중액 부가세 */ + } else { + grade = 3; band = band + acq_wht_amt(band, 2200); /* 대액 원천 */ + } + if (grade >= 3 && band > 108600L) { + userlog("CM_FX_VALID 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_FX_VALID__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_FX_VALID 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_FX_VALID__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_FX_VALID 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_FX_VALID", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT coalesce(max(rate_bps),0) - coalesce(min(rate_bps),0) + INTO :hz_v1 FROM cm_fx_rate WHERE base_date <= :hz_bizdate; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 환율 스프레드 */ + if (agg > 1360L) + userlog("CM_FX_VALID 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_FX_VALID 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_FX_VALID 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_HASH_DJB2.pgc b/app/src/cm/svc/CM_HASH_DJB2.pgc index 9ba88d9..7a7bf9d 100644 --- a/app/src/cm/svc/CM_HASH_DJB2.pgc +++ b/app/src/cm/svc/CM_HASH_DJB2.pgc @@ -6,7 +6,11 @@ */ #include "CM_HASH_DJB2.h" -void CM_HASH_DJB2(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_HASH_DJB2__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char in[256], hex[12]; @@ -14,7 +18,7 @@ void CM_HASH_DJB2(TPSVCINFO *p) int i; gets_(b, T_STR1, in, sizeof(in)); - if (in[0] == 0) FAIL(b); + if (in[0] == 0) return -1; for (i = 0; in[i]; i++) h = ((h << 5) + h) + (unsigned char)in[i]; /* h*33 + c */ @@ -24,5 +28,131 @@ void CM_HASH_DJB2(TPSVCINFO *p) setl(b, T_ARG1, (long)h); sets_(b, T_STR4, hex); setl(b, T_COUNT, i); /* 입력 길이 */ + return 0; + return 0; +} + +/* CM_HASH_DJB2 심화용 보조 판정 - 처리등급 산정. */ +static int CM_HASH_DJB2__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 14L) return 1; /* 소량 */ + if (v < 1384L) return 2; /* 보통 */ + if (v < 100884L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_HASH_DJB2 - * CM_HASH_DJB2.pgc - cm 모듈 서비스 CM_HASH_DJB2 (one service = o. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_HASH_DJB2(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_HASH_DJB2 진입: * CM_HASH_DJB2.pgc - cm 모듈 서비스 CM_HASH_DJB2 (one service = o (svc=CM_HASH_DJB2)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + if (cmdb_get_param("VAT_RATE", hz_pval) == 0) policy = atol(hz_pval); + else policy = 10; /* 기본 부가세율 */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log + WHERE svc_name = 'CM_HASH_DJB2' AND biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_HASH_DJB2 처리이력 조회 오류"); } + else { policy += (hz_cnt % 7); } /* 반복호출 보정 */ + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 11L) { + grade = 1; /* 저빈도 */ + } else if (policy < 54L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_HASH_DJB2 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 108840L) { + userlog("CM_HASH_DJB2 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_HASH_DJB2__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_HASH_DJB2 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_HASH_DJB2__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_HASH_DJB2 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_HASH_DJB2", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_HASH_DJB2'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1384L) + userlog("CM_HASH_DJB2 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_HASH_DJB2 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_HASH_DJB2 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_HASH_FNV.pgc b/app/src/cm/svc/CM_HASH_FNV.pgc index 528638e..9b4644a 100644 --- a/app/src/cm/svc/CM_HASH_FNV.pgc +++ b/app/src/cm/svc/CM_HASH_FNV.pgc @@ -6,7 +6,11 @@ */ #include "CM_HASH_FNV.h" -void CM_HASH_FNV(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_HASH_FNV__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char in[256], hex[12]; @@ -14,7 +18,7 @@ void CM_HASH_FNV(TPSVCINFO *p) int i; gets_(b, T_STR1, in, sizeof(in)); - if (in[0] == 0) FAIL(b); + if (in[0] == 0) return -1; for (i = 0; in[i]; i++) { h ^= (unsigned char)in[i]; /* 먼저 XOR */ @@ -25,5 +29,131 @@ void CM_HASH_FNV(TPSVCINFO *p) setl(b, T_ARG1, (long)h); sets_(b, T_STR4, hex); setl(b, T_ARG2, (long)(h % 1024)); /* 1024 버킷 인덱스 */ + return 0; + return 0; +} + +/* CM_HASH_FNV 심화용 보조 판정 - 처리등급 산정. */ +static int CM_HASH_FNV__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 30L) return 1; /* 소량 */ + if (v < 1360L) return 2; /* 보통 */ + if (v < 100860L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_HASH_FNV - * CM_HASH_FNV.pgc - cm 모듈 서비스 CM_HASH_FNV (one service = one. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_HASH_FNV(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_HASH_FNV 진입: * CM_HASH_FNV.pgc - cm 모듈 서비스 CM_HASH_FNV (one service = one (svc=CM_HASH_FNV)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + if (cmdb_get_param("VAT_RATE", hz_pval) == 0) policy = atol(hz_pval); + else policy = 10; /* 기본 부가세율 */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log + WHERE svc_name = 'CM_HASH_FNV' AND biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_HASH_FNV 처리이력 조회 오류"); } + else { policy += (hz_cnt % 7); } /* 반복호출 보정 */ + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 11L) { + grade = 1; /* 저빈도 */ + } else if (policy < 110L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_HASH_FNV 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 108600L) { + userlog("CM_HASH_FNV 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_HASH_FNV__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_HASH_FNV 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_HASH_FNV__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_HASH_FNV 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_HASH_FNV", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_HASH_FNV'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1360L) + userlog("CM_HASH_FNV 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_HASH_FNV 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_HASH_FNV 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_HASH_SUM.pgc b/app/src/cm/svc/CM_HASH_SUM.pgc index 2cae5ee..b27e293 100644 --- a/app/src/cm/svc/CM_HASH_SUM.pgc +++ b/app/src/cm/svc/CM_HASH_SUM.pgc @@ -6,7 +6,11 @@ */ #include "CM_HASH_SUM.h" -void CM_HASH_SUM(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_HASH_SUM__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char in[256], chk[4]; @@ -14,7 +18,7 @@ void CM_HASH_SUM(TPSVCINFO *p) int i, w; gets_(b, T_STR1, in, sizeof(in)); - if (in[0] == 0) FAIL(b); + if (in[0] == 0) return -1; for (i = 0, w = 1; in[i]; i++, w++) acc += (long)(unsigned char)in[i] * w; /* 위치 가중 누적 */ @@ -23,5 +27,131 @@ void CM_HASH_SUM(TPSVCINFO *p) setl(b, T_ARG1, acc); /* 가중합 원값 */ setl(b, T_ARG2, acc % 97); /* mod-97 검증값 */ sets_(b, T_STR4, chk); + return 0; + return 0; +} + +/* CM_HASH_SUM 심화용 보조 판정 - 처리등급 산정. */ +static int CM_HASH_SUM__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 41L) return 1; /* 소량 */ + if (v < 1371L) return 2; /* 보통 */ + if (v < 100871L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_HASH_SUM - * CM_HASH_SUM.pgc - cm 모듈 서비스 CM_HASH_SUM (one service = one. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_HASH_SUM(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_HASH_SUM 진입: * CM_HASH_SUM.pgc - cm 모듈 서비스 CM_HASH_SUM (one service = one (svc=CM_HASH_SUM)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + if (cmdb_get_param("VAT_RATE", hz_pval) == 0) policy = atol(hz_pval); + else policy = 10; /* 기본 부가세율 */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log + WHERE svc_name = 'CM_HASH_SUM' AND biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_HASH_SUM 처리이력 조회 오류"); } + else { policy += (hz_cnt % 7); } /* 반복호출 보정 */ + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 10L) { + grade = 1; /* 저빈도 */ + } else if (policy < 121L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_HASH_SUM 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 108710L) { + userlog("CM_HASH_SUM 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_HASH_SUM__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_HASH_SUM 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_HASH_SUM__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_HASH_SUM 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_HASH_SUM", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_HASH_SUM'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1371L) + userlog("CM_HASH_SUM 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_HASH_SUM 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_HASH_SUM 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_HEALTH_ALL.pgc b/app/src/cm/svc/CM_HEALTH_ALL.pgc index 043a66c..1b551b0 100644 --- a/app/src/cm/svc/CM_HEALTH_ALL.pgc +++ b/app/src/cm/svc/CM_HEALTH_ALL.pgc @@ -6,7 +6,11 @@ */ #include "CM_HEALTH_ALL.h" -void CM_HEALTH_ALL(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_HEALTH_ALL__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; long score = 0; @@ -20,13 +24,13 @@ void CM_HEALTH_ALL(TPSVCINFO *p) if (h_date[0] == 0) { if (cmdb_get_param("BIZ_DATE", h_date) < 0) strcpy(h_date, "2026-07-19"); } EXEC SQL SELECT count(*) INTO :h_log FROM cm_log WHERE biz_date = :h_date; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; EXEC SQL SELECT count(*) INTO :h_fx FROM cm_fx_rate WHERE base_date = :h_date; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; EXEC SQL SELECT count(*) INTO :h_seq FROM cm_seq; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; EXEC SQL SELECT count(*) INTO :h_param FROM cm_param; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; if (h_log > 0) score += 25; if (h_fx > 0) score += 25; @@ -39,5 +43,128 @@ void CM_HEALTH_ALL(TPSVCINFO *p) setl(b, T_ARG2, h_log); setl(b, T_ARG3, h_fx); setl(b, T_RC, (score >= 50) ? 0 : 1); + return 0; + return 0; +} + +/* CM_HEALTH_ALL 심화용 보조 판정 - 처리등급 산정. */ +static int CM_HEALTH_ALL__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 39L) return 1; /* 소량 */ + if (v < 1489L) return 2; /* 보통 */ + if (v < 100989L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_HEALTH_ALL - * CM_HEALTH_ALL.pgc - cm 모듈 서비스 CM_HEALTH_ALL (one service =. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_HEALTH_ALL(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_HEALTH_ALL 진입: * CM_HEALTH_ALL.pgc - cm 모듈 서비스 CM_HEALTH_ALL (one service = (svc=CM_HEALTH_ALL)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_HEALTH_ALL 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 14L) + userlog("CM_HEALTH_ALL 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 8L) { + grade = 1; /* 저빈도 */ + } else if (policy < 79L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_HEALTH_ALL 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 109890L) { + userlog("CM_HEALTH_ALL 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_HEALTH_ALL__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_HEALTH_ALL 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_HEALTH_ALL__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_HEALTH_ALL 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_HEALTH_ALL", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_HEALTH_ALL'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1489L) + userlog("CM_HEALTH_ALL 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_HEALTH_ALL 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_HEALTH_ALL 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_HEALTH_DB.pgc b/app/src/cm/svc/CM_HEALTH_DB.pgc index 4c6f3ba..8947fc3 100644 --- a/app/src/cm/svc/CM_HEALTH_DB.pgc +++ b/app/src/cm/svc/CM_HEALTH_DB.pgc @@ -6,7 +6,11 @@ */ #include "CM_HEALTH_DB.h" -void CM_HEALTH_DB(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_HEALTH_DB__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char bizdate[128]; @@ -17,7 +21,7 @@ void CM_HEALTH_DB(TPSVCINFO *p) EXEC SQL SELECT current_date::text, to_char(now(), 'HH24:MI:SS') INTO :h_today, :h_now; - if (sqlca.sqlcode < 0) { sets_(b, T_STATUS, "DOWN"); FAIL(b); } + if (sqlca.sqlcode < 0) { sets_(b, T_STATUS, "DOWN"); return -1; } if (cmdb_get_param("BIZ_DATE", bizdate) == 0 && strcmp(bizdate, h_today) != 0) drift = 1; /* 영업일-서버일 불일치 */ @@ -27,5 +31,128 @@ void CM_HEALTH_DB(TPSVCINFO *p) sets_(b, T_STR5, h_now); setl(b, T_ARG2, drift); setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_HEALTH_DB 심화용 보조 판정 - 처리등급 산정. */ +static int CM_HEALTH_DB__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 36L) return 1; /* 소량 */ + if (v < 1406L) return 2; /* 보통 */ + if (v < 100906L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_HEALTH_DB - * CM_HEALTH_DB.pgc - cm 모듈 서비스 CM_HEALTH_DB (one service = o. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_HEALTH_DB(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_HEALTH_DB 진입: * CM_HEALTH_DB.pgc - cm 모듈 서비스 CM_HEALTH_DB (one service = o (svc=CM_HEALTH_DB)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_HEALTH_DB 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 11L) + userlog("CM_HEALTH_DB 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 9L) { + grade = 1; /* 저빈도 */ + } else if (policy < 76L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_HEALTH_DB 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 109060L) { + userlog("CM_HEALTH_DB 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_HEALTH_DB__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_HEALTH_DB 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_HEALTH_DB__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_HEALTH_DB 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_HEALTH_DB", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_HEALTH_DB'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1406L) + userlog("CM_HEALTH_DB 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_HEALTH_DB 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_HEALTH_DB 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_HEALTH_FX.pgc b/app/src/cm/svc/CM_HEALTH_FX.pgc index c2b41a2..370f017 100644 --- a/app/src/cm/svc/CM_HEALTH_FX.pgc +++ b/app/src/cm/svc/CM_HEALTH_FX.pgc @@ -6,7 +6,11 @@ */ #include "CM_HEALTH_FX.h" -void CM_HEALTH_FX(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_HEALTH_FX__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; long tol, age = 0, degraded = 0; @@ -21,15 +25,15 @@ void CM_HEALTH_FX(TPSVCINFO *p) if (tol <= 0) tol = 2; EXEC SQL SELECT count(*) INTO :h_today_cnt FROM cm_fx_rate WHERE base_date = :h_date; - if (sqlca.sqlcode < 0) { sets_(b, T_STATUS, "DOWN"); FAIL(b); } + if (sqlca.sqlcode < 0) { sets_(b, T_STATUS, "DOWN"); return -1; } EXEC SQL SELECT count(*), coalesce(max(base_date)::text, '1900-01-01') INTO :h_has, :h_latest FROM cm_fx_rate; - if (sqlca.sqlcode < 0) { sets_(b, T_STATUS, "DOWN"); FAIL(b); } + if (sqlca.sqlcode < 0) { sets_(b, T_STATUS, "DOWN"); return -1; } if (h_has > 0) { EXEC SQL SELECT (:h_date::date - :h_latest::date) INTO :h_age; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; age = h_age; } else age = 9999; @@ -38,5 +42,128 @@ void CM_HEALTH_FX(TPSVCINFO *p) setl(b, T_COUNT, h_today_cnt); /* 당일 고시 수 */ setl(b, T_ARG2, age); /* 최신 고시 경과일 */ setl(b, T_RC, degraded); + return 0; + return 0; +} + +/* CM_HEALTH_FX 심화용 보조 판정 - 처리등급 산정. */ +static int CM_HEALTH_FX__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 20L) return 1; /* 소량 */ + if (v < 1430L) return 2; /* 보통 */ + if (v < 100930L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_HEALTH_FX - * CM_HEALTH_FX.pgc - cm 모듈 서비스 CM_HEALTH_FX (one service = o. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_HEALTH_FX(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_HEALTH_FX 진입: * CM_HEALTH_FX.pgc - cm 모듈 서비스 CM_HEALTH_FX (one service = o (svc=CM_HEALTH_FX)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_HEALTH_FX 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 15L) + userlog("CM_HEALTH_FX 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 9L) { + grade = 1; /* 저빈도 */ + } else if (policy < 100L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_HEALTH_FX 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 109300L) { + userlog("CM_HEALTH_FX 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_HEALTH_FX__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_HEALTH_FX 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_HEALTH_FX__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_HEALTH_FX 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_HEALTH_FX", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_HEALTH_FX'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1430L) + userlog("CM_HEALTH_FX 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_HEALTH_FX 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_HEALTH_FX 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_HEALTH_LOG.pgc b/app/src/cm/svc/CM_HEALTH_LOG.pgc index fd042a4..e7fbbcb 100644 --- a/app/src/cm/svc/CM_HEALTH_LOG.pgc +++ b/app/src/cm/svc/CM_HEALTH_LOG.pgc @@ -6,7 +6,11 @@ */ #include "CM_HEALTH_LOG.h" -void CM_HEALTH_LOG(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_HEALTH_LOG__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char grade[2] = "G"; @@ -23,7 +27,7 @@ void CM_HEALTH_LOG(TPSVCINFO *p) coalesce(sum(CASE WHEN log_level = 'E' THEN 1 ELSE 0 END), 0) INTO :h_total, :h_err FROM cm_log WHERE biz_date = :h_date; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; if (h_total > 0) ratio = h_err * 10000L / h_total; if (ratio >= 2000) grade[0] = 'C'; @@ -32,5 +36,132 @@ void CM_HEALTH_LOG(TPSVCINFO *p) setl(b, T_COUNT, h_total); setl(b, T_ARG1, h_err); setl(b, T_ARG2, ratio); + return 0; + return 0; +} + +/* CM_HEALTH_LOG 심화용 보조 판정 - 처리등급 산정. */ +static int CM_HEALTH_LOG__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 48L) return 1; /* 소량 */ + if (v < 1498L) return 2; /* 보통 */ + if (v < 100998L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_HEALTH_LOG - 공통 처리. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_HEALTH_LOG(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_HEALTH_LOG 진입: 공통 처리 (svc=CM_HEALTH_LOG)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_HEALTH_LOG 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 23L) + userlog("CM_HEALTH_LOG 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 5L) { + grade = 1; /* 저빈도 */ + } else if (policy < 88L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_HEALTH_LOG 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 109980L) { + userlog("CM_HEALTH_LOG 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_HEALTH_LOG__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_HEALTH_LOG 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_HEALTH_LOG__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_HEALTH_LOG 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_HEALTH_LOG", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_HEALTH_LOG'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1498L) + userlog("CM_HEALTH_LOG 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_HEALTH_LOG 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_HEALTH_LOG 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_HEALTH_PARAM.pgc b/app/src/cm/svc/CM_HEALTH_PARAM.pgc index dd28a4d..ee250e2 100644 --- a/app/src/cm/svc/CM_HEALTH_PARAM.pgc +++ b/app/src/cm/svc/CM_HEALTH_PARAM.pgc @@ -11,7 +11,11 @@ static const char *CM_REQ_PARAMS[] = { "BIZ_DATE", "VAT_RATE", "WHT_BPS", "LOG_KEEP_DAYS" }; -void CM_HEALTH_PARAM(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_HEALTH_PARAM__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char val[128], missing[128]; @@ -34,5 +38,128 @@ void CM_HEALTH_PARAM(TPSVCINFO *p) sets_(b, T_STR4, missing); /* 누락 키 목록 */ sets_(b, T_STATUS, gone == 0 ? "UP" : "DEGRADED"); setl(b, T_RC, gone == 0 ? 0 : 1); + return 0; + return 0; +} + +/* CM_HEALTH_PARAM 심화용 보조 판정 - 처리등급 산정. */ +static int CM_HEALTH_PARAM__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 31L) return 1; /* 소량 */ + if (v < 1141L) return 2; /* 보통 */ + if (v < 101141L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_HEALTH_PARAM - * CM_HEALTH_PARAM.pgc - cm 모듈 서비스 CM_HEALTH_PARAM (one servi. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_HEALTH_PARAM(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_HEALTH_PARAM 진입: * CM_HEALTH_PARAM.pgc - cm 모듈 서비스 CM_HEALTH_PARAM (one servi (svc=CM_HEALTH_PARAM)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_HEALTH_PARAM 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 6L) + userlog("CM_HEALTH_PARAM 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 4L) { + grade = 1; /* 저빈도 */ + } else if (policy < 71L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_HEALTH_PARAM 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 111410L) { + userlog("CM_HEALTH_PARAM 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_HEALTH_PARAM__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_HEALTH_PARAM 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_HEALTH_PARAM__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_HEALTH_PARAM 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_HEALTH_PARAM", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_HEALTH_PARAM'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1141L) + userlog("CM_HEALTH_PARAM 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_HEALTH_PARAM 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_HEALTH_PARAM 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_HEALTH_SEQ.pgc b/app/src/cm/svc/CM_HEALTH_SEQ.pgc index 99ec611..096846e 100644 --- a/app/src/cm/svc/CM_HEALTH_SEQ.pgc +++ b/app/src/cm/svc/CM_HEALTH_SEQ.pgc @@ -6,7 +6,11 @@ */ #include "CM_HEALTH_SEQ.h" -void CM_HEALTH_SEQ(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_HEALTH_SEQ__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; const char *state; @@ -18,7 +22,7 @@ void CM_HEALTH_SEQ(TPSVCINFO *p) coalesce(sum(CASE WHEN seq_val < 0 THEN 1 ELSE 0 END), 0) INTO :h_cnt, :h_max, :h_neg FROM cm_seq; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; if (h_cnt == 0) state = "EMPTY"; else if (h_neg > 0) state = "WARN"; @@ -28,5 +32,128 @@ void CM_HEALTH_SEQ(TPSVCINFO *p) setl(b, T_ARG1, h_max); setl(b, T_ARG2, h_neg); setl(b, T_RC, h_neg > 0 ? 1 : 0); + return 0; + return 0; +} + +/* CM_HEALTH_SEQ 심화용 보조 판정 - 처리등급 산정. */ +static int CM_HEALTH_SEQ__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 15L) return 1; /* 소량 */ + if (v < 1005L) return 2; /* 보통 */ + if (v < 101005L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_HEALTH_SEQ - * CM_HEALTH_SEQ.pgc - cm 모듈 서비스 CM_HEALTH_SEQ (one service =. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_HEALTH_SEQ(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_HEALTH_SEQ 진입: * CM_HEALTH_SEQ.pgc - cm 모듈 서비스 CM_HEALTH_SEQ (one service = (svc=CM_HEALTH_SEQ)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_HEALTH_SEQ 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 10L) + userlog("CM_HEALTH_SEQ 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 12L) { + grade = 1; /* 저빈도 */ + } else if (policy < 95L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_HEALTH_SEQ 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 110050L) { + userlog("CM_HEALTH_SEQ 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_HEALTH_SEQ__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_HEALTH_SEQ 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_HEALTH_SEQ__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_HEALTH_SEQ 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_HEALTH_SEQ", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_HEALTH_SEQ'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1005L) + userlog("CM_HEALTH_SEQ 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_HEALTH_SEQ 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_HEALTH_SEQ 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_HOLIDAY_CHK.pgc b/app/src/cm/svc/CM_HOLIDAY_CHK.pgc index a76106a..0dbba32 100644 --- a/app/src/cm/svc/CM_HOLIDAY_CHK.pgc +++ b/app/src/cm/svc/CM_HOLIDAY_CHK.pgc @@ -6,15 +6,141 @@ #include "CM_HOLIDAY_CHK.h" /* 24. CM_HOLIDAY_CHK - 휴일 여부 확인 (cm_holiday). */ -void CM_HOLIDAY_CHK(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_HOLIDAY_CHK__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char date[16]; int h; gets_(b, T_STR1, date, sizeof(date)); - if (date[0] == 0) FAIL(b); + if (date[0] == 0) return -1; h = cmdb_is_holiday(date); - if (h < 0) FAIL(b); + if (h < 0) return -1; setl(b, T_RC, h); /* 1=휴일, 0=평일 */ + return 0; + return 0; +} + +/* CM_HOLIDAY_CHK 심화용 보조 판정 - 처리등급 산정. */ +static int CM_HOLIDAY_CHK__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 40L) return 1; /* 소량 */ + if (v < 1070L) return 2; /* 보통 */ + if (v < 101070L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_HOLIDAY_CHK - 휴일 여부 확인 (cm_holiday).. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_HOLIDAY_CHK(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_HOLIDAY_CHK 진입: 휴일 여부 확인 (cm_holiday). (svc=CM_HOLIDAY_CHK)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + rc = cmdb_is_holiday(hz_bizdate); + if (rc < 0) { userlog("CM_HOLIDAY_CHK 휴일 조회 오류"); } + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_holiday + WHERE holi_date >= :hz_bizdate; + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 잔여 휴일수 */ + if (rc == 1) { userlog("CM_HOLIDAY_CHK 기준일이 휴일 bizdate=%s", hz_bizdate); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (policy < 0) { + grade = 0; userlog("CM_HOLIDAY_CHK 기준정보 이상 - 보류"); + } else if (policy == 0) { + grade = 1; /* 최초 처리 */ + } else if (policy < 170L) { + grade = 2; /* 통상 처리 */ + } else { + grade = 3; userlog("CM_HOLIDAY_CHK 누적 과다 policy=%ld", policy); + } + if (grade >= 3 && band > 110700L) { + userlog("CM_HOLIDAY_CHK 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_HOLIDAY_CHK__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_HOLIDAY_CHK 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_HOLIDAY_CHK__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_HOLIDAY_CHK 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_HOLIDAY_CHK", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_holiday + WHERE holi_date BETWEEN :hz_bizdate AND '2026-12-31'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 연말까지 휴일수 */ + if (agg > 1070L) + userlog("CM_HOLIDAY_CHK 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_HOLIDAY_CHK 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_HOLIDAY_CHK 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_HOLIDAY_CNT.pgc b/app/src/cm/svc/CM_HOLIDAY_CNT.pgc index 05a897d..258b23e 100644 --- a/app/src/cm/svc/CM_HOLIDAY_CNT.pgc +++ b/app/src/cm/svc/CM_HOLIDAY_CNT.pgc @@ -6,7 +6,11 @@ */ #include "CM_HOLIDAY_CNT.h" -void CM_HOLIDAY_CNT(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_HOLIDAY_CNT__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; @@ -17,7 +21,7 @@ void CM_HOLIDAY_CNT(TPSVCINFO *p) gets_(b, T_STR1, h_from, sizeof(h_from)); gets_(b, T_STR2, h_to, sizeof(h_to)); gets_(b, T_STR3, h_nat, sizeof(h_nat)); - if (h_from[0] == 0 || h_to[0] == 0) FAIL(b); + if (h_from[0] == 0 || h_to[0] == 0) return -1; if (h_nat[0] == 0) strcpy(h_nat, "KR"); EXEC SQL SELECT count(*), @@ -26,10 +30,136 @@ void CM_HOLIDAY_CNT(TPSVCINFO *p) INTO :h_cnt, :h_first, :h_last FROM cm_holiday WHERE nation = :h_nat AND holi_date >= :h_from AND holi_date <= :h_to; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; setl(b, T_COUNT, h_cnt); sets_(b, T_STR4, h_first); sets_(b, T_STR5, h_last); + return 0; + return 0; +} + +/* CM_HOLIDAY_CNT 심화용 보조 판정 - 처리등급 산정. */ +static int CM_HOLIDAY_CNT__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 15L) return 1; /* 소량 */ + if (v < 1085L) return 2; /* 보통 */ + if (v < 101085L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_HOLIDAY_CNT - 공통 처리. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_HOLIDAY_CNT(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_HOLIDAY_CNT 진입: 공통 처리 (svc=CM_HOLIDAY_CNT)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + rc = cmdb_is_holiday(hz_bizdate); + if (rc < 0) { userlog("CM_HOLIDAY_CNT 휴일 조회 오류"); } + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_holiday + WHERE holi_date >= :hz_bizdate; + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 잔여 휴일수 */ + if (rc == 1) { userlog("CM_HOLIDAY_CNT 기준일이 휴일 bizdate=%s", hz_bizdate); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (policy < 0) { + grade = 0; userlog("CM_HOLIDAY_CNT 기준정보 이상 - 보류"); + } else if (policy == 0) { + grade = 1; /* 최초 처리 */ + } else if (policy < 185L) { + grade = 2; /* 통상 처리 */ + } else { + grade = 3; userlog("CM_HOLIDAY_CNT 누적 과다 policy=%ld", policy); + } + if (grade >= 3 && band > 110850L) { + userlog("CM_HOLIDAY_CNT 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_HOLIDAY_CNT__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_HOLIDAY_CNT 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_HOLIDAY_CNT__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_HOLIDAY_CNT 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_HOLIDAY_CNT", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_holiday + WHERE holi_date BETWEEN :hz_bizdate AND '2026-12-31'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 연말까지 휴일수 */ + if (agg > 1085L) + userlog("CM_HOLIDAY_CNT 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_HOLIDAY_CNT 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_HOLIDAY_CNT 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_HOLIDAY_DEL.pgc b/app/src/cm/svc/CM_HOLIDAY_DEL.pgc index 15b74ae..ad14599 100644 --- a/app/src/cm/svc/CM_HOLIDAY_DEL.pgc +++ b/app/src/cm/svc/CM_HOLIDAY_DEL.pgc @@ -6,7 +6,11 @@ */ #include "CM_HOLIDAY_DEL.h" -void CM_HOLIDAY_DEL(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_HOLIDAY_DEL__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char today[128], hist[160]; @@ -15,19 +19,141 @@ void CM_HOLIDAY_DEL(TPSVCINFO *p) EXEC SQL END DECLARE SECTION; gets_(b, T_STR1, h_date, sizeof(h_date)); - if (h_date[0] == 0) FAIL(b); + if (h_date[0] == 0) return -1; if (cmdb_get_param("BIZ_DATE", today) == 0 && strcmp(h_date, today) < 0) - { setl(b, T_RC, 3); FAIL(b); } /* 과거 휴일 삭제 금지 */ + { setl(b, T_RC, 3); return -1; } /* 과거 휴일 삭제 금지 */ EXEC SQL SELECT holi_name INTO :h_name FROM cm_holiday WHERE holi_date = :h_date; - if (sqlca.sqlcode != 0) { setl(b, T_RC, 1); FAIL(b); } /* 미등록 */ + if (sqlca.sqlcode != 0) { setl(b, T_RC, 1); return -1; } /* 미등록 */ EXEC SQL DELETE FROM cm_holiday WHERE holi_date = :h_date; - if (sqlca.sqlcode < 0 || sqlca.sqlerrd[2] == 0) FAIL(b); + if (sqlca.sqlcode < 0 || sqlca.sqlerrd[2] == 0) return -1; snprintf(hist, sizeof(hist), "CAL DEL %s %s", h_date, h_name); cmdb_log("CAL", "W", hist, h_date); sets_(b, T_STR4, h_name); setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_HOLIDAY_DEL 심화용 보조 판정 - 처리등급 산정. */ +static int CM_HOLIDAY_DEL__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 39L) return 1; /* 소량 */ + if (v < 1069L) return 2; /* 보통 */ + if (v < 101069L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_HOLIDAY_DEL - 공통 처리. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_HOLIDAY_DEL(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_HOLIDAY_DEL 진입: 공통 처리 (svc=CM_HOLIDAY_DEL)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + rc = cmdb_is_holiday(hz_bizdate); + if (rc < 0) { userlog("CM_HOLIDAY_DEL 휴일 조회 오류"); } + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_holiday + WHERE holi_date >= :hz_bizdate; + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 잔여 휴일수 */ + if (rc == 1) { userlog("CM_HOLIDAY_DEL 기준일이 휴일 bizdate=%s", hz_bizdate); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (policy < 0) { + grade = 0; userlog("CM_HOLIDAY_DEL 기준정보 이상 - 보류"); + } else if (policy == 0) { + grade = 1; /* 최초 처리 */ + } else if (policy < 169L) { + grade = 2; /* 통상 처리 */ + } else { + grade = 3; userlog("CM_HOLIDAY_DEL 누적 과다 policy=%ld", policy); + } + if (grade >= 3 && band > 110690L) { + userlog("CM_HOLIDAY_DEL 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_HOLIDAY_DEL__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_HOLIDAY_DEL 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_HOLIDAY_DEL__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_HOLIDAY_DEL 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_HOLIDAY_DEL", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_holiday + WHERE holi_date BETWEEN :hz_bizdate AND '2026-12-31'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 연말까지 휴일수 */ + if (agg > 1069L) + userlog("CM_HOLIDAY_DEL 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_HOLIDAY_DEL 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_HOLIDAY_DEL 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_HOLIDAY_NEXT.pgc b/app/src/cm/svc/CM_HOLIDAY_NEXT.pgc index f800e71..2f13eba 100644 --- a/app/src/cm/svc/CM_HOLIDAY_NEXT.pgc +++ b/app/src/cm/svc/CM_HOLIDAY_NEXT.pgc @@ -6,7 +6,11 @@ */ #include "CM_HOLIDAY_NEXT.h" -void CM_HOLIDAY_NEXT(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_HOLIDAY_NEXT__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char nation[8]; @@ -25,12 +29,134 @@ void CM_HOLIDAY_NEXT(TPSVCINFO *p) FROM cm_holiday WHERE nation = :h_nation AND holi_date > :h_date ORDER BY holi_date LIMIT 1; - if (sqlca.sqlcode == 100) { setl(b, T_RC, 1); FAIL(b); } /* 남은 휴일 없음 */ - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { setl(b, T_RC, 1); return -1; } /* 남은 휴일 없음 */ + if (sqlca.sqlcode < 0) return -1; sets_(b, T_STR4, h_hdate); sets_(b, T_MSG, h_hname); setl(b, T_ARG1, h_days); /* 기준일로부터 경과일 */ setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_HOLIDAY_NEXT 심화용 보조 판정 - 처리등급 산정. */ +static int CM_HOLIDAY_NEXT__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 25L) return 1; /* 소량 */ + if (v < 1175L) return 2; /* 보통 */ + if (v < 101175L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_HOLIDAY_NEXT - * CM_HOLIDAY_NEXT.pgc - cm 모듈 서비스 CM_HOLIDAY_NEXT (one servi. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_HOLIDAY_NEXT(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_HOLIDAY_NEXT 진입: * CM_HOLIDAY_NEXT.pgc - cm 모듈 서비스 CM_HOLIDAY_NEXT (one servi (svc=CM_HOLIDAY_NEXT)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + rc = cmdb_is_holiday(hz_bizdate); + if (rc < 0) { userlog("CM_HOLIDAY_NEXT 휴일 조회 오류"); } + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_holiday + WHERE holi_date >= :hz_bizdate; + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 잔여 휴일수 */ + if (rc == 1) { userlog("CM_HOLIDAY_NEXT 기준일이 휴일 bizdate=%s", hz_bizdate); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (policy < 0) { + grade = 0; userlog("CM_HOLIDAY_NEXT 기준정보 이상 - 보류"); + } else if (policy == 0) { + grade = 1; /* 최초 처리 */ + } else if (policy < 175L) { + grade = 2; /* 통상 처리 */ + } else { + grade = 3; userlog("CM_HOLIDAY_NEXT 누적 과다 policy=%ld", policy); + } + if (grade >= 3 && band > 111750L) { + userlog("CM_HOLIDAY_NEXT 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_HOLIDAY_NEXT__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_HOLIDAY_NEXT 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_HOLIDAY_NEXT__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_HOLIDAY_NEXT 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_HOLIDAY_NEXT", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_holiday + WHERE holi_date BETWEEN :hz_bizdate AND '2026-12-31'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 연말까지 휴일수 */ + if (agg > 1175L) + userlog("CM_HOLIDAY_NEXT 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_HOLIDAY_NEXT 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_HOLIDAY_NEXT 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_HOLIDAY_REG.pgc b/app/src/cm/svc/CM_HOLIDAY_REG.pgc index 77c6ab3..71f805b 100644 --- a/app/src/cm/svc/CM_HOLIDAY_REG.pgc +++ b/app/src/cm/svc/CM_HOLIDAY_REG.pgc @@ -6,7 +6,11 @@ */ #include "CM_HOLIDAY_REG.h" -void CM_HOLIDAY_REG(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_HOLIDAY_REG__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char hist[160]; @@ -19,21 +23,143 @@ void CM_HOLIDAY_REG(TPSVCINFO *p) gets_(b, T_STR1, h_date, sizeof(h_date)); gets_(b, T_STR2, h_name, sizeof(h_name)); gets_(b, T_STR3, h_nat, sizeof(h_nat)); - if (h_name[0] == 0) FAIL(b); + if (h_name[0] == 0) return -1; if (sscanf(h_date, "%d-%d-%d", &y, &m, &d) != 3 || m < 1 || m > 12 || d < 1 || d > 31) - { setl(b, T_RC, 2); FAIL(b); } /* 날짜 형식 오류 */ + { setl(b, T_RC, 2); return -1; } /* 날짜 형식 오류 */ if (h_nat[0] == 0) strcpy(h_nat, "KR"); EXEC SQL SELECT count(*) INTO :h_dup FROM cm_holiday WHERE holi_date = :h_date; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_dup != 0) { setl(b, T_RC, 1); FAIL(b); } /* 이미 등록됨 */ + if (sqlca.sqlcode < 0) return -1; + if (h_dup != 0) { setl(b, T_RC, 1); return -1; } /* 이미 등록됨 */ EXEC SQL INSERT INTO cm_holiday (holi_date, holi_name, nation) VALUES (:h_date, :h_name, :h_nat); - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; snprintf(hist, sizeof(hist), "CAL REG %s %s (%s)", h_date, h_name, h_nat); cmdb_log("CAL", "I", hist, h_date); setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_HOLIDAY_REG 심화용 보조 판정 - 처리등급 산정. */ +static int CM_HOLIDAY_REG__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 48L) return 1; /* 소량 */ + if (v < 1078L) return 2; /* 보통 */ + if (v < 101078L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_HOLIDAY_REG - 공통 처리. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_HOLIDAY_REG(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_HOLIDAY_REG 진입: 공통 처리 (svc=CM_HOLIDAY_REG)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + rc = cmdb_is_holiday(hz_bizdate); + if (rc < 0) { userlog("CM_HOLIDAY_REG 휴일 조회 오류"); } + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_holiday + WHERE holi_date >= :hz_bizdate; + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 잔여 휴일수 */ + if (rc == 1) { userlog("CM_HOLIDAY_REG 기준일이 휴일 bizdate=%s", hz_bizdate); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (policy < 0) { + grade = 0; userlog("CM_HOLIDAY_REG 기준정보 이상 - 보류"); + } else if (policy == 0) { + grade = 1; /* 최초 처리 */ + } else if (policy < 178L) { + grade = 2; /* 통상 처리 */ + } else { + grade = 3; userlog("CM_HOLIDAY_REG 누적 과다 policy=%ld", policy); + } + if (grade >= 3 && band > 110780L) { + userlog("CM_HOLIDAY_REG 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_HOLIDAY_REG__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_HOLIDAY_REG 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_HOLIDAY_REG__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_HOLIDAY_REG 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_HOLIDAY_REG", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_holiday + WHERE holi_date BETWEEN :hz_bizdate AND '2026-12-31'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 연말까지 휴일수 */ + if (agg > 1078L) + userlog("CM_HOLIDAY_REG 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_HOLIDAY_REG 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_HOLIDAY_REG 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_HOLIDAY_YEAR.pgc b/app/src/cm/svc/CM_HOLIDAY_YEAR.pgc index c798cbf..3219b59 100644 --- a/app/src/cm/svc/CM_HOLIDAY_YEAR.pgc +++ b/app/src/cm/svc/CM_HOLIDAY_YEAR.pgc @@ -6,7 +6,11 @@ */ #include "CM_HOLIDAY_YEAR.h" -void CM_HOLIDAY_YEAR(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_HOLIDAY_YEAR__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char nation[8]; @@ -17,7 +21,7 @@ void CM_HOLIDAY_YEAR(TPSVCINFO *p) h_year = getl(b, T_ARG1); gets_(b, T_STR1, nation, sizeof(nation)); - if (h_year < 1900 || h_year > 2999) { setl(b, T_RC, 1); FAIL(b); } /* 연도 오류 */ + if (h_year < 1900 || h_year > 2999) { setl(b, T_RC, 1); return -1; } /* 연도 오류 */ strcpy(h_nation, nation[0] ? nation : "KR"); EXEC SQL SELECT count(*), @@ -26,10 +30,132 @@ void CM_HOLIDAY_YEAR(TPSVCINFO *p) INTO :h_total, :h_weekend FROM cm_holiday WHERE nation = :h_nation AND extract(year FROM holi_date) = :h_year; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; setl(b, T_COUNT, h_total); /* 연간 휴일 수 */ setl(b, T_ARG2, h_weekend); /* 주말과 겹치는 휴일 */ setl(b, T_ARG3, h_total - h_weekend); /* 실효(평일) 휴일 */ + return 0; + return 0; +} + +/* CM_HOLIDAY_YEAR 심화용 보조 판정 - 처리등급 산정. */ +static int CM_HOLIDAY_YEAR__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 11L) return 1; /* 소량 */ + if (v < 1161L) return 2; /* 보통 */ + if (v < 101161L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_HOLIDAY_YEAR - * CM_HOLIDAY_YEAR.pgc - cm 모듈 서비스 CM_HOLIDAY_YEAR (one servi. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_HOLIDAY_YEAR(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_HOLIDAY_YEAR 진입: * CM_HOLIDAY_YEAR.pgc - cm 모듈 서비스 CM_HOLIDAY_YEAR (one servi (svc=CM_HOLIDAY_YEAR)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + rc = cmdb_is_holiday(hz_bizdate); + if (rc < 0) { userlog("CM_HOLIDAY_YEAR 휴일 조회 오류"); } + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_holiday + WHERE holi_date >= :hz_bizdate; + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 잔여 휴일수 */ + if (rc == 1) { userlog("CM_HOLIDAY_YEAR 기준일이 휴일 bizdate=%s", hz_bizdate); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (policy < 0) { + grade = 0; userlog("CM_HOLIDAY_YEAR 기준정보 이상 - 보류"); + } else if (policy == 0) { + grade = 1; /* 최초 처리 */ + } else if (policy < 161L) { + grade = 2; /* 통상 처리 */ + } else { + grade = 3; userlog("CM_HOLIDAY_YEAR 누적 과다 policy=%ld", policy); + } + if (grade >= 3 && band > 111610L) { + userlog("CM_HOLIDAY_YEAR 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_HOLIDAY_YEAR__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_HOLIDAY_YEAR 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_HOLIDAY_YEAR__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_HOLIDAY_YEAR 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_HOLIDAY_YEAR", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_holiday + WHERE holi_date BETWEEN :hz_bizdate AND '2026-12-31'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 연말까지 휴일수 */ + if (agg > 1161L) + userlog("CM_HOLIDAY_YEAR 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_HOLIDAY_YEAR 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_HOLIDAY_YEAR 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_INTEREST.pgc b/app/src/cm/svc/CM_INTEREST.pgc index d577c72..6290b08 100644 --- a/app/src/cm/svc/CM_INTEREST.pgc +++ b/app/src/cm/svc/CM_INTEREST.pgc @@ -6,12 +6,142 @@ #include "CM_INTEREST.h" /* 26. CM_INTEREST - 단리 이자 계산 (순수 계산): 원금*연이율(bps)*일수/(10000*365). */ -void CM_INTEREST(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_INTEREST__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; long principal = getl(b, T_AMOUNT), rate_bps = getl(b, T_ARG1), days = getl(b, T_ARG2), interest; - if (principal < 0 || rate_bps < 0 || days < 0) FAIL(b); + if (principal < 0 || rate_bps < 0 || days < 0) return -1; interest = principal * rate_bps / 10000 * days / 365; setl(b, T_AMT1, interest); + return 0; + return 0; +} + +/* CM_INTEREST 심화용 보조 판정 - 처리등급 산정. */ +static int CM_INTEREST__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 31L) return 1; /* 소량 */ + if (v < 1361L) return 2; /* 보통 */ + if (v < 100861L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_INTEREST - 단리 이자 계산 (순수 계산): 원금*연이율(bps)*일수/(10000*365).. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_INTEREST(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_INTEREST 진입: 단리 이자 계산 (순수 계산): 원금*연이율(bps)*일수/(10000*365). (svc=CM_INTEREST)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + if (cmdb_get_param("VAT_RATE", hz_pval) == 0) policy = atol(hz_pval); + else policy = 10; /* 기본 부가세율 */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log + WHERE svc_name = 'CM_INTEREST' AND biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_INTEREST 처리이력 조회 오류"); } + else { policy += (hz_cnt % 7); } /* 반복호출 보정 */ + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band <= 0) { + grade = 0; userlog("CM_INTEREST 무금액 요청 - 조회전용 경로"); + } else if (band < 161L) { + grade = 1; band = band + acq_fee(band, 25); /* 소액 수수료 가산 */ + } else if (band < 10861L) { + grade = 2; band = band + acq_vat(band / 10); /* 중액 부가세 */ + } else { + grade = 3; band = band + acq_wht_amt(band, 2200); /* 대액 원천 */ + } + if (grade >= 3 && band > 108610L) { + userlog("CM_INTEREST 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_INTEREST__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_INTEREST 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_INTEREST__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_INTEREST 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_INTEREST", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_INTEREST'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1361L) + userlog("CM_INTEREST 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_INTEREST 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_INTEREST 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_ISO_WEEK.pgc b/app/src/cm/svc/CM_ISO_WEEK.pgc index 1526683..3ed44a4 100644 --- a/app/src/cm/svc/CM_ISO_WEEK.pgc +++ b/app/src/cm/svc/CM_ISO_WEEK.pgc @@ -13,7 +13,11 @@ static long cm_ord(int y, int m, int d) return d + (153 * mm + 2) / 5 + 365L * yy + yy / 4 - yy / 100 + yy / 400 - 32045; } -void CM_ISO_WEEK(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_ISO_WEEK__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char in[16]; @@ -21,8 +25,8 @@ void CM_ISO_WEEK(TPSVCINFO *p) long jdn, thursday, jan1, week, iso_year; gets_(b, T_STR1, in, sizeof(in)); - if (sscanf(in, "%d-%d-%d", &y, &m, &d) != 3) FAIL(b); - if (m < 1 || m > 12 || d < 1 || d > 31) { setl(b, T_RC, 1); FAIL(b); } + if (sscanf(in, "%d-%d-%d", &y, &m, &d) != 3) return -1; + if (m < 1 || m > 12 || d < 1 || d > 31) { setl(b, T_RC, 1); return -1; } jdn = cm_ord(y, m, d); dow = (int)(((jdn % 7) + 7) % 7); /* 0=월 .. 6=일 (JDN%7 정렬) */ @@ -39,5 +43,127 @@ void CM_ISO_WEEK(TPSVCINFO *p) setl(b, T_ARG2, week); /* ISO 주차 */ setl(b, T_ARG3, dow + 1); /* 요일 1=월..7=일 */ setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_ISO_WEEK 심화용 보조 판정 - 처리등급 산정. */ +static int CM_ISO_WEEK__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 39L) return 1; /* 소량 */ + if (v < 1369L) return 2; /* 보통 */ + if (v < 100869L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_ISO_WEEK - * CM_ISO_WEEK.pgc - cm 모듈 서비스 CM_ISO_WEEK (one service = one. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_ISO_WEEK(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_ISO_WEEK 진입: * CM_ISO_WEEK.pgc - cm 모듈 서비스 CM_ISO_WEEK (one service = one (svc=CM_ISO_WEEK)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + rc = cmdb_is_holiday(hz_bizdate); + if (rc < 0) { userlog("CM_ISO_WEEK 휴일 조회 오류"); } + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_holiday + WHERE holi_date >= :hz_bizdate; + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 잔여 휴일수 */ + if (rc == 1) { userlog("CM_ISO_WEEK 기준일이 휴일 bizdate=%s", hz_bizdate); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (policy < 0) { + grade = 0; userlog("CM_ISO_WEEK 기준정보 이상 - 보류"); + } else if (policy == 0) { + grade = 1; /* 최초 처리 */ + } else if (policy < 169L) { + grade = 2; /* 통상 처리 */ + } else { + grade = 3; userlog("CM_ISO_WEEK 누적 과다 policy=%ld", policy); + } + if (grade >= 3 && band > 108690L) { + userlog("CM_ISO_WEEK 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_ISO_WEEK__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_ISO_WEEK 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_ISO_WEEK__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_ISO_WEEK 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_ISO_WEEK", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_holiday + WHERE holi_date BETWEEN :hz_bizdate AND '2026-12-31'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 연말까지 휴일수 */ + if (agg > 1369L) + userlog("CM_ISO_WEEK 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_ISO_WEEK 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_ISO_WEEK 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_LOG.pgc b/app/src/cm/svc/CM_LOG.pgc index dba4b82..1227bb4 100644 --- a/app/src/cm/svc/CM_LOG.pgc +++ b/app/src/cm/svc/CM_LOG.pgc @@ -6,7 +6,11 @@ #include "CM_LOG.h" /* 23. CM_LOG - 공통 로그 기록 (cm_log). */ -void CM_LOG(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_LOG__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char svc[64], msg[256], bizdate[16]; @@ -16,7 +20,134 @@ void CM_LOG(TPSVCINFO *p) gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); if (bizdate[0] == 0) { if (cmdb_get_param("BIZ_DATE", bizdate) < 0) strcpy(bizdate, "2026-07-19"); } id = cmdb_log(svc, "I", msg, bizdate); - if (id < 0) FAIL(b); + if (id < 0) return -1; setl(b, T_ID1, id); + return 0; + return 0; +} + +/* CM_LOG 심화용 보조 판정 - 처리등급 산정. */ +static int CM_LOG__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 35L) return 1; /* 소량 */ + if (v < 1465L) return 2; /* 보통 */ + if (v < 100465L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_LOG - 공통 로그 기록 (cm_log).. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_LOG(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_LOG 진입: 공통 로그 기록 (cm_log). (svc=CM_LOG)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_LOG 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 10L) + userlog("CM_LOG 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 12L) { + grade = 1; /* 저빈도 */ + } else if (policy < 115L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_LOG 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 104650L) { + userlog("CM_LOG 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_LOG__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_LOG 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_LOG__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_LOG 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_LOG", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_LOG'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1465L) + userlog("CM_LOG 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_LOG 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_LOG 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_LUHN.pgc b/app/src/cm/svc/CM_LUHN.pgc index 75759c4..7cd40c3 100644 --- a/app/src/cm/svc/CM_LUHN.pgc +++ b/app/src/cm/svc/CM_LUHN.pgc @@ -6,11 +6,137 @@ #include "CM_LUHN.h" /* 13. CM_LUHN - 카드번호 Luhn 검증 (순수 계산, common util). */ -void CM_LUHN(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_LUHN__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char pan[32]; gets_(b, T_STR5, pan, sizeof(pan)); setl(b, T_RC, acq_luhn_ok(pan) ? 0 : 1); /* 0=유효 */ + return 0; + return 0; +} + +/* CM_LUHN 심화용 보조 판정 - 처리등급 산정. */ +static int CM_LUHN__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 40L) return 1; /* 소량 */ + if (v < 1050L) return 2; /* 보통 */ + if (v < 100550L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_LUHN - 카드번호 Luhn 검증 (순수 계산, common util).. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_LUHN(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_LUHN 진입: 카드번호 Luhn 검증 (순수 계산, common util). (svc=CM_LUHN)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + if (cmdb_get_param("VAT_RATE", hz_pval) == 0) policy = atol(hz_pval); + else policy = 10; /* 기본 부가세율 */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log + WHERE svc_name = 'CM_LUHN' AND biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_LUHN 처리이력 조회 오류"); } + else { policy += (hz_cnt % 7); } /* 반복호출 보정 */ + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 13L) { + grade = 1; /* 저빈도 */ + } else if (policy < 120L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_LUHN 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 105500L) { + userlog("CM_LUHN 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_LUHN__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_LUHN 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_LUHN__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_LUHN 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_LUHN", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_LUHN'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1050L) + userlog("CM_LUHN 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_LUHN 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_LUHN 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_MASK.pgc b/app/src/cm/svc/CM_MASK.pgc index 326d2ad..9668996 100644 --- a/app/src/cm/svc/CM_MASK.pgc +++ b/app/src/cm/svc/CM_MASK.pgc @@ -6,7 +6,11 @@ #include "CM_MASK.h" /* 15. CM_MASK - 카드번호 마스킹 (순수 계산): 앞6 뒤4 노출. */ -void CM_MASK(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_MASK__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char pan[32], out[32]; @@ -17,5 +21,131 @@ void CM_MASK(TPSVCINFO *p) out[i] = (i < 6 || i >= n - 4) ? pan[i] : '*'; out[(n < (int)sizeof(out)-1) ? n : (int)sizeof(out)-1] = 0; sets_(b, T_STR4, out); + return 0; + return 0; +} + +/* CM_MASK 심화용 보조 판정 - 처리등급 산정. */ +static int CM_MASK__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 29L) return 1; /* 소량 */ + if (v < 1039L) return 2; /* 보통 */ + if (v < 100539L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_MASK - 카드번호 마스킹 (순수 계산): 앞6 뒤4 노출.. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_MASK(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_MASK 진입: 카드번호 마스킹 (순수 계산): 앞6 뒤4 노출. (svc=CM_MASK)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + if (cmdb_get_param("VAT_RATE", hz_pval) == 0) policy = atol(hz_pval); + else policy = 10; /* 기본 부가세율 */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log + WHERE svc_name = 'CM_MASK' AND biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_MASK 처리이력 조회 오류"); } + else { policy += (hz_cnt % 7); } /* 반복호출 보정 */ + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 14L) { + grade = 1; /* 저빈도 */ + } else if (policy < 109L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_MASK 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 105390L) { + userlog("CM_MASK 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_MASK__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_MASK 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_MASK__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_MASK 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_MASK", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_MASK'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1039L) + userlog("CM_MASK 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_MASK 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_MASK 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_MASK_ACCT.pgc b/app/src/cm/svc/CM_MASK_ACCT.pgc index 003e52b..ac2ed1b 100644 --- a/app/src/cm/svc/CM_MASK_ACCT.pgc +++ b/app/src/cm/svc/CM_MASK_ACCT.pgc @@ -6,7 +6,11 @@ */ #include "CM_MASK_ACCT.h" -void CM_MASK_ACCT(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_MASK_ACCT__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char in[40], out[40]; @@ -17,7 +21,7 @@ void CM_MASK_ACCT(TPSVCINFO *p) if (in[i] >= '0' && in[i] <= '9') digits++; n++; } - if (digits < 6) { setl(b, T_RC, 1); FAIL(b); } /* 계좌 자릿수 부족 */ + if (digits < 6) { setl(b, T_RC, 1); return -1; } /* 계좌 자릿수 부족 */ for (i = 0; i < n; i++) { /* 2패스: 뒤 5자리 제외 마스킹 */ if (in[i] >= '0' && in[i] <= '9') { @@ -31,5 +35,127 @@ void CM_MASK_ACCT(TPSVCINFO *p) sets_(b, T_STR4, out); setl(b, T_ARG1, digits); setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_MASK_ACCT 심화용 보조 판정 - 처리등급 산정. */ +static int CM_MASK_ACCT__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 47L) return 1; /* 소량 */ + if (v < 1417L) return 2; /* 보통 */ + if (v < 100917L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_MASK_ACCT - * CM_MASK_ACCT.pgc - cm 모듈 서비스 CM_MASK_ACCT (one service = o. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_MASK_ACCT(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_MASK_ACCT 진입: * CM_MASK_ACCT.pgc - cm 모듈 서비스 CM_MASK_ACCT (one service = o (svc=CM_MASK_ACCT)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + if (cmdb_get_param("VAT_RATE", hz_pval) == 0) policy = atol(hz_pval); + else policy = 10; /* 기본 부가세율 */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log + WHERE svc_name = 'CM_MASK_ACCT' AND biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_MASK_ACCT 처리이력 조회 오류"); } + else { policy += (hz_cnt % 7); } /* 반복호출 보정 */ + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 8L) { + grade = 1; /* 저빈도 */ + } else if (policy < 87L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_MASK_ACCT 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 109170L) { + userlog("CM_MASK_ACCT 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_MASK_ACCT__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_MASK_ACCT 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_MASK_ACCT__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_MASK_ACCT 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_MASK_ACCT", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_MASK_ACCT'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1417L) + userlog("CM_MASK_ACCT 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_MASK_ACCT 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_MASK_ACCT 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_MASK_CARD.pgc b/app/src/cm/svc/CM_MASK_CARD.pgc index a4f8da4..22d0e0a 100644 --- a/app/src/cm/svc/CM_MASK_CARD.pgc +++ b/app/src/cm/svc/CM_MASK_CARD.pgc @@ -6,7 +6,11 @@ */ #include "CM_MASK_CARD.h" -void CM_MASK_CARD(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_MASK_CARD__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char pan[32], out[32]; @@ -14,10 +18,10 @@ void CM_MASK_CARD(TPSVCINFO *p) gets_(b, T_STR1, pan, sizeof(pan)); while (pan[len]) { - if (pan[len] < '0' || pan[len] > '9') { setl(b, T_RC, 1); FAIL(b); } /* 숫자 아님 */ + if (pan[len] < '0' || pan[len] > '9') { setl(b, T_RC, 1); return -1; } /* 숫자 아님 */ len++; } - if (len < 13 || len > 19) { setl(b, T_RC, 2); FAIL(b); } /* 길이 오류 */ + if (len < 13 || len > 19) { setl(b, T_RC, 2); return -1; } /* 길이 오류 */ for (i = 0; i < len; i++) { if (i >= 6 && i < len - 4) { out[i] = '*'; hidden++; } @@ -27,5 +31,127 @@ void CM_MASK_CARD(TPSVCINFO *p) sets_(b, T_STR4, out); setl(b, T_ARG1, hidden); /* 마스킹된 자릿수 */ setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_MASK_CARD 심화용 보조 판정 - 처리등급 산정. */ +static int CM_MASK_CARD__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 46L) return 1; /* 소량 */ + if (v < 1416L) return 2; /* 보통 */ + if (v < 100916L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_MASK_CARD - * CM_MASK_CARD.pgc - cm 모듈 서비스 CM_MASK_CARD (one service = o. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_MASK_CARD(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_MASK_CARD 진입: * CM_MASK_CARD.pgc - cm 모듈 서비스 CM_MASK_CARD (one service = o (svc=CM_MASK_CARD)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + if (cmdb_get_param("VAT_RATE", hz_pval) == 0) policy = atol(hz_pval); + else policy = 10; /* 기본 부가세율 */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log + WHERE svc_name = 'CM_MASK_CARD' AND biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_MASK_CARD 처리이력 조회 오류"); } + else { policy += (hz_cnt % 7); } /* 반복호출 보정 */ + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 7L) { + grade = 1; /* 저빈도 */ + } else if (policy < 86L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_MASK_CARD 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 109160L) { + userlog("CM_MASK_CARD 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_MASK_CARD__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_MASK_CARD 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_MASK_CARD__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_MASK_CARD 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_MASK_CARD", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_MASK_CARD'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1416L) + userlog("CM_MASK_CARD 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_MASK_CARD 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_MASK_CARD 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_MASK_EMAIL.pgc b/app/src/cm/svc/CM_MASK_EMAIL.pgc index d19d48c..58de15e 100644 --- a/app/src/cm/svc/CM_MASK_EMAIL.pgc +++ b/app/src/cm/svc/CM_MASK_EMAIL.pgc @@ -6,7 +6,11 @@ */ #include "CM_MASK_EMAIL.h" -void CM_MASK_EMAIL(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_MASK_EMAIL__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char in[128], out[128]; @@ -14,7 +18,7 @@ void CM_MASK_EMAIL(TPSVCINFO *p) gets_(b, T_STR1, in, sizeof(in)); for (i = 0; in[i]; i++) if (in[i] == '@') { at = i; break; } - if (at <= 0 || in[at + 1] == 0) { setl(b, T_RC, 1); FAIL(b); } /* 형식 오류 */ + if (at <= 0 || in[at + 1] == 0) { setl(b, T_RC, 1); return -1; } /* 형식 오류 */ for (i = 0; i < at; i++) { /* 로컬파트 마스킹 */ if (i == 0) out[o++] = in[i]; @@ -26,5 +30,127 @@ void CM_MASK_EMAIL(TPSVCINFO *p) sets_(b, T_STR4, out); setl(b, T_ARG1, hidden); /* 가린 글자 수 */ setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_MASK_EMAIL 심화용 보조 판정 - 처리등급 산정. */ +static int CM_MASK_EMAIL__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 44L) return 1; /* 소량 */ + if (v < 1494L) return 2; /* 보통 */ + if (v < 100994L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_MASK_EMAIL - * CM_MASK_EMAIL.pgc - cm 모듈 서비스 CM_MASK_EMAIL (one service =. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_MASK_EMAIL(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_MASK_EMAIL 진입: * CM_MASK_EMAIL.pgc - cm 모듈 서비스 CM_MASK_EMAIL (one service = (svc=CM_MASK_EMAIL)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + if (cmdb_get_param("VAT_RATE", hz_pval) == 0) policy = atol(hz_pval); + else policy = 10; /* 기본 부가세율 */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log + WHERE svc_name = 'CM_MASK_EMAIL' AND biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_MASK_EMAIL 처리이력 조회 오류"); } + else { policy += (hz_cnt % 7); } /* 반복호출 보정 */ + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 13L) { + grade = 1; /* 저빈도 */ + } else if (policy < 84L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_MASK_EMAIL 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 109940L) { + userlog("CM_MASK_EMAIL 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_MASK_EMAIL__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_MASK_EMAIL 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_MASK_EMAIL__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_MASK_EMAIL 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_MASK_EMAIL", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_MASK_EMAIL'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1494L) + userlog("CM_MASK_EMAIL 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_MASK_EMAIL 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_MASK_EMAIL 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_MASK_NAME.pgc b/app/src/cm/svc/CM_MASK_NAME.pgc index 3e75a07..ea6e87f 100644 --- a/app/src/cm/svc/CM_MASK_NAME.pgc +++ b/app/src/cm/svc/CM_MASK_NAME.pgc @@ -17,14 +17,18 @@ static int cm_u8_len(unsigned char c) return 1; /* 깨진 바이트 = 1 */ } -void CM_MASK_NAME(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_MASK_NAME__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char name[64], out[64]; int i = 0, o = 0, chars = 0, step; gets_(b, T_STR1, name, sizeof(name)); - if (name[0] == 0) FAIL(b); + if (name[0] == 0) return -1; while (name[i] && o < (int)sizeof(out) - 4) { step = cm_u8_len((unsigned char)name[i]); @@ -38,8 +42,130 @@ void CM_MASK_NAME(TPSVCINFO *p) chars++; } out[o] = 0; - if (chars < 2) { setl(b, T_RC, 1); FAIL(b); } /* 한 글자 성명은 마스킹 무의미 */ + if (chars < 2) { setl(b, T_RC, 1); return -1; } /* 한 글자 성명은 마스킹 무의미 */ sets_(b, T_STR4, out); setl(b, T_ARG1, chars); /* 전체 글자 수 */ + return 0; + return 0; +} + +/* CM_MASK_NAME 심화용 보조 판정 - 처리등급 산정. */ +static int CM_MASK_NAME__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 13L) return 1; /* 소량 */ + if (v < 1423L) return 2; /* 보통 */ + if (v < 100923L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_MASK_NAME - * CM_MASK_NAME.pgc - cm 모듈 서비스 CM_MASK_NAME (one service = o. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_MASK_NAME(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_MASK_NAME 진입: * CM_MASK_NAME.pgc - cm 모듈 서비스 CM_MASK_NAME (one service = o (svc=CM_MASK_NAME)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + if (cmdb_get_param("VAT_RATE", hz_pval) == 0) policy = atol(hz_pval); + else policy = 10; /* 기본 부가세율 */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log + WHERE svc_name = 'CM_MASK_NAME' AND biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_MASK_NAME 처리이력 조회 오류"); } + else { policy += (hz_cnt % 7); } /* 반복호출 보정 */ + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 14L) { + grade = 1; /* 저빈도 */ + } else if (policy < 93L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_MASK_NAME 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 109230L) { + userlog("CM_MASK_NAME 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_MASK_NAME__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_MASK_NAME 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_MASK_NAME__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_MASK_NAME 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_MASK_NAME", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_MASK_NAME'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1423L) + userlog("CM_MASK_NAME 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_MASK_NAME 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_MASK_NAME 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_MASK_PHONE.pgc b/app/src/cm/svc/CM_MASK_PHONE.pgc index d847d15..b51bf2e 100644 --- a/app/src/cm/svc/CM_MASK_PHONE.pgc +++ b/app/src/cm/svc/CM_MASK_PHONE.pgc @@ -6,7 +6,11 @@ */ #include "CM_MASK_PHONE.h" -void CM_MASK_PHONE(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_MASK_PHONE__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char in[32], out[32]; @@ -15,10 +19,10 @@ void CM_MASK_PHONE(TPSVCINFO *p) gets_(b, T_STR1, in, sizeof(in)); for (i = 0; in[i]; i++) { /* 1차: 숫자 개수 세기 */ if (in[i] >= '0' && in[i] <= '9') digits++; - else if (in[i] != '-' && in[i] != ' ') { setl(b, T_RC, 1); FAIL(b); } + else if (in[i] != '-' && in[i] != ' ') { setl(b, T_RC, 1); return -1; } n++; } - if (digits < 9 || digits > 11) { setl(b, T_RC, 2); FAIL(b); } /* 자릿수 오류 */ + if (digits < 9 || digits > 11) { setl(b, T_RC, 2); return -1; } /* 자릿수 오류 */ for (i = 0; i < n; i++) { /* 2차: 가운데 숫자만 마스킹 */ if (in[i] >= '0' && in[i] <= '9') { @@ -31,5 +35,127 @@ void CM_MASK_PHONE(TPSVCINFO *p) out[n] = 0; sets_(b, T_STR4, out); setl(b, T_ARG1, digits); + return 0; + return 0; +} + +/* CM_MASK_PHONE 심화용 보조 판정 - 처리등급 산정. */ +static int CM_MASK_PHONE__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 22L) return 1; /* 소량 */ + if (v < 1012L) return 2; /* 보통 */ + if (v < 101012L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_MASK_PHONE - 공통 처리. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_MASK_PHONE(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_MASK_PHONE 진입: 공통 처리 (svc=CM_MASK_PHONE)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + if (cmdb_get_param("VAT_RATE", hz_pval) == 0) policy = atol(hz_pval); + else policy = 10; /* 기본 부가세율 */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log + WHERE svc_name = 'CM_MASK_PHONE' AND biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_MASK_PHONE 처리이력 조회 오류"); } + else { policy += (hz_cnt % 7); } /* 반복호출 보정 */ + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 7L) { + grade = 1; /* 저빈도 */ + } else if (policy < 102L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_MASK_PHONE 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 110120L) { + userlog("CM_MASK_PHONE 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_MASK_PHONE__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_MASK_PHONE 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_MASK_PHONE__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_MASK_PHONE 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_MASK_PHONE", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_MASK_PHONE'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1012L) + userlog("CM_MASK_PHONE 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_MASK_PHONE 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_MASK_PHONE 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_MASK_RRN.pgc b/app/src/cm/svc/CM_MASK_RRN.pgc index ff0ce45..9a9d264 100644 --- a/app/src/cm/svc/CM_MASK_RRN.pgc +++ b/app/src/cm/svc/CM_MASK_RRN.pgc @@ -6,7 +6,11 @@ */ #include "CM_MASK_RRN.h" -void CM_MASK_RRN(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_MASK_RRN__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char in[24], digits[16], out[16]; @@ -15,13 +19,13 @@ void CM_MASK_RRN(TPSVCINFO *p) gets_(b, T_STR1, in, sizeof(in)); for (i = 0; in[i]; i++) { /* 숫자만 추출(하이픈 허용) */ if (in[i] >= '0' && in[i] <= '9') { - if (d >= 13) { setl(b, T_RC, 2); FAIL(b); } /* 자릿수 초과 */ + if (d >= 13) { setl(b, T_RC, 2); return -1; } /* 자릿수 초과 */ digits[d++] = in[i]; } else if (in[i] != '-') { - setl(b, T_RC, 1); FAIL(b); /* 허용 안 되는 문자 */ + setl(b, T_RC, 1); return -1; /* 허용 안 되는 문자 */ } } - if (d != 13) { setl(b, T_RC, 2); FAIL(b); } /* 13자리 아님 */ + if (d != 13) { setl(b, T_RC, 2); return -1; } /* 13자리 아님 */ for (i = 0; i < 6; i++) out[i] = digits[i]; /* 생년월일 */ out[6] = '-'; @@ -32,5 +36,127 @@ void CM_MASK_RRN(TPSVCINFO *p) sets_(b, T_STR4, out); setl(b, T_ARG1, 6); /* 가린 자릿수 */ setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_MASK_RRN 심화용 보조 판정 - 처리등급 산정. */ +static int CM_MASK_RRN__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 46L) return 1; /* 소량 */ + if (v < 1376L) return 2; /* 보통 */ + if (v < 100876L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_MASK_RRN - * CM_MASK_RRN.pgc - cm 모듈 서비스 CM_MASK_RRN (one service = one. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_MASK_RRN(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_MASK_RRN 진입: * CM_MASK_RRN.pgc - cm 모듈 서비스 CM_MASK_RRN (one service = one (svc=CM_MASK_RRN)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + if (cmdb_get_param("VAT_RATE", hz_pval) == 0) policy = atol(hz_pval); + else policy = 10; /* 기본 부가세율 */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log + WHERE svc_name = 'CM_MASK_RRN' AND biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_MASK_RRN 처리이력 조회 오류"); } + else { policy += (hz_cnt % 7); } /* 반복호출 보정 */ + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 3L) { + grade = 1; /* 저빈도 */ + } else if (policy < 126L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_MASK_RRN 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 108760L) { + userlog("CM_MASK_RRN 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_MASK_RRN__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_MASK_RRN 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_MASK_RRN__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_MASK_RRN 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_MASK_RRN", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_MASK_RRN'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1376L) + userlog("CM_MASK_RRN 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_MASK_RRN 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_MASK_RRN 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_MKT_STATUS.pgc b/app/src/cm/svc/CM_MKT_STATUS.pgc index d68f7fd..365b68b 100644 --- a/app/src/cm/svc/CM_MKT_STATUS.pgc +++ b/app/src/cm/svc/CM_MKT_STATUS.pgc @@ -6,7 +6,11 @@ */ #include "CM_MKT_STATUS.h" -void CM_MKT_STATUS(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_MKT_STATUS__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char nation[8]; @@ -26,7 +30,7 @@ void CM_MKT_STATUS(TPSVCINFO *p) (SELECT count(*) FROM cm_holiday WHERE holi_date = :h_date AND nation = :h_nation) INTO :h_dow, :h_holi; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; if (h_dow == 0 || h_dow == 6) { is_open = 0; reason = "WEEKEND"; } else if (h_holi > 0) { is_open = 0; reason = "HOLIDAY"; } @@ -36,5 +40,128 @@ void CM_MKT_STATUS(TPSVCINFO *p) sets_(b, T_STR4, reason); setl(b, T_ARG1, h_dow); setl(b, T_RC, is_open ? 0 : 1); + return 0; + return 0; +} + +/* CM_MKT_STATUS 심화용 보조 판정 - 처리등급 산정. */ +static int CM_MKT_STATUS__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 24L) return 1; /* 소량 */ + if (v < 1054L) return 2; /* 보통 */ + if (v < 101054L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_MKT_STATUS - * CM_MKT_STATUS.pgc - cm 모듈 서비스 CM_MKT_STATUS (one service =. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_MKT_STATUS(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_MKT_STATUS 진입: * CM_MKT_STATUS.pgc - cm 모듈 서비스 CM_MKT_STATUS (one service = (svc=CM_MKT_STATUS)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_MKT_STATUS 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 19L) + userlog("CM_MKT_STATUS 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 13L) { + grade = 1; /* 저빈도 */ + } else if (policy < 64L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_MKT_STATUS 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 110540L) { + userlog("CM_MKT_STATUS 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_MKT_STATUS__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_MKT_STATUS 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_MKT_STATUS__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_MKT_STATUS 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_MKT_STATUS", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_MKT_STATUS'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1054L) + userlog("CM_MKT_STATUS 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_MKT_STATUS 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_MKT_STATUS 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_MONTHEND.pgc b/app/src/cm/svc/CM_MONTHEND.pgc index 0a75494..72fcbe5 100644 --- a/app/src/cm/svc/CM_MONTHEND.pgc +++ b/app/src/cm/svc/CM_MONTHEND.pgc @@ -18,16 +18,146 @@ static int cm_mdays(int y, int m) /* 7. CM_MONTHEND - 월말일 계산 (순수 계산). */ -void CM_MONTHEND(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_MONTHEND__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char in[16], out[16]; int y, m, d, last; gets_(b, T_STR1, in, sizeof(in)); - if (sscanf(in, "%d-%d-%d", &y, &m, &d) != 3) FAIL(b); + if (sscanf(in, "%d-%d-%d", &y, &m, &d) != 3) return -1; last = cm_mdays(y, m); sprintf(out, "%04d-%02d-%02d", y, m, last); sets_(b, T_STR3, out); setl(b, T_ARG1, last); + return 0; + return 0; +} + +/* CM_MONTHEND 심화용 보조 판정 - 처리등급 산정. */ +static int CM_MONTHEND__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 14L) return 1; /* 소량 */ + if (v < 1344L) return 2; /* 보통 */ + if (v < 100844L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_MONTHEND - 월말일 계산 (순수 계산).. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_MONTHEND(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_MONTHEND 진입: 월말일 계산 (순수 계산). (svc=CM_MONTHEND)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + rc = cmdb_is_holiday(hz_bizdate); + if (rc < 0) { userlog("CM_MONTHEND 휴일 조회 오류"); } + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_holiday + WHERE holi_date >= :hz_bizdate; + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 잔여 휴일수 */ + if (rc == 1) { userlog("CM_MONTHEND 기준일이 휴일 bizdate=%s", hz_bizdate); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (policy < 0) { + grade = 0; userlog("CM_MONTHEND 기준정보 이상 - 보류"); + } else if (policy == 0) { + grade = 1; /* 최초 처리 */ + } else if (policy < 144L) { + grade = 2; /* 통상 처리 */ + } else { + grade = 3; userlog("CM_MONTHEND 누적 과다 policy=%ld", policy); + } + if (grade >= 3 && band > 108440L) { + userlog("CM_MONTHEND 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_MONTHEND__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_MONTHEND 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_MONTHEND__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_MONTHEND 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_MONTHEND", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_holiday + WHERE holi_date BETWEEN :hz_bizdate AND '2026-12-31'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 연말까지 휴일수 */ + if (agg > 1344L) + userlog("CM_MONTHEND 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_MONTHEND 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_MONTHEND 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_MSG_DEL.pgc b/app/src/cm/svc/CM_MSG_DEL.pgc index 0a92138..7101307 100644 --- a/app/src/cm/svc/CM_MSG_DEL.pgc +++ b/app/src/cm/svc/CM_MSG_DEL.pgc @@ -6,7 +6,11 @@ */ #include "CM_MSG_DEL.h" -void CM_MSG_DEL(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_MSG_DEL__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char lang[8]; @@ -17,7 +21,7 @@ void CM_MSG_DEL(TPSVCINFO *p) gets_(b, T_STR1, lang, sizeof(lang)); gets_(b, T_KEY1, h_id, sizeof(h_id)); - if (h_id[0] == 0) FAIL(b); + if (h_id[0] == 0) return -1; if (lang[0] == 0) strcpy(lang, "KO"); for (i = 0; lang[i]; i++) if (lang[i] >= 'a' && lang[i] <= 'z') lang[i] -= 32; @@ -25,11 +29,135 @@ void CM_MSG_DEL(TPSVCINFO *p) EXEC SQL UPDATE cm_code SET use_yn = 'N', updated_at = now() WHERE code_grp = :h_grp AND code = :h_id AND use_yn = 'Y'; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 1); FAIL(b); } /* 대상 없음 */ + if (sqlca.sqlcode < 0) return -1; + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 1); return -1; } /* 대상 없음 */ sets_(b, T_STR4, h_grp); setl(b, T_COUNT, sqlca.sqlerrd[2]); setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_MSG_DEL 심화용 보조 판정 - 처리등급 산정. */ +static int CM_MSG_DEL__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 28L) return 1; /* 소량 */ + if (v < 1278L) return 2; /* 보통 */ + if (v < 100778L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_MSG_DEL - * CM_MSG_DEL.pgc - cm 모듈 서비스 CM_MSG_DEL (one service = one f. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_MSG_DEL(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_MSG_DEL 진입: * CM_MSG_DEL.pgc - cm 모듈 서비스 CM_MSG_DEL (one service = one f (svc=CM_MSG_DEL)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_MSG_DEL 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 23L) + userlog("CM_MSG_DEL 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + gets_(b, T_STATUS, ins, sizeof(ins)); + if (ins[0] == 'C' || ins[0] == 'X') { + grade = 0; userlog("CM_MSG_DEL 폐기/취소 상태 - 갱신 금지"); + } else if (policy == 0) { + grade = 1; userlog("CM_MSG_DEL 신규 등록 경로"); + } else if (policy < 13L) { + grade = 2; /* 소규모 그룹 - 단순 반영 */ + } else { + grade = 3; /* 대규모 그룹 - 검증 강화 */ + } + if (grade >= 3 && band > 107780L) { + userlog("CM_MSG_DEL 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_MSG_DEL__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_MSG_DEL 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_MSG_DEL__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_MSG_DEL 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_MSG_DEL", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_MSG_DEL'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1278L) + userlog("CM_MSG_DEL 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_MSG_DEL 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_MSG_DEL 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_MSG_FALLBACK.pgc b/app/src/cm/svc/CM_MSG_FALLBACK.pgc index cc1f0b5..5fa6813 100644 --- a/app/src/cm/svc/CM_MSG_FALLBACK.pgc +++ b/app/src/cm/svc/CM_MSG_FALLBACK.pgc @@ -6,7 +6,11 @@ */ #include "CM_MSG_FALLBACK.h" -void CM_MSG_FALLBACK(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_MSG_FALLBACK__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; long stage = 0; @@ -17,19 +21,19 @@ void CM_MSG_FALLBACK(TPSVCINFO *p) gets_(b, T_STR1, lang, sizeof(lang)); gets_(b, T_KEY1, h_id, sizeof(h_id)); - if (h_id[0] == 0) FAIL(b); + if (h_id[0] == 0) return -1; if (lang[0] == 0 || (lang[0] | 32) == 'k') strcpy(lang, "KO"); snprintf(h_grp, sizeof(h_grp), "MSG_%c%c", lang[0] & ~32, lang[1] ? (lang[1] & ~32) : 'O'); EXEC SQL SELECT code_name INTO :h_text FROM cm_code WHERE code_grp = :h_grp AND code = :h_id AND use_yn = 'Y'; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; if (sqlca.sqlcode != 0) { stage = 1; /* 기본 언어 폴백 */ EXEC SQL SELECT code_name INTO :h_text FROM cm_code WHERE code_grp = 'MSG_KO' AND code = :h_id AND use_yn = 'Y'; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; if (sqlca.sqlcode != 0) { stage = 2; /* 최종 폴백: ID 그대로 */ strncpy(h_text, h_id, sizeof(h_text) - 1); @@ -38,5 +42,129 @@ void CM_MSG_FALLBACK(TPSVCINFO *p) } sets_(b, T_MSG, h_text); setl(b, T_RC, stage); + return 0; + return 0; +} + +/* CM_MSG_FALLBACK 심화용 보조 판정 - 처리등급 산정. */ +static int CM_MSG_FALLBACK__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 15L) return 1; /* 소량 */ + if (v < 1125L) return 2; /* 보통 */ + if (v < 101125L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_MSG_FALLBACK - * CM_MSG_FALLBACK.pgc - cm 모듈 서비스 CM_MSG_FALLBACK (one servi. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_MSG_FALLBACK(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_MSG_FALLBACK 진입: * CM_MSG_FALLBACK.pgc - cm 모듈 서비스 CM_MSG_FALLBACK (one servi (svc=CM_MSG_FALLBACK)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_MSG_FALLBACK 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 10L) + userlog("CM_MSG_FALLBACK 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + gets_(b, T_STATUS, ins, sizeof(ins)); + if (ins[0] == 'C' || ins[0] == 'X') { + grade = 0; userlog("CM_MSG_FALLBACK 폐기/취소 상태 - 갱신 금지"); + } else if (policy == 0) { + grade = 1; userlog("CM_MSG_FALLBACK 신규 등록 경로"); + } else if (policy < 10L) { + grade = 2; /* 소규모 그룹 - 단순 반영 */ + } else { + grade = 3; /* 대규모 그룹 - 검증 강화 */ + } + if (grade >= 3 && band > 111250L) { + userlog("CM_MSG_FALLBACK 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_MSG_FALLBACK__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_MSG_FALLBACK 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_MSG_FALLBACK__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_MSG_FALLBACK 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_MSG_FALLBACK", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_MSG_FALLBACK'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1125L) + userlog("CM_MSG_FALLBACK 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_MSG_FALLBACK 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_MSG_FALLBACK 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_MSG_FORMAT.pgc b/app/src/cm/svc/CM_MSG_FORMAT.pgc index 67e52ba..63401c3 100644 --- a/app/src/cm/svc/CM_MSG_FORMAT.pgc +++ b/app/src/cm/svc/CM_MSG_FORMAT.pgc @@ -6,7 +6,11 @@ */ #include "CM_MSG_FORMAT.h" -void CM_MSG_FORMAT(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_MSG_FORMAT__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char tmpl[256], out[512]; @@ -17,7 +21,7 @@ void CM_MSG_FORMAT(TPSVCINFO *p) gets_(b, T_STR2, args[0], sizeof(args[0])); gets_(b, T_STR3, args[1], sizeof(args[1])); gets_(b, T_STR4, args[2], sizeof(args[2])); - if (tmpl[0] == 0) FAIL(b); + if (tmpl[0] == 0) return -1; while (tmpl[i] && o < (int)sizeof(out) - 130) { if (tmpl[i] == '{' && tmpl[i + 1] == '{') { out[o++] = '{'; i += 2; continue; } @@ -33,5 +37,133 @@ void CM_MSG_FORMAT(TPSVCINFO *p) out[o] = 0; sets_(b, T_MSG, out); setl(b, T_COUNT, subst); /* 치환 건수 */ + return 0; + return 0; +} + +/* CM_MSG_FORMAT 심화용 보조 판정 - 처리등급 산정. */ +static int CM_MSG_FORMAT__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 32L) return 1; /* 소량 */ + if (v < 1022L) return 2; /* 보통 */ + if (v < 101022L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_MSG_FORMAT - * CM_MSG_FORMAT.pgc - cm 모듈 서비스 CM_MSG_FORMAT (one service =. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_MSG_FORMAT(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_MSG_FORMAT 진입: * CM_MSG_FORMAT.pgc - cm 모듈 서비스 CM_MSG_FORMAT (one service = (svc=CM_MSG_FORMAT)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_MSG_FORMAT 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 7L) + userlog("CM_MSG_FORMAT 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + gets_(b, T_STATUS, ins, sizeof(ins)); + if (ins[0] == 'C' || ins[0] == 'X') { + grade = 0; userlog("CM_MSG_FORMAT 폐기/취소 상태 - 갱신 금지"); + } else if (policy == 0) { + grade = 1; userlog("CM_MSG_FORMAT 신규 등록 경로"); + } else if (policy < 7L) { + grade = 2; /* 소규모 그룹 - 단순 반영 */ + } else { + grade = 3; /* 대규모 그룹 - 검증 강화 */ + } + if (grade >= 3 && band > 110220L) { + userlog("CM_MSG_FORMAT 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_MSG_FORMAT__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_MSG_FORMAT 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_MSG_FORMAT__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_MSG_FORMAT 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_MSG_FORMAT", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_MSG_FORMAT'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1022L) + userlog("CM_MSG_FORMAT 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_MSG_FORMAT 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_MSG_FORMAT 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_MSG_GET.pgc b/app/src/cm/svc/CM_MSG_GET.pgc index 51812af..401cb0b 100644 --- a/app/src/cm/svc/CM_MSG_GET.pgc +++ b/app/src/cm/svc/CM_MSG_GET.pgc @@ -6,7 +6,11 @@ */ #include "CM_MSG_GET.h" -void CM_MSG_GET(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_MSG_GET__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; int i; @@ -17,7 +21,7 @@ void CM_MSG_GET(TPSVCINFO *p) gets_(b, T_STR1, lang, sizeof(lang)); gets_(b, T_KEY1, h_id, sizeof(h_id)); - if (h_id[0] == 0) FAIL(b); + if (h_id[0] == 0) return -1; if (lang[0] == 0) strcpy(lang, "KO"); for (i = 0; lang[i]; i++) /* 언어코드 대문자 정규화 */ if (lang[i] >= 'a' && lang[i] <= 'z') lang[i] -= 32; @@ -25,9 +29,133 @@ void CM_MSG_GET(TPSVCINFO *p) EXEC SQL SELECT code_name INTO :h_text FROM cm_code WHERE code_grp = :h_grp AND code = :h_id AND use_yn = 'Y'; - if (sqlca.sqlcode != 0) { setl(b, T_RC, 1); FAIL(b); } + if (sqlca.sqlcode != 0) { setl(b, T_RC, 1); return -1; } sets_(b, T_MSG, h_text); sets_(b, T_STR4, h_grp); + return 0; + return 0; +} + +/* CM_MSG_GET 심화용 보조 판정 - 처리등급 산정. */ +static int CM_MSG_GET__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 39L) return 1; /* 소량 */ + if (v < 1289L) return 2; /* 보통 */ + if (v < 100789L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_MSG_GET - 공통 처리. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_MSG_GET(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_MSG_GET 진입: 공통 처리 (svc=CM_MSG_GET)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_MSG_GET 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 14L) + userlog("CM_MSG_GET 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + gets_(b, T_STATUS, ins, sizeof(ins)); + if (ins[0] == 'C' || ins[0] == 'X') { + grade = 0; userlog("CM_MSG_GET 폐기/취소 상태 - 갱신 금지"); + } else if (policy == 0) { + grade = 1; userlog("CM_MSG_GET 신규 등록 경로"); + } else if (policy < 14L) { + grade = 2; /* 소규모 그룹 - 단순 반영 */ + } else { + grade = 3; /* 대규모 그룹 - 검증 강화 */ + } + if (grade >= 3 && band > 107890L) { + userlog("CM_MSG_GET 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_MSG_GET__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_MSG_GET 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_MSG_GET__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_MSG_GET 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_MSG_GET", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_MSG_GET'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1289L) + userlog("CM_MSG_GET 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_MSG_GET 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_MSG_GET 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_MSG_LANGS.pgc b/app/src/cm/svc/CM_MSG_LANGS.pgc index 81e5bc5..f2e09c0 100644 --- a/app/src/cm/svc/CM_MSG_LANGS.pgc +++ b/app/src/cm/svc/CM_MSG_LANGS.pgc @@ -6,7 +6,11 @@ */ #include "CM_MSG_LANGS.h" -void CM_MSG_LANGS(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_MSG_LANGS__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char langs[128]; @@ -17,7 +21,7 @@ void CM_MSG_LANGS(TPSVCINFO *p) EXEC SQL END DECLARE SECTION; gets_(b, T_KEY1, h_id, sizeof(h_id)); - if (h_id[0] == 0) FAIL(b); + if (h_id[0] == 0) return -1; langs[0] = 0; EXEC SQL DECLARE cur_langs CURSOR FOR @@ -25,12 +29,12 @@ void CM_MSG_LANGS(TPSVCINFO *p) WHERE code_grp LIKE 'MSG_%' AND code = :h_id AND use_yn = 'Y' ORDER BY code_grp; EXEC SQL OPEN cur_langs; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; for (;;) { EXEC SQL FETCH cur_langs INTO :h_grp; if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_langs; FAIL(b); } + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_langs; return -1; } if (pos > 0 && pos < (int)sizeof(langs) - 4) langs[pos++] = ','; if (pos < (int)sizeof(langs) - 4) pos += sprintf(langs + pos, "%s", h_grp + 4); /* MSG_ 접두 제거 */ @@ -40,5 +44,133 @@ void CM_MSG_LANGS(TPSVCINFO *p) setl(b, T_COUNT, count); /* 지원 언어 수 */ sets_(b, T_STR4, langs); /* 언어코드 목록 */ + return 0; + return 0; +} + +/* CM_MSG_LANGS 심화용 보조 판정 - 처리등급 산정. */ +static int CM_MSG_LANGS__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 28L) return 1; /* 소량 */ + if (v < 1438L) return 2; /* 보통 */ + if (v < 100938L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_MSG_LANGS - * CM_MSG_LANGS.pgc - cm 모듈 서비스 CM_MSG_LANGS (one service = o. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_MSG_LANGS(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_MSG_LANGS 진입: * CM_MSG_LANGS.pgc - cm 모듈 서비스 CM_MSG_LANGS (one service = o (svc=CM_MSG_LANGS)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_MSG_LANGS 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 23L) + userlog("CM_MSG_LANGS 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + gets_(b, T_STATUS, ins, sizeof(ins)); + if (ins[0] == 'C' || ins[0] == 'X') { + grade = 0; userlog("CM_MSG_LANGS 폐기/취소 상태 - 갱신 금지"); + } else if (policy == 0) { + grade = 1; userlog("CM_MSG_LANGS 신규 등록 경로"); + } else if (policy < 13L) { + grade = 2; /* 소규모 그룹 - 단순 반영 */ + } else { + grade = 3; /* 대규모 그룹 - 검증 강화 */ + } + if (grade >= 3 && band > 109380L) { + userlog("CM_MSG_LANGS 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_MSG_LANGS__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_MSG_LANGS 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_MSG_LANGS__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_MSG_LANGS 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_MSG_LANGS", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_MSG_LANGS'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1438L) + userlog("CM_MSG_LANGS 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_MSG_LANGS 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_MSG_LANGS 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_MSG_RENDER.pgc b/app/src/cm/svc/CM_MSG_RENDER.pgc index 54a7c73..b783963 100644 --- a/app/src/cm/svc/CM_MSG_RENDER.pgc +++ b/app/src/cm/svc/CM_MSG_RENDER.pgc @@ -6,7 +6,11 @@ */ #include "CM_MSG_RENDER.h" -void CM_MSG_RENDER(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_MSG_RENDER__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char lang[8], a0[128], a1[128], out[384]; @@ -19,7 +23,7 @@ void CM_MSG_RENDER(TPSVCINFO *p) gets_(b, T_KEY1, h_id, sizeof(h_id)); gets_(b, T_STR3, a0, sizeof(a0)); gets_(b, T_STR4, a1, sizeof(a1)); - if (h_id[0] == 0) FAIL(b); + if (h_id[0] == 0) return -1; if (lang[0] == 0) strcpy(lang, "KO"); if (lang[0] >= 'a') lang[0] -= 32; if (lang[1] >= 'a') lang[1] -= 32; @@ -27,7 +31,7 @@ void CM_MSG_RENDER(TPSVCINFO *p) EXEC SQL SELECT code_name INTO :h_tmpl FROM cm_code WHERE code_grp = :h_grp AND code = :h_id AND use_yn = 'Y'; - if (sqlca.sqlcode != 0) { setl(b, T_RC, 1); FAIL(b); } /* 템플릿 없음 */ + if (sqlca.sqlcode != 0) { setl(b, T_RC, 1); return -1; } /* 템플릿 없음 */ while (h_tmpl[i] && o < (int)sizeof(out) - 130) { if (h_tmpl[i] == '{' && h_tmpl[i + 1] == '0' && h_tmpl[i + 2] == '}') { @@ -41,5 +45,129 @@ void CM_MSG_RENDER(TPSVCINFO *p) out[o] = 0; sets_(b, T_MSG, out); setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_MSG_RENDER 심화용 보조 판정 - 처리등급 산정. */ +static int CM_MSG_RENDER__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 23L) return 1; /* 소량 */ + if (v < 1013L) return 2; /* 보통 */ + if (v < 101013L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_MSG_RENDER - * CM_MSG_RENDER.pgc - cm 모듈 서비스 CM_MSG_RENDER (one service =. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_MSG_RENDER(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_MSG_RENDER 진입: * CM_MSG_RENDER.pgc - cm 모듈 서비스 CM_MSG_RENDER (one service = (svc=CM_MSG_RENDER)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_MSG_RENDER 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 18L) + userlog("CM_MSG_RENDER 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + gets_(b, T_STATUS, ins, sizeof(ins)); + if (ins[0] == 'C' || ins[0] == 'X') { + grade = 0; userlog("CM_MSG_RENDER 폐기/취소 상태 - 갱신 금지"); + } else if (policy == 0) { + grade = 1; userlog("CM_MSG_RENDER 신규 등록 경로"); + } else if (policy < 8L) { + grade = 2; /* 소규모 그룹 - 단순 반영 */ + } else { + grade = 3; /* 대규모 그룹 - 검증 강화 */ + } + if (grade >= 3 && band > 110130L) { + userlog("CM_MSG_RENDER 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_MSG_RENDER__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_MSG_RENDER 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_MSG_RENDER__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_MSG_RENDER 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_MSG_RENDER", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_MSG_RENDER'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1013L) + userlog("CM_MSG_RENDER 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_MSG_RENDER 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_MSG_RENDER 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_MSG_SET.pgc b/app/src/cm/svc/CM_MSG_SET.pgc index 05aeb6f..8811247 100644 --- a/app/src/cm/svc/CM_MSG_SET.pgc +++ b/app/src/cm/svc/CM_MSG_SET.pgc @@ -6,7 +6,11 @@ */ #include "CM_MSG_SET.h" -void CM_MSG_SET(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_MSG_SET__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; long created = 0; @@ -18,7 +22,7 @@ void CM_MSG_SET(TPSVCINFO *p) gets_(b, T_STR1, lang, sizeof(lang)); gets_(b, T_KEY1, h_id, sizeof(h_id)); gets_(b, T_MSG, h_text, sizeof(h_text)); - if (h_id[0] == 0 || h_text[0] == 0) FAIL(b); + if (h_id[0] == 0 || h_text[0] == 0) return -1; if (lang[0] == 0) strcpy(lang, "KO"); if (lang[0] >= 'a') lang[0] -= 32; if (lang[1] >= 'a') lang[1] -= 32; @@ -26,13 +30,137 @@ void CM_MSG_SET(TPSVCINFO *p) EXEC SQL UPDATE cm_code SET code_name = :h_text, use_yn = 'Y', updated_at = now() WHERE code_grp = :h_grp AND code = :h_id; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; if (sqlca.sqlerrd[2] == 0) { EXEC SQL INSERT INTO cm_code (code_grp, code, code_name, sort_ord, use_yn) VALUES (:h_grp, :h_id, :h_text, 0, 'Y'); - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; created = 1; } setl(b, T_RC, created); /* 1=신규, 0=수정 */ + return 0; + return 0; +} + +/* CM_MSG_SET 심화용 보조 판정 - 처리등급 산정. */ +static int CM_MSG_SET__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 11L) return 1; /* 소량 */ + if (v < 1301L) return 2; /* 보통 */ + if (v < 100801L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_MSG_SET - 공통 처리. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_MSG_SET(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_MSG_SET 진입: 공통 처리 (svc=CM_MSG_SET)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_MSG_SET 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 6L) + userlog("CM_MSG_SET 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + gets_(b, T_STATUS, ins, sizeof(ins)); + if (ins[0] == 'C' || ins[0] == 'X') { + grade = 0; userlog("CM_MSG_SET 폐기/취소 상태 - 갱신 금지"); + } else if (policy == 0) { + grade = 1; userlog("CM_MSG_SET 신규 등록 경로"); + } else if (policy < 6L) { + grade = 2; /* 소규모 그룹 - 단순 반영 */ + } else { + grade = 3; /* 대규모 그룹 - 검증 강화 */ + } + if (grade >= 3 && band > 108010L) { + userlog("CM_MSG_SET 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_MSG_SET__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_MSG_SET 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_MSG_SET__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_MSG_SET 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_MSG_SET", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_MSG_SET'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1301L) + userlog("CM_MSG_SET 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_MSG_SET 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_MSG_SET 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_NETTING_CALC.pgc b/app/src/cm/svc/CM_NETTING_CALC.pgc index 35abbbe..a2bfa00 100644 --- a/app/src/cm/svc/CM_NETTING_CALC.pgc +++ b/app/src/cm/svc/CM_NETTING_CALC.pgc @@ -6,12 +6,138 @@ #include "CM_NETTING_CALC.h" /* 28. CM_NETTING_CALC - 네팅 계산 (순수 계산): 차변-대변. */ -void CM_NETTING_CALC(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_NETTING_CALC__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; long debit = getl(b, T_AMT1), credit = getl(b, T_AMT2), net; net = debit - credit; setl(b, T_NET, net); setl(b, T_RC, net > 0 ? 1 : (net < 0 ? -1 : 0)); /* 1=수취, -1=지급 */ + return 0; + return 0; +} + +/* CM_NETTING_CALC 심화용 보조 판정 - 처리등급 산정. */ +static int CM_NETTING_CALC__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 36L) return 1; /* 소량 */ + if (v < 1146L) return 2; /* 보통 */ + if (v < 101146L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_NETTING_CALC - 네팅 계산 (순수 계산): 차변-대변.. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_NETTING_CALC(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_NETTING_CALC 진입: 네팅 계산 (순수 계산): 차변-대변. (svc=CM_NETTING_CALC)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + if (cmdb_get_param("VAT_RATE", hz_pval) == 0) policy = atol(hz_pval); + else policy = 10; /* 기본 부가세율 */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log + WHERE svc_name = 'CM_NETTING_CALC' AND biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_NETTING_CALC 처리이력 조회 오류"); } + else { policy += (hz_cnt % 7); } /* 반복호출 보정 */ + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band <= 0) { + grade = 0; userlog("CM_NETTING_CALC 무금액 요청 - 조회전용 경로"); + } else if (band < 146L) { + grade = 1; band = band + acq_fee(band, 25); /* 소액 수수료 가산 */ + } else if (band < 11146L) { + grade = 2; band = band + acq_vat(band / 10); /* 중액 부가세 */ + } else { + grade = 3; band = band + acq_wht_amt(band, 2200); /* 대액 원천 */ + } + if (grade >= 3 && band > 111460L) { + userlog("CM_NETTING_CALC 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_NETTING_CALC__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_NETTING_CALC 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_NETTING_CALC__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_NETTING_CALC 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_NETTING_CALC", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_NETTING_CALC'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1146L) + userlog("CM_NETTING_CALC 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_NETTING_CALC 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_NETTING_CALC 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_NEXTBIZ.pgc b/app/src/cm/svc/CM_NEXTBIZ.pgc index 6865ded..5512c4d 100644 --- a/app/src/cm/svc/CM_NEXTBIZ.pgc +++ b/app/src/cm/svc/CM_NEXTBIZ.pgc @@ -6,7 +6,11 @@ #include "CM_NEXTBIZ.h" /* 4. CM_NEXTBIZ - 다음 영업일 (주말 skip=common util + 휴일 skip=cm_holiday). */ -void CM_NEXTBIZ(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_NEXTBIZ__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char in[16], cur[16], nxt[16]; @@ -15,12 +19,138 @@ void CM_NEXTBIZ(TPSVCINFO *p) if (in[0] == 0) { if (cmdb_get_param("BIZ_DATE", in) < 0) strcpy(in, "2026-07-19"); } strncpy(cur, in, sizeof(cur)-1); cur[sizeof(cur)-1] = 0; do { - if (acq_next_bizday(cur, nxt) < 0) FAIL(b); /* 주말 건너뜀 */ + if (acq_next_bizday(cur, nxt) < 0) return -1; /* 주말 건너뜀 */ strncpy(cur, nxt, sizeof(cur)-1); cur[sizeof(cur)-1] = 0; hol = cmdb_is_holiday(nxt); - if (hol < 0) FAIL(b); + if (hol < 0) return -1; if (++guard > 30) break; } while (hol == 1); /* 휴일이면 다시 +1영업일 */ sets_(b, T_STR3, nxt); + return 0; + return 0; +} + +/* CM_NEXTBIZ 심화용 보조 판정 - 처리등급 산정. */ +static int CM_NEXTBIZ__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 37L) return 1; /* 소량 */ + if (v < 1287L) return 2; /* 보통 */ + if (v < 100787L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_NEXTBIZ - 다음 영업일 (주말 skip=common util + 휴일 skip=cm_holiday).. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_NEXTBIZ(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_NEXTBIZ 진입: 다음 영업일 (주말 skip=common util + 휴일 skip=cm_holiday). (svc=CM_NEXTBIZ)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + rc = cmdb_is_holiday(hz_bizdate); + if (rc < 0) { userlog("CM_NEXTBIZ 휴일 조회 오류"); } + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_holiday + WHERE holi_date >= :hz_bizdate; + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 잔여 휴일수 */ + if (rc == 1) { userlog("CM_NEXTBIZ 기준일이 휴일 bizdate=%s", hz_bizdate); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (policy < 0) { + grade = 0; userlog("CM_NEXTBIZ 기준정보 이상 - 보류"); + } else if (policy == 0) { + grade = 1; /* 최초 처리 */ + } else if (policy < 187L) { + grade = 2; /* 통상 처리 */ + } else { + grade = 3; userlog("CM_NEXTBIZ 누적 과다 policy=%ld", policy); + } + if (grade >= 3 && band > 107870L) { + userlog("CM_NEXTBIZ 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_NEXTBIZ__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_NEXTBIZ 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_NEXTBIZ__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_NEXTBIZ 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_NEXTBIZ", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_holiday + WHERE holi_date BETWEEN :hz_bizdate AND '2026-12-31'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 연말까지 휴일수 */ + if (agg > 1287L) + userlog("CM_NEXTBIZ 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_NEXTBIZ 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_NEXTBIZ 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_NOTI_HIST.pgc b/app/src/cm/svc/CM_NOTI_HIST.pgc index 5545819..e872743 100644 --- a/app/src/cm/svc/CM_NOTI_HIST.pgc +++ b/app/src/cm/svc/CM_NOTI_HIST.pgc @@ -6,7 +6,11 @@ */ #include "CM_NOTI_HIST.h" -void CM_NOTI_HIST(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_NOTI_HIST__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char ch[12], lastmsg[256]; @@ -24,12 +28,139 @@ void CM_NOTI_HIST(TPSVCINFO *p) EXEC SQL SELECT count(*), coalesce(max(log_id), 0) INTO :h_cnt, :h_lastid FROM cm_log WHERE svc_name = 'NOTI' AND biz_date = :h_date AND msg LIKE :h_pat; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; lastmsg[0] = 0; if (h_lastid > 0) cmdb_log_message(h_lastid, lastmsg); setl(b, T_COUNT, h_cnt); setl(b, T_ID1, h_lastid); sets_(b, T_MSG, lastmsg); + return 0; + return 0; +} + +/* CM_NOTI_HIST 심화용 보조 판정 - 처리등급 산정. */ +static int CM_NOTI_HIST__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 10L) return 1; /* 소량 */ + if (v < 1460L) return 2; /* 보통 */ + if (v < 100960L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_NOTI_HIST - 공통 처리. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_NOTI_HIST(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_NOTI_HIST 진입: 공통 처리 (svc=CM_NOTI_HIST)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_NOTI_HIST 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 5L) + userlog("CM_NOTI_HIST 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 3L) { + grade = 1; /* 저빈도 */ + } else if (policy < 50L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_NOTI_HIST 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 109600L) { + userlog("CM_NOTI_HIST 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_NOTI_HIST__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_NOTI_HIST 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_NOTI_HIST__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_NOTI_HIST 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_NOTI_HIST", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_NOTI_HIST'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1460L) + userlog("CM_NOTI_HIST 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_NOTI_HIST 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_NOTI_HIST 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_NOTI_QUOTA.pgc b/app/src/cm/svc/CM_NOTI_QUOTA.pgc index 3e5f2b9..8da0128 100644 --- a/app/src/cm/svc/CM_NOTI_QUOTA.pgc +++ b/app/src/cm/svc/CM_NOTI_QUOTA.pgc @@ -6,7 +6,11 @@ */ #include "CM_NOTI_QUOTA.h" -void CM_NOTI_QUOTA(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_NOTI_QUOTA__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char ch[12], qkey[40], qval[128]; @@ -29,7 +33,7 @@ void CM_NOTI_QUOTA(TPSVCINFO *p) snprintf(h_pat, sizeof(h_pat), "[%s>%%", ch); EXEC SQL SELECT count(*) INTO :h_used FROM cm_log WHERE svc_name = 'NOTI' AND biz_date = :h_date AND msg LIKE :h_pat; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; remain = quota - h_used; exceeded = (remain <= 0) ? 1 : 0; @@ -37,5 +41,128 @@ void CM_NOTI_QUOTA(TPSVCINFO *p) setl(b, T_COUNT, h_used); /* 당일 발송 수 */ setl(b, T_ARG3, remain < 0 ? 0 : remain); /* 잔여 발송 가능량 */ setl(b, T_RC, exceeded); + return 0; + return 0; +} + +/* CM_NOTI_QUOTA 심화용 보조 판정 - 처리등급 산정. */ +static int CM_NOTI_QUOTA__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 12L) return 1; /* 소량 */ + if (v < 1042L) return 2; /* 보통 */ + if (v < 101042L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_NOTI_QUOTA - * CM_NOTI_QUOTA.pgc - cm 모듈 서비스 CM_NOTI_QUOTA (one service =. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_NOTI_QUOTA(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_NOTI_QUOTA 진입: * CM_NOTI_QUOTA.pgc - cm 모듈 서비스 CM_NOTI_QUOTA (one service = (svc=CM_NOTI_QUOTA)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_NOTI_QUOTA 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 7L) + userlog("CM_NOTI_QUOTA 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 13L) { + grade = 1; /* 저빈도 */ + } else if (policy < 52L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_NOTI_QUOTA 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 110420L) { + userlog("CM_NOTI_QUOTA 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_NOTI_QUOTA__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_NOTI_QUOTA 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_NOTI_QUOTA__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_NOTI_QUOTA 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_NOTI_QUOTA", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_NOTI_QUOTA'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1042L) + userlog("CM_NOTI_QUOTA 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_NOTI_QUOTA 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_NOTI_QUOTA 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_NOTI_RETRY.pgc b/app/src/cm/svc/CM_NOTI_RETRY.pgc index c27cfef..23e195e 100644 --- a/app/src/cm/svc/CM_NOTI_RETRY.pgc +++ b/app/src/cm/svc/CM_NOTI_RETRY.pgc @@ -6,27 +6,154 @@ */ #include "CM_NOTI_RETRY.h" -void CM_NOTI_RETRY(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_NOTI_RETRY__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char orig[256], packed[288], bizdate[16]; long src_id, new_id; src_id = getl(b, T_ID1); - if (src_id <= 0) FAIL(b); + if (src_id <= 0) return -1; orig[0] = 0; - if (cmdb_log_message(src_id, orig) < 0 || orig[0] == 0) { setl(b, T_RC, 1); FAIL(b); } - if (orig[0] != '[') { setl(b, T_RC, 2); FAIL(b); } /* 발송건 형식 아님 */ + if (cmdb_log_message(src_id, orig) < 0 || orig[0] == 0) { setl(b, T_RC, 1); return -1; } + if (orig[0] != '[') { setl(b, T_RC, 2); return -1; } /* 발송건 형식 아님 */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); if (bizdate[0] == 0) { if (cmdb_get_param("BIZ_DATE", bizdate) < 0) strcpy(bizdate, "2026-07-19"); } snprintf(packed, sizeof(packed), "[RETRY] %s", orig); new_id = cmdb_log("NOTI", "W", packed, bizdate); - if (new_id < 0) FAIL(b); + if (new_id < 0) return -1; setl(b, T_ID2, new_id); /* 재발송 ID */ sets_(b, T_MSG, orig); setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_NOTI_RETRY 심화용 보조 판정 - 처리등급 산정. */ +static int CM_NOTI_RETRY__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 24L) return 1; /* 소량 */ + if (v < 1054L) return 2; /* 보통 */ + if (v < 101054L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_NOTI_RETRY - * CM_NOTI_RETRY.pgc - cm 모듈 서비스 CM_NOTI_RETRY (one service =. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_NOTI_RETRY(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_NOTI_RETRY 진입: * CM_NOTI_RETRY.pgc - cm 모듈 서비스 CM_NOTI_RETRY (one service = (svc=CM_NOTI_RETRY)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_NOTI_RETRY 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 19L) + userlog("CM_NOTI_RETRY 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 13L) { + grade = 1; /* 저빈도 */ + } else if (policy < 64L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_NOTI_RETRY 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 110540L) { + userlog("CM_NOTI_RETRY 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_NOTI_RETRY__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_NOTI_RETRY 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_NOTI_RETRY__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_NOTI_RETRY 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_NOTI_RETRY", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_NOTI_RETRY'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1054L) + userlog("CM_NOTI_RETRY 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_NOTI_RETRY 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_NOTI_RETRY 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_NOTI_SEND.pgc b/app/src/cm/svc/CM_NOTI_SEND.pgc index 31a44fd..93ff52a 100644 --- a/app/src/cm/svc/CM_NOTI_SEND.pgc +++ b/app/src/cm/svc/CM_NOTI_SEND.pgc @@ -6,7 +6,11 @@ */ #include "CM_NOTI_SEND.h" -void CM_NOTI_SEND(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_NOTI_SEND__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char ch[12], target[64], body[256], packed[320], bizdate[16]; @@ -17,17 +21,140 @@ void CM_NOTI_SEND(TPSVCINFO *p) gets_(b, T_STR1, target, sizeof(target)); gets_(b, T_MSG, body, sizeof(body)); if (ch[0] == 0) strcpy(ch, "SMS"); - if (target[0] == 0 || body[0] == 0) FAIL(b); + if (target[0] == 0 || body[0] == 0) return -1; if (strcmp(ch, "SMS") == 0 || strcmp(ch, "EMAIL") == 0 || strcmp(ch, "PUSH") == 0) ok = 1; - if (!ok) { setl(b, T_RC, 1); FAIL(b); } /* 미지원 채널 */ + if (!ok) { setl(b, T_RC, 1); return -1; } /* 미지원 채널 */ if (strlen(body) > 200) body[200] = 0; /* 본문 절단 */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); if (bizdate[0] == 0) { if (cmdb_get_param("BIZ_DATE", bizdate) < 0) strcpy(bizdate, "2026-07-19"); } snprintf(packed, sizeof(packed), "[%s>%s] %s", ch, target, body); id = cmdb_log("NOTI", "I", packed, bizdate); - if (id < 0) FAIL(b); + if (id < 0) return -1; setl(b, T_ID1, id); /* 발송 ID */ setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_NOTI_SEND 심화용 보조 판정 - 처리등급 산정. */ +static int CM_NOTI_SEND__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 36L) return 1; /* 소량 */ + if (v < 1446L) return 2; /* 보통 */ + if (v < 100946L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_NOTI_SEND - * CM_NOTI_SEND.pgc - cm 모듈 서비스 CM_NOTI_SEND (one service = o. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_NOTI_SEND(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_NOTI_SEND 진입: * CM_NOTI_SEND.pgc - cm 모듈 서비스 CM_NOTI_SEND (one service = o (svc=CM_NOTI_SEND)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_NOTI_SEND 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 11L) + userlog("CM_NOTI_SEND 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 13L) { + grade = 1; /* 저빈도 */ + } else if (policy < 116L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_NOTI_SEND 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 109460L) { + userlog("CM_NOTI_SEND 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_NOTI_SEND__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_NOTI_SEND 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_NOTI_SEND__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_NOTI_SEND 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_NOTI_SEND", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_NOTI_SEND'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1446L) + userlog("CM_NOTI_SEND 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_NOTI_SEND 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_NOTI_SEND 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_NOTI_STAT.pgc b/app/src/cm/svc/CM_NOTI_STAT.pgc index 0349128..757ee91 100644 --- a/app/src/cm/svc/CM_NOTI_STAT.pgc +++ b/app/src/cm/svc/CM_NOTI_STAT.pgc @@ -6,7 +6,11 @@ */ #include "CM_NOTI_STAT.h" -void CM_NOTI_STAT(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_NOTI_STAT__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; @@ -24,11 +28,138 @@ void CM_NOTI_STAT(TPSVCINFO *p) count(*) INTO :h_sms, :h_email, :h_push, :h_total FROM cm_log WHERE svc_name = 'NOTI' AND biz_date = :h_date; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; setl(b, T_ARG1, h_sms); setl(b, T_ARG2, h_email); setl(b, T_ARG3, h_push); setl(b, T_COUNT, h_total); + return 0; + return 0; +} + +/* CM_NOTI_STAT 심화용 보조 판정 - 처리등급 산정. */ +static int CM_NOTI_STAT__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 14L) return 1; /* 소량 */ + if (v < 1464L) return 2; /* 보통 */ + if (v < 100964L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_NOTI_STAT - * CM_NOTI_STAT.pgc - cm 모듈 서비스 CM_NOTI_STAT (one service = o. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_NOTI_STAT(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_NOTI_STAT 진입: * CM_NOTI_STAT.pgc - cm 모듈 서비스 CM_NOTI_STAT (one service = o (svc=CM_NOTI_STAT)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_NOTI_STAT 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 9L) + userlog("CM_NOTI_STAT 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 7L) { + grade = 1; /* 저빈도 */ + } else if (policy < 54L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_NOTI_STAT 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 109640L) { + userlog("CM_NOTI_STAT 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_NOTI_STAT__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_NOTI_STAT 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_NOTI_STAT__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_NOTI_STAT 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_NOTI_STAT", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_NOTI_STAT'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1464L) + userlog("CM_NOTI_STAT 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_NOTI_STAT 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_NOTI_STAT 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_NOTI_TMPL.pgc b/app/src/cm/svc/CM_NOTI_TMPL.pgc index e5131b9..1f849a5 100644 --- a/app/src/cm/svc/CM_NOTI_TMPL.pgc +++ b/app/src/cm/svc/CM_NOTI_TMPL.pgc @@ -6,7 +6,11 @@ */ #include "CM_NOTI_TMPL.h" -void CM_NOTI_TMPL(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_NOTI_TMPL__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char lang[8], arg0[128], ch[12], target[64], body[256], packed[320], bizdate[16]; @@ -21,7 +25,7 @@ void CM_NOTI_TMPL(TPSVCINFO *p) gets_(b, T_STR3, arg0, sizeof(arg0)); gets_(b, T_CHANNEL, ch, sizeof(ch)); gets_(b, T_STR1, target, sizeof(target)); - if (h_id[0] == 0 || target[0] == 0) FAIL(b); + if (h_id[0] == 0 || target[0] == 0) return -1; if (ch[0] == 0) strcpy(ch, "SMS"); if (lang[0] == 0) strcpy(lang, "KO"); if (lang[0] >= 'a') lang[0] -= 32; @@ -30,7 +34,7 @@ void CM_NOTI_TMPL(TPSVCINFO *p) EXEC SQL SELECT code_name INTO :h_tmpl FROM cm_code WHERE code_grp = :h_grp AND code = :h_id AND use_yn = 'Y'; - if (sqlca.sqlcode != 0) { setl(b, T_RC, 1); FAIL(b); } /* 템플릿 없음 */ + if (sqlca.sqlcode != 0) { setl(b, T_RC, 1); return -1; } /* 템플릿 없음 */ while (h_tmpl[i] && o < (int)sizeof(body) - 130) { if (h_tmpl[i] == '{' && h_tmpl[i + 1] == '0' && h_tmpl[i + 2] == '}') { @@ -43,10 +47,133 @@ void CM_NOTI_TMPL(TPSVCINFO *p) if (bizdate[0] == 0) { if (cmdb_get_param("BIZ_DATE", bizdate) < 0) strcpy(bizdate, "2026-07-19"); } snprintf(packed, sizeof(packed), "[%s>%s] %s", ch, target, body); id = cmdb_log("NOTI", "I", packed, bizdate); - if (id < 0) FAIL(b); + if (id < 0) return -1; setl(b, T_ID1, id); /* 발송 ID */ sets_(b, T_MSG, body); setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_NOTI_TMPL 심화용 보조 판정 - 처리등급 산정. */ +static int CM_NOTI_TMPL__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 15L) return 1; /* 소량 */ + if (v < 1465L) return 2; /* 보통 */ + if (v < 100965L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_NOTI_TMPL - * CM_NOTI_TMPL.pgc - cm 모듈 서비스 CM_NOTI_TMPL (one service = o. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_NOTI_TMPL(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_NOTI_TMPL 진입: * CM_NOTI_TMPL.pgc - cm 모듈 서비스 CM_NOTI_TMPL (one service = o (svc=CM_NOTI_TMPL)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_NOTI_TMPL 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 10L) + userlog("CM_NOTI_TMPL 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 8L) { + grade = 1; /* 저빈도 */ + } else if (policy < 55L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_NOTI_TMPL 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 109650L) { + userlog("CM_NOTI_TMPL 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_NOTI_TMPL__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_NOTI_TMPL 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_NOTI_TMPL__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_NOTI_TMPL 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_NOTI_TMPL", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_NOTI_TMPL'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1465L) + userlog("CM_NOTI_TMPL 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_NOTI_TMPL 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_NOTI_TMPL 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_PARAM.pgc b/app/src/cm/svc/CM_PARAM.pgc index 426b9db..ca821ee 100644 --- a/app/src/cm/svc/CM_PARAM.pgc +++ b/app/src/cm/svc/CM_PARAM.pgc @@ -6,13 +6,142 @@ #include "CM_PARAM.h" /* 18. CM_PARAM - 공통 파라미터 조회. */ -void CM_PARAM(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_PARAM__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char key[64], val[128]; gets_(b, T_STR1, key, sizeof(key)); - if (key[0] == 0) FAIL(b); - if (cmdb_get_param(key, val) < 0) { userlog("CM_PARAM 미존재 key=%s", key); FAIL(b); } + if (key[0] == 0) return -1; + if (cmdb_get_param(key, val) < 0) { userlog("CM_PARAM 미존재 key=%s", key); return -1; } sets_(b, T_STR3, val); + return 0; + return 0; +} + +/* CM_PARAM 심화용 보조 판정 - 처리등급 산정. */ +static int CM_PARAM__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 18L) return 1; /* 소량 */ + if (v < 1108L) return 2; /* 보통 */ + if (v < 100608L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_PARAM - 공통 파라미터 조회.. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_PARAM(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_PARAM 진입: 공통 파라미터 조회. (svc=CM_PARAM)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_KEY1, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "BIZ_DATE"); + EXEC SQL SELECT param_val INTO :hz_pval FROM cm_param WHERE param_key = :hz_key; + if (sqlca.sqlcode == 100) { userlog("CM_PARAM 파라미터 미존재 key=%s", hz_key); policy = 0; } + else if (sqlca.sqlcode < 0) { userlog("CM_PARAM 파라미터 조회 오류"); } + else { policy = atol(hz_pval); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + gets_(b, T_STATUS, ins, sizeof(ins)); + if (ins[0] == 'C' || ins[0] == 'X') { + grade = 0; userlog("CM_PARAM 폐기/취소 상태 - 갱신 금지"); + } else if (policy == 0) { + grade = 1; userlog("CM_PARAM 신규 등록 경로"); + } else if (policy < 13L) { + grade = 2; /* 소규모 그룹 - 단순 반영 */ + } else { + grade = 3; /* 대규모 그룹 - 검증 강화 */ + } + if (grade >= 3 && band > 106080L) { + userlog("CM_PARAM 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_PARAM__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_PARAM 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_PARAM__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_PARAM 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_PARAM", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(DISTINCT code_grp) INTO :hz_v1 FROM cm_code; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 코드그룹 수 */ + if (agg > 1108L) + userlog("CM_PARAM 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_PARAM 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_PARAM 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_PARAM_APPROVE.pgc b/app/src/cm/svc/CM_PARAM_APPROVE.pgc index e6f2df3..ade32cd 100644 --- a/app/src/cm/svc/CM_PARAM_APPROVE.pgc +++ b/app/src/cm/svc/CM_PARAM_APPROVE.pgc @@ -6,7 +6,11 @@ */ #include "CM_PARAM_APPROVE.h" -void CM_PARAM_APPROVE(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_PARAM_APPROVE__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char key[64], who[32], pend[128], hist[256], bizdate[16]; @@ -16,15 +20,15 @@ void CM_PARAM_APPROVE(TPSVCINFO *p) gets_(b, T_STR1, key, sizeof(key)); gets_(b, T_STR3, who, sizeof(who)); - if (key[0] == 0) FAIL(b); + if (key[0] == 0) return -1; if (who[0] == 0) strcpy(who, "ADMIN"); snprintf(h_reqkey, sizeof(h_reqkey), "REQ:%s", key); - if (cmdb_get_param(h_reqkey, pend) < 0) { setl(b, T_RC, 1); FAIL(b); } /* 대기 없음 */ - if (cmdb_set_param(key, pend) < 0) FAIL(b); + if (cmdb_get_param(h_reqkey, pend) < 0) { setl(b, T_RC, 1); return -1; } /* 대기 없음 */ + if (cmdb_set_param(key, pend) < 0) return -1; EXEC SQL DELETE FROM cm_param WHERE param_key = :h_reqkey; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); if (bizdate[0] == 0) { if (cmdb_get_param("BIZ_DATE", bizdate) < 0) strcpy(bizdate, "2026-07-19"); } @@ -32,5 +36,126 @@ void CM_PARAM_APPROVE(TPSVCINFO *p) cmdb_log("PARAMWF", "I", hist, bizdate); sets_(b, T_STR4, pend); /* 반영된 값 */ setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_PARAM_APPROVE 심화용 보조 판정 - 처리등급 산정. */ +static int CM_PARAM_APPROVE__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 14L) return 1; /* 소량 */ + if (v < 1244L) return 2; /* 보통 */ + if (v < 101244L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_PARAM_APPROVE - * CM_PARAM_APPROVE.pgc - cm 모듈 서비스 CM_PARAM_APPROVE (one ser. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_PARAM_APPROVE(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_PARAM_APPROVE 진입: * CM_PARAM_APPROVE.pgc - cm 모듈 서비스 CM_PARAM_APPROVE (one ser (svc=CM_PARAM_APPROVE)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_KEY1, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "BIZ_DATE"); + EXEC SQL SELECT param_val INTO :hz_pval FROM cm_param WHERE param_key = :hz_key; + if (sqlca.sqlcode == 100) { userlog("CM_PARAM_APPROVE 파라미터 미존재 key=%s", hz_key); policy = 0; } + else if (sqlca.sqlcode < 0) { userlog("CM_PARAM_APPROVE 파라미터 조회 오류"); } + else { policy = atol(hz_pval); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + gets_(b, T_STATUS, ins, sizeof(ins)); + if (ins[0] == 'C' || ins[0] == 'X') { + grade = 0; userlog("CM_PARAM_APPROVE 폐기/취소 상태 - 갱신 금지"); + } else if (policy == 0) { + grade = 1; userlog("CM_PARAM_APPROVE 신규 등록 경로"); + } else if (policy < 9L) { + grade = 2; /* 소규모 그룹 - 단순 반영 */ + } else { + grade = 3; /* 대규모 그룹 - 검증 강화 */ + } + if (grade >= 3 && band > 112440L) { + userlog("CM_PARAM_APPROVE 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_PARAM_APPROVE__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_PARAM_APPROVE 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_PARAM_APPROVE__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_PARAM_APPROVE 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_PARAM_APPROVE", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(DISTINCT code_grp) INTO :hz_v1 FROM cm_code; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 코드그룹 수 */ + if (agg > 1244L) + userlog("CM_PARAM_APPROVE 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_PARAM_APPROVE 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_PARAM_APPROVE 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_PARAM_CANCEL.pgc b/app/src/cm/svc/CM_PARAM_CANCEL.pgc index c001615..371bb18 100644 --- a/app/src/cm/svc/CM_PARAM_CANCEL.pgc +++ b/app/src/cm/svc/CM_PARAM_CANCEL.pgc @@ -6,7 +6,11 @@ */ #include "CM_PARAM_CANCEL.h" -void CM_PARAM_CANCEL(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_PARAM_CANCEL__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char key[64], who[32], pend[128], bizdate[16], hist[224]; @@ -16,14 +20,14 @@ void CM_PARAM_CANCEL(TPSVCINFO *p) gets_(b, T_STR1, key, sizeof(key)); gets_(b, T_STR3, who, sizeof(who)); - if (key[0] == 0) FAIL(b); + if (key[0] == 0) return -1; if (who[0] == 0) strcpy(who, "SYSTEM"); snprintf(h_reqkey, sizeof(h_reqkey), "REQ:%s", key); - if (cmdb_get_param(h_reqkey, pend) < 0) { setl(b, T_RC, 1); FAIL(b); } /* 대기 없음 */ + if (cmdb_get_param(h_reqkey, pend) < 0) { setl(b, T_RC, 1); return -1; } /* 대기 없음 */ EXEC SQL DELETE FROM cm_param WHERE param_key = :h_reqkey; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); if (bizdate[0] == 0) { if (cmdb_get_param("BIZ_DATE", bizdate) < 0) strcpy(bizdate, "2026-07-19"); } @@ -31,5 +35,126 @@ void CM_PARAM_CANCEL(TPSVCINFO *p) cmdb_log("PARAMWF", "I", hist, bizdate); sets_(b, T_STR4, pend); /* 철회된 요청 값 */ setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_PARAM_CANCEL 심화용 보조 판정 - 처리등급 산정. */ +static int CM_PARAM_CANCEL__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 15L) return 1; /* 소량 */ + if (v < 1125L) return 2; /* 보통 */ + if (v < 101125L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_PARAM_CANCEL - * CM_PARAM_CANCEL.pgc - cm 모듈 서비스 CM_PARAM_CANCEL (one servi. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_PARAM_CANCEL(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_PARAM_CANCEL 진입: * CM_PARAM_CANCEL.pgc - cm 모듈 서비스 CM_PARAM_CANCEL (one servi (svc=CM_PARAM_CANCEL)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_KEY1, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "BIZ_DATE"); + EXEC SQL SELECT param_val INTO :hz_pval FROM cm_param WHERE param_key = :hz_key; + if (sqlca.sqlcode == 100) { userlog("CM_PARAM_CANCEL 파라미터 미존재 key=%s", hz_key); policy = 0; } + else if (sqlca.sqlcode < 0) { userlog("CM_PARAM_CANCEL 파라미터 조회 오류"); } + else { policy = atol(hz_pval); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + gets_(b, T_STATUS, ins, sizeof(ins)); + if (ins[0] == 'C' || ins[0] == 'X') { + grade = 0; userlog("CM_PARAM_CANCEL 폐기/취소 상태 - 갱신 금지"); + } else if (policy == 0) { + grade = 1; userlog("CM_PARAM_CANCEL 신규 등록 경로"); + } else if (policy < 10L) { + grade = 2; /* 소규모 그룹 - 단순 반영 */ + } else { + grade = 3; /* 대규모 그룹 - 검증 강화 */ + } + if (grade >= 3 && band > 111250L) { + userlog("CM_PARAM_CANCEL 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_PARAM_CANCEL__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_PARAM_CANCEL 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_PARAM_CANCEL__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_PARAM_CANCEL 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_PARAM_CANCEL", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(DISTINCT code_grp) INTO :hz_v1 FROM cm_code; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 코드그룹 수 */ + if (agg > 1125L) + userlog("CM_PARAM_CANCEL 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_PARAM_CANCEL 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_PARAM_CANCEL 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_PARAM_DIFF.pgc b/app/src/cm/svc/CM_PARAM_DIFF.pgc index 5d0fe45..a1e4b6b 100644 --- a/app/src/cm/svc/CM_PARAM_DIFF.pgc +++ b/app/src/cm/svc/CM_PARAM_DIFF.pgc @@ -6,17 +6,21 @@ */ #include "CM_PARAM_DIFF.h" -void CM_PARAM_DIFF(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_PARAM_DIFF__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char key[64], reqkey[72], cur[128], req[128]; long changed, numeric = 0, delta = 0; gets_(b, T_STR1, key, sizeof(key)); - if (key[0] == 0) FAIL(b); + if (key[0] == 0) return -1; snprintf(reqkey, sizeof(reqkey), "REQ:%s", key); - if (cmdb_get_param(reqkey, req) < 0) { setl(b, T_RC, 1); FAIL(b); } /* 대기 없음 */ + if (cmdb_get_param(reqkey, req) < 0) { setl(b, T_RC, 1); return -1; } /* 대기 없음 */ if (cmdb_get_param(key, cur) < 0) cur[0] = 0; /* 신규 키면 현재값 없음 */ changed = (strcmp(cur, req) != 0) ? 1 : 0; @@ -32,5 +36,126 @@ void CM_PARAM_DIFF(TPSVCINFO *p) setl(b, T_ARG1, changed); setl(b, T_ARG2, numeric); setl(b, T_DELTA, delta); /* 수치 증감(수치일 때만) */ + return 0; + return 0; +} + +/* CM_PARAM_DIFF 심화용 보조 판정 - 처리등급 산정. */ +static int CM_PARAM_DIFF__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 34L) return 1; /* 소량 */ + if (v < 1484L) return 2; /* 보통 */ + if (v < 100984L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_PARAM_DIFF - * CM_PARAM_DIFF.pgc - cm 모듈 서비스 CM_PARAM_DIFF (one service =. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_PARAM_DIFF(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_PARAM_DIFF 진입: * CM_PARAM_DIFF.pgc - cm 모듈 서비스 CM_PARAM_DIFF (one service = (svc=CM_PARAM_DIFF)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_KEY1, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "BIZ_DATE"); + EXEC SQL SELECT param_val INTO :hz_pval FROM cm_param WHERE param_key = :hz_key; + if (sqlca.sqlcode == 100) { userlog("CM_PARAM_DIFF 파라미터 미존재 key=%s", hz_key); policy = 0; } + else if (sqlca.sqlcode < 0) { userlog("CM_PARAM_DIFF 파라미터 조회 오류"); } + else { policy = atol(hz_pval); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + gets_(b, T_STATUS, ins, sizeof(ins)); + if (ins[0] == 'C' || ins[0] == 'X') { + grade = 0; userlog("CM_PARAM_DIFF 폐기/취소 상태 - 갱신 금지"); + } else if (policy == 0) { + grade = 1; userlog("CM_PARAM_DIFF 신규 등록 경로"); + } else if (policy < 9L) { + grade = 2; /* 소규모 그룹 - 단순 반영 */ + } else { + grade = 3; /* 대규모 그룹 - 검증 강화 */ + } + if (grade >= 3 && band > 109840L) { + userlog("CM_PARAM_DIFF 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_PARAM_DIFF__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_PARAM_DIFF 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_PARAM_DIFF__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_PARAM_DIFF 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_PARAM_DIFF", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(DISTINCT code_grp) INTO :hz_v1 FROM cm_code; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 코드그룹 수 */ + if (agg > 1484L) + userlog("CM_PARAM_DIFF 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_PARAM_DIFF 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_PARAM_DIFF 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_PARAM_HIST.pgc b/app/src/cm/svc/CM_PARAM_HIST.pgc index c3fcb08..e015a45 100644 --- a/app/src/cm/svc/CM_PARAM_HIST.pgc +++ b/app/src/cm/svc/CM_PARAM_HIST.pgc @@ -6,7 +6,11 @@ */ #include "CM_PARAM_HIST.h" -void CM_PARAM_HIST(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_PARAM_HIST__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char key[64], lastmsg[256]; @@ -21,12 +25,137 @@ void CM_PARAM_HIST(TPSVCINFO *p) EXEC SQL SELECT count(*), coalesce(max(log_id), 0) INTO :h_cnt, :h_last FROM cm_log WHERE svc_name = 'PARAMWF' AND msg LIKE :h_pat; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; lastmsg[0] = 0; if (h_last > 0) cmdb_log_message(h_last, lastmsg); setl(b, T_COUNT, h_cnt); setl(b, T_ID1, h_last); sets_(b, T_MSG, lastmsg); + return 0; + return 0; +} + +/* CM_PARAM_HIST 심화용 보조 판정 - 처리등급 산정. */ +static int CM_PARAM_HIST__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 25L) return 1; /* 소량 */ + if (v < 1015L) return 2; /* 보통 */ + if (v < 101015L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_PARAM_HIST - * CM_PARAM_HIST.pgc - cm 모듈 서비스 CM_PARAM_HIST (one service =. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_PARAM_HIST(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_PARAM_HIST 진입: * CM_PARAM_HIST.pgc - cm 모듈 서비스 CM_PARAM_HIST (one service = (svc=CM_PARAM_HIST)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_KEY1, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "BIZ_DATE"); + EXEC SQL SELECT param_val INTO :hz_pval FROM cm_param WHERE param_key = :hz_key; + if (sqlca.sqlcode == 100) { userlog("CM_PARAM_HIST 파라미터 미존재 key=%s", hz_key); policy = 0; } + else if (sqlca.sqlcode < 0) { userlog("CM_PARAM_HIST 파라미터 조회 오류"); } + else { policy = atol(hz_pval); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + gets_(b, T_STATUS, ins, sizeof(ins)); + if (ins[0] == 'C' || ins[0] == 'X') { + grade = 0; userlog("CM_PARAM_HIST 폐기/취소 상태 - 갱신 금지"); + } else if (policy == 0) { + grade = 1; userlog("CM_PARAM_HIST 신규 등록 경로"); + } else if (policy < 10L) { + grade = 2; /* 소규모 그룹 - 단순 반영 */ + } else { + grade = 3; /* 대규모 그룹 - 검증 강화 */ + } + if (grade >= 3 && band > 110150L) { + userlog("CM_PARAM_HIST 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_PARAM_HIST__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_PARAM_HIST 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_PARAM_HIST__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_PARAM_HIST 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_PARAM_HIST", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(DISTINCT code_grp) INTO :hz_v1 FROM cm_code; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 코드그룹 수 */ + if (agg > 1015L) + userlog("CM_PARAM_HIST 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_PARAM_HIST 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_PARAM_HIST 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_PARAM_LIST.pgc b/app/src/cm/svc/CM_PARAM_LIST.pgc index 79ba157..63efc0e 100644 --- a/app/src/cm/svc/CM_PARAM_LIST.pgc +++ b/app/src/cm/svc/CM_PARAM_LIST.pgc @@ -6,7 +6,11 @@ */ #include "CM_PARAM_LIST.h" -void CM_PARAM_LIST(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_PARAM_LIST__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char prefix[48]; @@ -20,12 +24,137 @@ void CM_PARAM_LIST(TPSVCINFO *p) else snprintf(h_pat, sizeof(h_pat), "%s%%", prefix); EXEC SQL SELECT count(*) INTO :h_total FROM cm_param WHERE param_key LIKE :h_pat; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; EXEC SQL SELECT count(*) INTO :h_pending FROM cm_param WHERE param_key LIKE 'REQ:%'; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; setl(b, T_COUNT, h_total); /* 접두 일치 파라미터 수 */ setl(b, T_ARG1, h_pending); /* 대기 요청 수 */ + return 0; + return 0; +} + +/* CM_PARAM_LIST 심화용 보조 판정 - 처리등급 산정. */ +static int CM_PARAM_LIST__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 29L) return 1; /* 소량 */ + if (v < 1019L) return 2; /* 보통 */ + if (v < 101019L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_PARAM_LIST - * CM_PARAM_LIST.pgc - cm 모듈 서비스 CM_PARAM_LIST (one service =. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_PARAM_LIST(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_PARAM_LIST 진입: * CM_PARAM_LIST.pgc - cm 모듈 서비스 CM_PARAM_LIST (one service = (svc=CM_PARAM_LIST)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_KEY1, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "BIZ_DATE"); + EXEC SQL SELECT param_val INTO :hz_pval FROM cm_param WHERE param_key = :hz_key; + if (sqlca.sqlcode == 100) { userlog("CM_PARAM_LIST 파라미터 미존재 key=%s", hz_key); policy = 0; } + else if (sqlca.sqlcode < 0) { userlog("CM_PARAM_LIST 파라미터 조회 오류"); } + else { policy = atol(hz_pval); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + gets_(b, T_STATUS, ins, sizeof(ins)); + if (ins[0] == 'C' || ins[0] == 'X') { + grade = 0; userlog("CM_PARAM_LIST 폐기/취소 상태 - 갱신 금지"); + } else if (policy == 0) { + grade = 1; userlog("CM_PARAM_LIST 신규 등록 경로"); + } else if (policy < 14L) { + grade = 2; /* 소규모 그룹 - 단순 반영 */ + } else { + grade = 3; /* 대규모 그룹 - 검증 강화 */ + } + if (grade >= 3 && band > 110190L) { + userlog("CM_PARAM_LIST 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_PARAM_LIST__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_PARAM_LIST 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_PARAM_LIST__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_PARAM_LIST 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_PARAM_LIST", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(DISTINCT code_grp) INTO :hz_v1 FROM cm_code; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 코드그룹 수 */ + if (agg > 1019L) + userlog("CM_PARAM_LIST 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_PARAM_LIST 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_PARAM_LIST 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_PARAM_NUM.pgc b/app/src/cm/svc/CM_PARAM_NUM.pgc index 73160aa..2562a61 100644 --- a/app/src/cm/svc/CM_PARAM_NUM.pgc +++ b/app/src/cm/svc/CM_PARAM_NUM.pgc @@ -6,7 +6,11 @@ */ #include "CM_PARAM_NUM.h" -void CM_PARAM_NUM(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_PARAM_NUM__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char key[64], val[128], *end; @@ -15,11 +19,11 @@ void CM_PARAM_NUM(TPSVCINFO *p) gets_(b, T_STR1, key, sizeof(key)); lo = getl(b, T_ARG1); hi = getl(b, T_ARG2); - if (key[0] == 0) FAIL(b); - if (cmdb_get_param(key, val) < 0) { setl(b, T_RC, 1); FAIL(b); } /* 미등록 */ + if (key[0] == 0) return -1; + if (cmdb_get_param(key, val) < 0) { setl(b, T_RC, 1); return -1; } /* 미등록 */ raw = strtol(val, &end, 10); - if (*end != 0) { setl(b, T_RC, 2); FAIL(b); } /* 숫자 아님 */ + if (*end != 0) { setl(b, T_RC, 2); return -1; } /* 숫자 아님 */ clamped = raw; if (hi > lo) { /* 경계가 유효할 때만 검증 */ @@ -29,5 +33,126 @@ void CM_PARAM_NUM(TPSVCINFO *p) setl(b, T_ARG3, raw); /* 원값 */ setl(b, T_ARG4, clamped); /* 클램프 값 */ setl(b, T_RC, violated); + return 0; + return 0; +} + +/* CM_PARAM_NUM 심화용 보조 판정 - 처리등급 산정. */ +static int CM_PARAM_NUM__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 33L) return 1; /* 소량 */ + if (v < 1443L) return 2; /* 보통 */ + if (v < 100943L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_PARAM_NUM - * CM_PARAM_NUM.pgc - cm 모듈 서비스 CM_PARAM_NUM (one service = o. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_PARAM_NUM(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_PARAM_NUM 진입: * CM_PARAM_NUM.pgc - cm 모듈 서비스 CM_PARAM_NUM (one service = o (svc=CM_PARAM_NUM)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_KEY1, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "BIZ_DATE"); + EXEC SQL SELECT param_val INTO :hz_pval FROM cm_param WHERE param_key = :hz_key; + if (sqlca.sqlcode == 100) { userlog("CM_PARAM_NUM 파라미터 미존재 key=%s", hz_key); policy = 0; } + else if (sqlca.sqlcode < 0) { userlog("CM_PARAM_NUM 파라미터 조회 오류"); } + else { policy = atol(hz_pval); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + gets_(b, T_STATUS, ins, sizeof(ins)); + if (ins[0] == 'C' || ins[0] == 'X') { + grade = 0; userlog("CM_PARAM_NUM 폐기/취소 상태 - 갱신 금지"); + } else if (policy == 0) { + grade = 1; userlog("CM_PARAM_NUM 신규 등록 경로"); + } else if (policy < 8L) { + grade = 2; /* 소규모 그룹 - 단순 반영 */ + } else { + grade = 3; /* 대규모 그룹 - 검증 강화 */ + } + if (grade >= 3 && band > 109430L) { + userlog("CM_PARAM_NUM 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_PARAM_NUM__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_PARAM_NUM 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_PARAM_NUM__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_PARAM_NUM 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_PARAM_NUM", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(DISTINCT code_grp) INTO :hz_v1 FROM cm_code; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 코드그룹 수 */ + if (agg > 1443L) + userlog("CM_PARAM_NUM 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_PARAM_NUM 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_PARAM_NUM 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_PARAM_PENDING.pgc b/app/src/cm/svc/CM_PARAM_PENDING.pgc index 50afb98..cb9848e 100644 --- a/app/src/cm/svc/CM_PARAM_PENDING.pgc +++ b/app/src/cm/svc/CM_PARAM_PENDING.pgc @@ -6,7 +6,11 @@ */ #include "CM_PARAM_PENDING.h" -void CM_PARAM_PENDING(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_PARAM_PENDING__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; @@ -19,11 +23,132 @@ void CM_PARAM_PENDING(TPSVCINFO *p) coalesce(max((now()::date - updated_at::date))::int, 0) INTO :h_cnt, :h_oldest, :h_maxage FROM cm_param WHERE param_key LIKE 'REQ:%'; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; setl(b, T_COUNT, h_cnt); sets_(b, T_STR4, h_oldest); setl(b, T_ARG1, h_maxage); setl(b, T_RC, h_cnt > 0 ? 1 : 0); /* 1=처리할 대기 있음 */ + return 0; + return 0; +} + +/* CM_PARAM_PENDING 심화용 보조 판정 - 처리등급 산정. */ +static int CM_PARAM_PENDING__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 30L) return 1; /* 소량 */ + if (v < 1220L) return 2; /* 보통 */ + if (v < 101220L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_PARAM_PENDING - 공통 처리. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_PARAM_PENDING(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_PARAM_PENDING 진입: 공통 처리 (svc=CM_PARAM_PENDING)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_KEY1, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "BIZ_DATE"); + EXEC SQL SELECT param_val INTO :hz_pval FROM cm_param WHERE param_key = :hz_key; + if (sqlca.sqlcode == 100) { userlog("CM_PARAM_PENDING 파라미터 미존재 key=%s", hz_key); policy = 0; } + else if (sqlca.sqlcode < 0) { userlog("CM_PARAM_PENDING 파라미터 조회 오류"); } + else { policy = atol(hz_pval); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + gets_(b, T_STATUS, ins, sizeof(ins)); + if (ins[0] == 'C' || ins[0] == 'X') { + grade = 0; userlog("CM_PARAM_PENDING 폐기/취소 상태 - 갱신 금지"); + } else if (policy == 0) { + grade = 1; userlog("CM_PARAM_PENDING 신규 등록 경로"); + } else if (policy < 5L) { + grade = 2; /* 소규모 그룹 - 단순 반영 */ + } else { + grade = 3; /* 대규모 그룹 - 검증 강화 */ + } + if (grade >= 3 && band > 112200L) { + userlog("CM_PARAM_PENDING 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_PARAM_PENDING__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_PARAM_PENDING 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_PARAM_PENDING__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_PARAM_PENDING 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_PARAM_PENDING", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(DISTINCT code_grp) INTO :hz_v1 FROM cm_code; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 코드그룹 수 */ + if (agg > 1220L) + userlog("CM_PARAM_PENDING 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_PARAM_PENDING 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_PARAM_PENDING 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_PARAM_REJECT.pgc b/app/src/cm/svc/CM_PARAM_REJECT.pgc index cc1eda6..d1f133b 100644 --- a/app/src/cm/svc/CM_PARAM_REJECT.pgc +++ b/app/src/cm/svc/CM_PARAM_REJECT.pgc @@ -6,7 +6,11 @@ */ #include "CM_PARAM_REJECT.h" -void CM_PARAM_REJECT(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_PARAM_REJECT__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char key[64], reason[96], hist[224], bizdate[16]; @@ -16,18 +20,139 @@ void CM_PARAM_REJECT(TPSVCINFO *p) gets_(b, T_STR1, key, sizeof(key)); gets_(b, T_REASON, reason, sizeof(reason)); - if (key[0] == 0) FAIL(b); + if (key[0] == 0) return -1; if (reason[0] == 0) strcpy(reason, "NO REASON"); snprintf(h_reqkey, sizeof(h_reqkey), "REQ:%s", key); EXEC SQL DELETE FROM cm_param WHERE param_key = :h_reqkey; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlerrd[2] != 1) { setl(b, T_RC, 1); FAIL(b); } /* 대기 없음 */ + if (sqlca.sqlcode < 0) return -1; + if (sqlca.sqlerrd[2] != 1) { setl(b, T_RC, 1); return -1; } /* 대기 없음 */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); if (bizdate[0] == 0) { if (cmdb_get_param("BIZ_DATE", bizdate) < 0) strcpy(bizdate, "2026-07-19"); } snprintf(hist, sizeof(hist), "REJECT %s (%s)", key, reason); cmdb_log("PARAMWF", "W", hist, bizdate); setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_PARAM_REJECT 심화용 보조 판정 - 처리등급 산정. */ +static int CM_PARAM_REJECT__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 38L) return 1; /* 소량 */ + if (v < 1148L) return 2; /* 보통 */ + if (v < 101148L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_PARAM_REJECT - * CM_PARAM_REJECT.pgc - cm 모듈 서비스 CM_PARAM_REJECT (one servi. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_PARAM_REJECT(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_PARAM_REJECT 진입: * CM_PARAM_REJECT.pgc - cm 모듈 서비스 CM_PARAM_REJECT (one servi (svc=CM_PARAM_REJECT)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_KEY1, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "BIZ_DATE"); + EXEC SQL SELECT param_val INTO :hz_pval FROM cm_param WHERE param_key = :hz_key; + if (sqlca.sqlcode == 100) { userlog("CM_PARAM_REJECT 파라미터 미존재 key=%s", hz_key); policy = 0; } + else if (sqlca.sqlcode < 0) { userlog("CM_PARAM_REJECT 파라미터 조회 오류"); } + else { policy = atol(hz_pval); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + gets_(b, T_STATUS, ins, sizeof(ins)); + if (ins[0] == 'C' || ins[0] == 'X') { + grade = 0; userlog("CM_PARAM_REJECT 폐기/취소 상태 - 갱신 금지"); + } else if (policy == 0) { + grade = 1; userlog("CM_PARAM_REJECT 신규 등록 경로"); + } else if (policy < 13L) { + grade = 2; /* 소규모 그룹 - 단순 반영 */ + } else { + grade = 3; /* 대규모 그룹 - 검증 강화 */ + } + if (grade >= 3 && band > 111480L) { + userlog("CM_PARAM_REJECT 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_PARAM_REJECT__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_PARAM_REJECT 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_PARAM_REJECT__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_PARAM_REJECT 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_PARAM_REJECT", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(DISTINCT code_grp) INTO :hz_v1 FROM cm_code; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 코드그룹 수 */ + if (agg > 1148L) + userlog("CM_PARAM_REJECT 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_PARAM_REJECT 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_PARAM_REJECT 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_PARAM_REQ.pgc b/app/src/cm/svc/CM_PARAM_REQ.pgc index 9897c82..3c71873 100644 --- a/app/src/cm/svc/CM_PARAM_REQ.pgc +++ b/app/src/cm/svc/CM_PARAM_REQ.pgc @@ -6,20 +6,24 @@ */ #include "CM_PARAM_REQ.h" -void CM_PARAM_REQ(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_PARAM_REQ__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char key[64], val[128], who[32], reqkey[72], hist[256], bizdate[16], cur[128]; gets_(b, T_STR1, key, sizeof(key)); gets_(b, T_STR2, val, sizeof(val)); gets_(b, T_STR3, who, sizeof(who)); - if (key[0] == 0 || val[0] == 0) FAIL(b); + if (key[0] == 0 || val[0] == 0) return -1; if (who[0] == 0) strcpy(who, "SYSTEM"); - if (strncmp(key, "REQ:", 4) == 0) { setl(b, T_RC, 3); FAIL(b); } /* 중첩 요청 금지 */ + if (strncmp(key, "REQ:", 4) == 0) { setl(b, T_RC, 3); return -1; } /* 중첩 요청 금지 */ snprintf(reqkey, sizeof(reqkey), "REQ:%s", key); - if (cmdb_get_param(reqkey, cur) == 0) { setl(b, T_RC, 1); FAIL(b); } /* 미결 요청 존재 */ - if (cmdb_set_param(reqkey, val) < 0) FAIL(b); + if (cmdb_get_param(reqkey, cur) == 0) { setl(b, T_RC, 1); return -1; } /* 미결 요청 존재 */ + if (cmdb_set_param(reqkey, val) < 0) return -1; gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); if (bizdate[0] == 0) { if (cmdb_get_param("BIZ_DATE", bizdate) < 0) strcpy(bizdate, "2026-07-19"); } @@ -27,5 +31,126 @@ void CM_PARAM_REQ(TPSVCINFO *p) cmdb_log("PARAMWF", "I", hist, bizdate); sets_(b, T_STR4, reqkey); setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_PARAM_REQ 심화용 보조 판정 - 처리등급 산정. */ +static int CM_PARAM_REQ__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 25L) return 1; /* 소량 */ + if (v < 1435L) return 2; /* 보통 */ + if (v < 100935L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_PARAM_REQ - * CM_PARAM_REQ.pgc - cm 모듈 서비스 CM_PARAM_REQ (one service = o. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_PARAM_REQ(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_PARAM_REQ 진입: * CM_PARAM_REQ.pgc - cm 모듈 서비스 CM_PARAM_REQ (one service = o (svc=CM_PARAM_REQ)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_KEY1, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "BIZ_DATE"); + EXEC SQL SELECT param_val INTO :hz_pval FROM cm_param WHERE param_key = :hz_key; + if (sqlca.sqlcode == 100) { userlog("CM_PARAM_REQ 파라미터 미존재 key=%s", hz_key); policy = 0; } + else if (sqlca.sqlcode < 0) { userlog("CM_PARAM_REQ 파라미터 조회 오류"); } + else { policy = atol(hz_pval); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + gets_(b, T_STATUS, ins, sizeof(ins)); + if (ins[0] == 'C' || ins[0] == 'X') { + grade = 0; userlog("CM_PARAM_REQ 폐기/취소 상태 - 갱신 금지"); + } else if (policy == 0) { + grade = 1; userlog("CM_PARAM_REQ 신규 등록 경로"); + } else if (policy < 10L) { + grade = 2; /* 소규모 그룹 - 단순 반영 */ + } else { + grade = 3; /* 대규모 그룹 - 검증 강화 */ + } + if (grade >= 3 && band > 109350L) { + userlog("CM_PARAM_REQ 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_PARAM_REQ__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_PARAM_REQ 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_PARAM_REQ__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_PARAM_REQ 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_PARAM_REQ", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(DISTINCT code_grp) INTO :hz_v1 FROM cm_code; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 코드그룹 수 */ + if (agg > 1435L) + userlog("CM_PARAM_REQ 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_PARAM_REQ 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_PARAM_REQ 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_PARAM_SET.pgc b/app/src/cm/svc/CM_PARAM_SET.pgc index 51a2e33..ea5d0b2 100644 --- a/app/src/cm/svc/CM_PARAM_SET.pgc +++ b/app/src/cm/svc/CM_PARAM_SET.pgc @@ -6,14 +6,143 @@ #include "CM_PARAM_SET.h" /* 19. CM_PARAM_SET - 공통 파라미터 설정 (owner-writes, upsert). */ -void CM_PARAM_SET(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_PARAM_SET__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char key[64], val[128]; gets_(b, T_STR1, key, sizeof(key)); gets_(b, T_STR3, val, sizeof(val)); - if (key[0] == 0) FAIL(b); - if (cmdb_set_param(key, val) < 0) FAIL(b); + if (key[0] == 0) return -1; + if (cmdb_set_param(key, val) < 0) return -1; userlog("CM_PARAM_SET %s=%s", key, val); + return 0; + return 0; +} + +/* CM_PARAM_SET 심화용 보조 판정 - 처리등급 산정. */ +static int CM_PARAM_SET__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 29L) return 1; /* 소량 */ + if (v < 1439L) return 2; /* 보통 */ + if (v < 100939L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_PARAM_SET - 공통 파라미터 설정 (owner-writes, upsert).. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_PARAM_SET(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_PARAM_SET 진입: 공통 파라미터 설정 (owner-writes, upsert). (svc=CM_PARAM_SET)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_KEY1, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "BIZ_DATE"); + EXEC SQL SELECT param_val INTO :hz_pval FROM cm_param WHERE param_key = :hz_key; + if (sqlca.sqlcode == 100) { userlog("CM_PARAM_SET 파라미터 미존재 key=%s", hz_key); policy = 0; } + else if (sqlca.sqlcode < 0) { userlog("CM_PARAM_SET 파라미터 조회 오류"); } + else { policy = atol(hz_pval); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + gets_(b, T_STATUS, ins, sizeof(ins)); + if (ins[0] == 'C' || ins[0] == 'X') { + grade = 0; userlog("CM_PARAM_SET 폐기/취소 상태 - 갱신 금지"); + } else if (policy == 0) { + grade = 1; userlog("CM_PARAM_SET 신규 등록 경로"); + } else if (policy < 14L) { + grade = 2; /* 소규모 그룹 - 단순 반영 */ + } else { + grade = 3; /* 대규모 그룹 - 검증 강화 */ + } + if (grade >= 3 && band > 109390L) { + userlog("CM_PARAM_SET 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_PARAM_SET__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_PARAM_SET 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_PARAM_SET__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_PARAM_SET 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_PARAM_SET", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(DISTINCT code_grp) INTO :hz_v1 FROM cm_code; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 코드그룹 수 */ + if (agg > 1439L) + userlog("CM_PARAM_SET 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_PARAM_SET 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_PARAM_SET 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_PING.pgc b/app/src/cm/svc/CM_PING.pgc index 05ba779..f163d0c 100644 --- a/app/src/cm/svc/CM_PING.pgc +++ b/app/src/cm/svc/CM_PING.pgc @@ -6,10 +6,137 @@ #include "CM_PING.h" /* 30. CM_PING - 핑 (순수 처리). */ -void CM_PING(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_PING__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; sets_(b, T_MSG, "PONG"); setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_PING 심화용 보조 판정 - 처리등급 산정. */ +static int CM_PING__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 31L) return 1; /* 소량 */ + if (v < 1041L) return 2; /* 보통 */ + if (v < 100541L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_PING - 핑 (순수 처리).. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_PING(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_PING 진입: 핑 (순수 처리). (svc=CM_PING)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_PING 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 6L) + userlog("CM_PING 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 4L) { + grade = 1; /* 저빈도 */ + } else if (policy < 111L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_PING 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 105410L) { + userlog("CM_PING 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_PING__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_PING 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_PING__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_PING 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_PING", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_PING'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1041L) + userlog("CM_PING 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_PING 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_PING 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_PREVBIZ.pgc b/app/src/cm/svc/CM_PREVBIZ.pgc index 0b8c986..102c1a5 100644 --- a/app/src/cm/svc/CM_PREVBIZ.pgc +++ b/app/src/cm/svc/CM_PREVBIZ.pgc @@ -22,7 +22,11 @@ static void cm_back_day(int *y, int *m, int *d) if (*m == 2 && ((*y % 4 == 0 && *y % 100 != 0) || *y % 400 == 0)) *d = 29; } -void CM_PREVBIZ(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_PREVBIZ__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char in[16], cur[16]; @@ -31,19 +35,145 @@ void CM_PREVBIZ(TPSVCINFO *p) gets_(b, T_STR1, in, sizeof(in)); if (in[0] == 0) { if (cmdb_get_param("BIZ_DATE", in) < 0) strcpy(in, "2026-07-19"); } - if (sscanf(in, "%d-%d-%d", &y, &m, &d) != 3) FAIL(b); + if (sscanf(in, "%d-%d-%d", &y, &m, &d) != 3) return -1; for (;;) { cm_back_day(&y, &m, &d); - if (++skipped > 30) FAIL(b); /* 30일 내 영업일 없음 = 이상 */ + if (++skipped > 30) return -1; /* 30일 내 영업일 없음 = 이상 */ sprintf(cur, "%04d-%02d-%02d", y, m, d); w = cm_back_dow(y, m, d); if (w == 0 || w == 6) continue; /* 주말 */ hol = cmdb_is_holiday(cur); - if (hol < 0) FAIL(b); + if (hol < 0) return -1; if (hol == 0) break; /* 영업일 확정 */ } sets_(b, T_STR3, cur); setl(b, T_ARG1, skipped); /* 거슬러간 일수 */ + return 0; + return 0; +} + +/* CM_PREVBIZ 심화용 보조 판정 - 처리등급 산정. */ +static int CM_PREVBIZ__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 35L) return 1; /* 소량 */ + if (v < 1285L) return 2; /* 보통 */ + if (v < 100785L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_PREVBIZ - * CM_PREVBIZ.pgc - cm 모듈 서비스 CM_PREVBIZ (one service = one f. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_PREVBIZ(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_PREVBIZ 진입: * CM_PREVBIZ.pgc - cm 모듈 서비스 CM_PREVBIZ (one service = one f (svc=CM_PREVBIZ)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + rc = cmdb_is_holiday(hz_bizdate); + if (rc < 0) { userlog("CM_PREVBIZ 휴일 조회 오류"); } + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_holiday + WHERE holi_date >= :hz_bizdate; + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 잔여 휴일수 */ + if (rc == 1) { userlog("CM_PREVBIZ 기준일이 휴일 bizdate=%s", hz_bizdate); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (policy < 0) { + grade = 0; userlog("CM_PREVBIZ 기준정보 이상 - 보류"); + } else if (policy == 0) { + grade = 1; /* 최초 처리 */ + } else if (policy < 185L) { + grade = 2; /* 통상 처리 */ + } else { + grade = 3; userlog("CM_PREVBIZ 누적 과다 policy=%ld", policy); + } + if (grade >= 3 && band > 107850L) { + userlog("CM_PREVBIZ 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_PREVBIZ__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_PREVBIZ 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_PREVBIZ__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_PREVBIZ 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_PREVBIZ", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_holiday + WHERE holi_date BETWEEN :hz_bizdate AND '2026-12-31'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 연말까지 휴일수 */ + if (agg > 1285L) + userlog("CM_PREVBIZ 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_PREVBIZ 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_PREVBIZ 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_PRORATE.pgc b/app/src/cm/svc/CM_PRORATE.pgc index 62d75bd..956146d 100644 --- a/app/src/cm/svc/CM_PRORATE.pgc +++ b/app/src/cm/svc/CM_PRORATE.pgc @@ -6,14 +6,144 @@ #include "CM_PRORATE.h" /* 27. CM_PRORATE - 일할 계산 (순수 계산): 금액*사용일/총일. */ -void CM_PRORATE(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_PRORATE__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; long amt = getl(b, T_AMOUNT), used = getl(b, T_ARG1), total = getl(b, T_ARG2), pro; - if (total <= 0) FAIL(b); + if (total <= 0) return -1; if (used < 0) used = 0; if (used > total) used = total; pro = amt * used / total; setl(b, T_AMT1, pro); + return 0; + return 0; +} + +/* CM_PRORATE 심화용 보조 판정 - 처리등급 산정. */ +static int CM_PRORATE__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 30L) return 1; /* 소량 */ + if (v < 1280L) return 2; /* 보통 */ + if (v < 100780L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_PRORATE - 일할 계산 (순수 계산): 금액*사용일/총일.. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_PRORATE(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_PRORATE 진입: 일할 계산 (순수 계산): 금액*사용일/총일. (svc=CM_PRORATE)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + if (cmdb_get_param("VAT_RATE", hz_pval) == 0) policy = atol(hz_pval); + else policy = 10; /* 기본 부가세율 */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log + WHERE svc_name = 'CM_PRORATE' AND biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_PRORATE 처리이력 조회 오류"); } + else { policy += (hz_cnt % 7); } /* 반복호출 보정 */ + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band <= 0) { + grade = 0; userlog("CM_PRORATE 무금액 요청 - 조회전용 경로"); + } else if (band < 180L) { + grade = 1; band = band + acq_fee(band, 25); /* 소액 수수료 가산 */ + } else if (band < 10780L) { + grade = 2; band = band + acq_vat(band / 10); /* 중액 부가세 */ + } else { + grade = 3; band = band + acq_wht_amt(band, 2200); /* 대액 원천 */ + } + if (grade >= 3 && band > 107800L) { + userlog("CM_PRORATE 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_PRORATE__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_PRORATE 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_PRORATE__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_PRORATE 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_PRORATE", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_PRORATE'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1280L) + userlog("CM_PRORATE 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_PRORATE 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_PRORATE 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_QTR_END.pgc b/app/src/cm/svc/CM_QTR_END.pgc index 3b9c7a5..a206111 100644 --- a/app/src/cm/svc/CM_QTR_END.pgc +++ b/app/src/cm/svc/CM_QTR_END.pgc @@ -6,15 +6,19 @@ */ #include "CM_QTR_END.h" -void CM_QTR_END(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_QTR_END__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char in[16], out[16]; int y, m, d, q, endmon, endday; gets_(b, T_STR1, in, sizeof(in)); - if (sscanf(in, "%d-%d-%d", &y, &m, &d) != 3) FAIL(b); - if (m < 1 || m > 12) { setl(b, T_RC, 1); FAIL(b); } + if (sscanf(in, "%d-%d-%d", &y, &m, &d) != 3) return -1; + if (m < 1 || m > 12) { setl(b, T_RC, 1); return -1; } q = (m - 1) / 3 + 1; /* 분기 1~4 */ endmon = q * 3; /* 분기말월 3/6/9/12 */ @@ -28,5 +32,127 @@ void CM_QTR_END(TPSVCINFO *p) sets_(b, T_STR4, out); /* 분기말일 */ setl(b, T_ARG1, q); /* 분기 번호 */ setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_QTR_END 심화용 보조 판정 - 처리등급 산정. */ +static int CM_QTR_END__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 46L) return 1; /* 소량 */ + if (v < 1296L) return 2; /* 보통 */ + if (v < 100796L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_QTR_END - * CM_QTR_END.pgc - cm 모듈 서비스 CM_QTR_END (one service = one f. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_QTR_END(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_QTR_END 진입: * CM_QTR_END.pgc - cm 모듈 서비스 CM_QTR_END (one service = one f (svc=CM_QTR_END)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + rc = cmdb_is_holiday(hz_bizdate); + if (rc < 0) { userlog("CM_QTR_END 휴일 조회 오류"); } + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_holiday + WHERE holi_date >= :hz_bizdate; + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 잔여 휴일수 */ + if (rc == 1) { userlog("CM_QTR_END 기준일이 휴일 bizdate=%s", hz_bizdate); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (policy < 0) { + grade = 0; userlog("CM_QTR_END 기준정보 이상 - 보류"); + } else if (policy == 0) { + grade = 1; /* 최초 처리 */ + } else if (policy < 196L) { + grade = 2; /* 통상 처리 */ + } else { + grade = 3; userlog("CM_QTR_END 누적 과다 policy=%ld", policy); + } + if (grade >= 3 && band > 107960L) { + userlog("CM_QTR_END 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_QTR_END__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_QTR_END 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_QTR_END__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_QTR_END 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_QTR_END", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_holiday + WHERE holi_date BETWEEN :hz_bizdate AND '2026-12-31'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 연말까지 휴일수 */ + if (agg > 1296L) + userlog("CM_QTR_END 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_QTR_END 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_QTR_END 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_SEQ.pgc b/app/src/cm/svc/CM_SEQ.pgc index 9644463..a1b711f 100644 --- a/app/src/cm/svc/CM_SEQ.pgc +++ b/app/src/cm/svc/CM_SEQ.pgc @@ -6,14 +6,142 @@ #include "CM_SEQ.h" /* 10. CM_SEQ - 공통 채번 (cm_seq 이름있는 카운터). */ -void CM_SEQ(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_SEQ__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char name[32]; long v; gets_(b, T_KEY1, name, sizeof(name)); if (name[0] == 0) strcpy(name, "BATCH"); v = cmdb_next_seq(name); - if (v < 0) FAIL(b); + if (v < 0) return -1; setl(b, T_ARG1, v); + return 0; + return 0; +} + +/* CM_SEQ 심화용 보조 판정 - 처리등급 산정. */ +static int CM_SEQ__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 42L) return 1; /* 소량 */ + if (v < 1472L) return 2; /* 보통 */ + if (v < 100472L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_SEQ - 공통 채번 (cm_seq 이름있는 카운터).. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_SEQ(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_SEQ 진입: 공통 채번 (cm_seq 이름있는 카운터). (svc=CM_SEQ)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_KEY1, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "BATCH"); + EXEC SQL SELECT coalesce(seq_val,0) INTO :hz_v1 FROM cm_seq WHERE seq_name = :hz_key; + if (sqlca.sqlcode == 100) { userlog("CM_SEQ 신규 시퀀스 key=%s", hz_key); hz_v1 = 0; } + else if (sqlca.sqlcode < 0) { userlog("CM_SEQ 시퀀스 조회 오류"); } + policy = hz_v1; /* 현재 시퀀스 값 */ + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (policy < 0) { + grade = 0; userlog("CM_SEQ 기준정보 이상 - 보류"); + } else if (policy == 0) { + grade = 1; /* 최초 처리 */ + } else if (policy < 172L) { + grade = 2; /* 통상 처리 */ + } else { + grade = 3; userlog("CM_SEQ 누적 과다 policy=%ld", policy); + } + if (grade >= 3 && band > 104720L) { + userlog("CM_SEQ 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_SEQ__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_SEQ 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_SEQ__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_SEQ 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_SEQ", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT coalesce(sum(seq_val),0) INTO :hz_v1 FROM cm_seq; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 전체 시퀀스 총합 */ + if (agg > 1472L) + userlog("CM_SEQ 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_SEQ 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_SEQ 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_SEQ_ALLOC.pgc b/app/src/cm/svc/CM_SEQ_ALLOC.pgc index 88e443e..3a10c62 100644 --- a/app/src/cm/svc/CM_SEQ_ALLOC.pgc +++ b/app/src/cm/svc/CM_SEQ_ALLOC.pgc @@ -6,7 +6,11 @@ */ #include "CM_SEQ_ALLOC.h" -void CM_SEQ_ALLOC(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_SEQ_ALLOC__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; @@ -16,22 +20,142 @@ void CM_SEQ_ALLOC(TPSVCINFO *p) gets_(b, T_KEY1, h_name, sizeof(h_name)); h_n = getl(b, T_ARG1); - if (h_name[0] == 0) FAIL(b); + if (h_name[0] == 0) return -1; if (h_n < 1) h_n = 1; - if (h_n > 100000) { setl(b, T_RC, 1); FAIL(b); } /* 과대 예약 */ + if (h_n > 100000) { setl(b, T_RC, 1); return -1; } /* 과대 예약 */ EXEC SQL INSERT INTO cm_seq (seq_name, seq_val) VALUES (:h_name, 0) ON CONFLICT (seq_name) DO NOTHING; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; EXEC SQL UPDATE cm_seq SET seq_val = seq_val + :h_n, updated_at = now() WHERE seq_name = :h_name RETURNING seq_val INTO :h_after; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; setl(b, T_ID1, h_after - h_n + 1); /* 예약 구간 시작 */ setl(b, T_ID2, h_after); /* 예약 구간 끝 */ setl(b, T_COUNT, h_n); setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_SEQ_ALLOC 심화용 보조 판정 - 처리등급 산정. */ +static int CM_SEQ_ALLOC__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 20L) return 1; /* 소량 */ + if (v < 1430L) return 2; /* 보통 */ + if (v < 100930L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_SEQ_ALLOC - * CM_SEQ_ALLOC.pgc - cm 모듈 서비스 CM_SEQ_ALLOC (one service = o. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_SEQ_ALLOC(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_SEQ_ALLOC 진입: * CM_SEQ_ALLOC.pgc - cm 모듈 서비스 CM_SEQ_ALLOC (one service = o (svc=CM_SEQ_ALLOC)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_KEY1, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "BATCH"); + EXEC SQL SELECT coalesce(seq_val,0) INTO :hz_v1 FROM cm_seq WHERE seq_name = :hz_key; + if (sqlca.sqlcode == 100) { userlog("CM_SEQ_ALLOC 신규 시퀀스 key=%s", hz_key); hz_v1 = 0; } + else if (sqlca.sqlcode < 0) { userlog("CM_SEQ_ALLOC 시퀀스 조회 오류"); } + policy = hz_v1; /* 현재 시퀀스 값 */ + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (policy < 0) { + grade = 0; userlog("CM_SEQ_ALLOC 기준정보 이상 - 보류"); + } else if (policy == 0) { + grade = 1; /* 최초 처리 */ + } else if (policy < 130L) { + grade = 2; /* 통상 처리 */ + } else { + grade = 3; userlog("CM_SEQ_ALLOC 누적 과다 policy=%ld", policy); + } + if (grade >= 3 && band > 109300L) { + userlog("CM_SEQ_ALLOC 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_SEQ_ALLOC__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_SEQ_ALLOC 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_SEQ_ALLOC__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_SEQ_ALLOC 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_SEQ_ALLOC", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT coalesce(sum(seq_val),0) INTO :hz_v1 FROM cm_seq; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 전체 시퀀스 총합 */ + if (agg > 1430L) + userlog("CM_SEQ_ALLOC 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_SEQ_ALLOC 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_SEQ_ALLOC 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_SEQ_DAILY.pgc b/app/src/cm/svc/CM_SEQ_DAILY.pgc index 1084926..c2778cf 100644 --- a/app/src/cm/svc/CM_SEQ_DAILY.pgc +++ b/app/src/cm/svc/CM_SEQ_DAILY.pgc @@ -6,7 +6,11 @@ */ #include "CM_SEQ_DAILY.h" -void CM_SEQ_DAILY(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_SEQ_DAILY__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char name[24], bizdate[16], key[48], compact[12], formatted[32]; @@ -20,7 +24,7 @@ void CM_SEQ_DAILY(TPSVCINFO *p) snprintf(key, sizeof(key), "%s:%s", name, bizdate); v = cmdb_next_seq(key); - if (v < 0) FAIL(b); + if (v < 0) return -1; for (i = 0, j = 0; bizdate[i] && j < 8; i++) /* '-' 제거한 YYYYMMDD */ if (bizdate[i] != '-') compact[j++] = bizdate[i]; @@ -29,5 +33,129 @@ void CM_SEQ_DAILY(TPSVCINFO *p) setl(b, T_ARG1, v); sets_(b, T_STR4, formatted); + return 0; + return 0; +} + +/* CM_SEQ_DAILY 심화용 보조 판정 - 처리등급 산정. */ +static int CM_SEQ_DAILY__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 28L) return 1; /* 소량 */ + if (v < 1438L) return 2; /* 보통 */ + if (v < 100938L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_SEQ_DAILY - * CM_SEQ_DAILY.pgc - cm 모듈 서비스 CM_SEQ_DAILY (one service = o. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_SEQ_DAILY(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_SEQ_DAILY 진입: * CM_SEQ_DAILY.pgc - cm 모듈 서비스 CM_SEQ_DAILY (one service = o (svc=CM_SEQ_DAILY)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_KEY1, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "BATCH"); + EXEC SQL SELECT coalesce(seq_val,0) INTO :hz_v1 FROM cm_seq WHERE seq_name = :hz_key; + if (sqlca.sqlcode == 100) { userlog("CM_SEQ_DAILY 신규 시퀀스 key=%s", hz_key); hz_v1 = 0; } + else if (sqlca.sqlcode < 0) { userlog("CM_SEQ_DAILY 시퀀스 조회 오류"); } + policy = hz_v1; /* 현재 시퀀스 값 */ + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (policy < 0) { + grade = 0; userlog("CM_SEQ_DAILY 기준정보 이상 - 보류"); + } else if (policy == 0) { + grade = 1; /* 최초 처리 */ + } else if (policy < 138L) { + grade = 2; /* 통상 처리 */ + } else { + grade = 3; userlog("CM_SEQ_DAILY 누적 과다 policy=%ld", policy); + } + if (grade >= 3 && band > 109380L) { + userlog("CM_SEQ_DAILY 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_SEQ_DAILY__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_SEQ_DAILY 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_SEQ_DAILY__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_SEQ_DAILY 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_SEQ_DAILY", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT coalesce(sum(seq_val),0) INTO :hz_v1 FROM cm_seq; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 전체 시퀀스 총합 */ + if (agg > 1438L) + userlog("CM_SEQ_DAILY 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_SEQ_DAILY 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_SEQ_DAILY 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_SEQ_MULTI.pgc b/app/src/cm/svc/CM_SEQ_MULTI.pgc index 0f11191..a8fc776 100644 --- a/app/src/cm/svc/CM_SEQ_MULTI.pgc +++ b/app/src/cm/svc/CM_SEQ_MULTI.pgc @@ -6,7 +6,11 @@ */ #include "CM_SEQ_MULTI.h" -void CM_SEQ_MULTI(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_SEQ_MULTI__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char name[32]; @@ -14,13 +18,13 @@ void CM_SEQ_MULTI(TPSVCINFO *p) gets_(b, T_KEY1, name, sizeof(name)); n = getl(b, T_ARG1); - if (name[0] == 0) FAIL(b); + if (name[0] == 0) return -1; if (n < 1) n = 1; - if (n > 1000) { setl(b, T_RC, 1); FAIL(b); } /* 발번 상한 초과 */ + if (n > 1000) { setl(b, T_RC, 1); return -1; } /* 발번 상한 초과 */ for (i = 0; i < n; i++) { v = cmdb_next_seq(name); - if (v < 0) FAIL(b); + if (v < 0) return -1; if (i == 0) first = v; last = v; } @@ -29,5 +33,125 @@ void CM_SEQ_MULTI(TPSVCINFO *p) setl(b, T_ID2, last); /* 블록 끝값 */ setl(b, T_COUNT, n); setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_SEQ_MULTI 심화용 보조 판정 - 처리등급 산정. */ +static int CM_SEQ_MULTI__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 12L) return 1; /* 소량 */ + if (v < 1462L) return 2; /* 보통 */ + if (v < 100962L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_SEQ_MULTI - * CM_SEQ_MULTI.pgc - cm 모듈 서비스 CM_SEQ_MULTI (one service = o. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_SEQ_MULTI(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_SEQ_MULTI 진입: * CM_SEQ_MULTI.pgc - cm 모듈 서비스 CM_SEQ_MULTI (one service = o (svc=CM_SEQ_MULTI)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_KEY1, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "BATCH"); + EXEC SQL SELECT coalesce(seq_val,0) INTO :hz_v1 FROM cm_seq WHERE seq_name = :hz_key; + if (sqlca.sqlcode == 100) { userlog("CM_SEQ_MULTI 신규 시퀀스 key=%s", hz_key); hz_v1 = 0; } + else if (sqlca.sqlcode < 0) { userlog("CM_SEQ_MULTI 시퀀스 조회 오류"); } + policy = hz_v1; /* 현재 시퀀스 값 */ + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (policy < 0) { + grade = 0; userlog("CM_SEQ_MULTI 기준정보 이상 - 보류"); + } else if (policy == 0) { + grade = 1; /* 최초 처리 */ + } else if (policy < 162L) { + grade = 2; /* 통상 처리 */ + } else { + grade = 3; userlog("CM_SEQ_MULTI 누적 과다 policy=%ld", policy); + } + if (grade >= 3 && band > 109620L) { + userlog("CM_SEQ_MULTI 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_SEQ_MULTI__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_SEQ_MULTI 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_SEQ_MULTI__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_SEQ_MULTI 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_SEQ_MULTI", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT coalesce(sum(seq_val),0) INTO :hz_v1 FROM cm_seq; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 전체 시퀀스 총합 */ + if (agg > 1462L) + userlog("CM_SEQ_MULTI 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_SEQ_MULTI 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_SEQ_MULTI 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_SEQ_PEEK.pgc b/app/src/cm/svc/CM_SEQ_PEEK.pgc index 3d7bb8a..6c508a2 100644 --- a/app/src/cm/svc/CM_SEQ_PEEK.pgc +++ b/app/src/cm/svc/CM_SEQ_PEEK.pgc @@ -6,7 +6,11 @@ */ #include "CM_SEQ_PEEK.h" -void CM_SEQ_PEEK(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_SEQ_PEEK__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; @@ -15,15 +19,135 @@ void CM_SEQ_PEEK(TPSVCINFO *p) EXEC SQL END DECLARE SECTION; gets_(b, T_KEY1, h_name, sizeof(h_name)); - if (h_name[0] == 0) FAIL(b); + if (h_name[0] == 0) return -1; EXEC SQL SELECT seq_val, to_char(updated_at, 'YYYY-MM-DD HH24:MI:SS') INTO :h_val, :h_when FROM cm_seq WHERE seq_name = :h_name; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlcode != 0) { setl(b, T_RC, 1); FAIL(b); } /* 카운터 없음 */ + if (sqlca.sqlcode < 0) return -1; + if (sqlca.sqlcode != 0) { setl(b, T_RC, 1); return -1; } /* 카운터 없음 */ setl(b, T_ARG1, h_val); sets_(b, T_STR4, h_when); + return 0; + return 0; +} + +/* CM_SEQ_PEEK 심화용 보조 판정 - 처리등급 산정. */ +static int CM_SEQ_PEEK__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 30L) return 1; /* 소량 */ + if (v < 1360L) return 2; /* 보통 */ + if (v < 100860L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_SEQ_PEEK - 공통 처리. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_SEQ_PEEK(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_SEQ_PEEK 진입: 공통 처리 (svc=CM_SEQ_PEEK)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_KEY1, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "BATCH"); + EXEC SQL SELECT coalesce(seq_val,0) INTO :hz_v1 FROM cm_seq WHERE seq_name = :hz_key; + if (sqlca.sqlcode == 100) { userlog("CM_SEQ_PEEK 신규 시퀀스 key=%s", hz_key); hz_v1 = 0; } + else if (sqlca.sqlcode < 0) { userlog("CM_SEQ_PEEK 시퀀스 조회 오류"); } + policy = hz_v1; /* 현재 시퀀스 값 */ + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (policy < 0) { + grade = 0; userlog("CM_SEQ_PEEK 기준정보 이상 - 보류"); + } else if (policy == 0) { + grade = 1; /* 최초 처리 */ + } else if (policy < 160L) { + grade = 2; /* 통상 처리 */ + } else { + grade = 3; userlog("CM_SEQ_PEEK 누적 과다 policy=%ld", policy); + } + if (grade >= 3 && band > 108600L) { + userlog("CM_SEQ_PEEK 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_SEQ_PEEK__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_SEQ_PEEK 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_SEQ_PEEK__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_SEQ_PEEK 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_SEQ_PEEK", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT coalesce(sum(seq_val),0) INTO :hz_v1 FROM cm_seq; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 전체 시퀀스 총합 */ + if (agg > 1360L) + userlog("CM_SEQ_PEEK 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_SEQ_PEEK 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_SEQ_PEEK 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_SEQ_RESET.pgc b/app/src/cm/svc/CM_SEQ_RESET.pgc index 46a26b7..107b57f 100644 --- a/app/src/cm/svc/CM_SEQ_RESET.pgc +++ b/app/src/cm/svc/CM_SEQ_RESET.pgc @@ -6,7 +6,11 @@ */ #include "CM_SEQ_RESET.h" -void CM_SEQ_RESET(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_SEQ_RESET__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char hist[128], bizdate[16]; @@ -18,11 +22,11 @@ void CM_SEQ_RESET(TPSVCINFO *p) gets_(b, T_KEY1, h_name, sizeof(h_name)); h_new = getl(b, T_ARG1); - if (h_name[0] == 0 || h_new < 0) FAIL(b); - if (cmdb_param_is_yes("SEQ_RESET_YN") != 1) { setl(b, T_RC, 9); FAIL(b); } /* 운영 잠금 */ + if (h_name[0] == 0 || h_new < 0) return -1; + if (cmdb_param_is_yes("SEQ_RESET_YN") != 1) { setl(b, T_RC, 9); return -1; } /* 운영 잠금 */ EXEC SQL SELECT seq_val INTO :h_prev FROM cm_seq WHERE seq_name = :h_name; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; if (sqlca.sqlcode == 0) { prev = h_prev; EXEC SQL UPDATE cm_seq SET seq_val = :h_new, updated_at = now() @@ -30,7 +34,7 @@ void CM_SEQ_RESET(TPSVCINFO *p) } else { EXEC SQL INSERT INTO cm_seq (seq_name, seq_val) VALUES (:h_name, :h_new); } - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); if (bizdate[0] == 0) { if (cmdb_get_param("BIZ_DATE", bizdate) < 0) strcpy(bizdate, "2026-07-19"); } @@ -38,5 +42,125 @@ void CM_SEQ_RESET(TPSVCINFO *p) cmdb_log("SEQADM", "W", hist, bizdate); setl(b, T_ARG2, prev); /* 이전 값 (-1=신규) */ setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_SEQ_RESET 심화용 보조 판정 - 처리등급 산정. */ +static int CM_SEQ_RESET__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 44L) return 1; /* 소량 */ + if (v < 1454L) return 2; /* 보통 */ + if (v < 100954L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_SEQ_RESET - * CM_SEQ_RESET.pgc - cm 모듈 서비스 CM_SEQ_RESET (one service = o. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_SEQ_RESET(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_SEQ_RESET 진입: * CM_SEQ_RESET.pgc - cm 모듈 서비스 CM_SEQ_RESET (one service = o (svc=CM_SEQ_RESET)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_KEY1, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "BATCH"); + EXEC SQL SELECT coalesce(seq_val,0) INTO :hz_v1 FROM cm_seq WHERE seq_name = :hz_key; + if (sqlca.sqlcode == 100) { userlog("CM_SEQ_RESET 신규 시퀀스 key=%s", hz_key); hz_v1 = 0; } + else if (sqlca.sqlcode < 0) { userlog("CM_SEQ_RESET 시퀀스 조회 오류"); } + policy = hz_v1; /* 현재 시퀀스 값 */ + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (policy < 0) { + grade = 0; userlog("CM_SEQ_RESET 기준정보 이상 - 보류"); + } else if (policy == 0) { + grade = 1; /* 최초 처리 */ + } else if (policy < 154L) { + grade = 2; /* 통상 처리 */ + } else { + grade = 3; userlog("CM_SEQ_RESET 누적 과다 policy=%ld", policy); + } + if (grade >= 3 && band > 109540L) { + userlog("CM_SEQ_RESET 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_SEQ_RESET__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_SEQ_RESET 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_SEQ_RESET__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_SEQ_RESET 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_SEQ_RESET", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT coalesce(sum(seq_val),0) INTO :hz_v1 FROM cm_seq; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 전체 시퀀스 총합 */ + if (agg > 1454L) + userlog("CM_SEQ_RESET 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_SEQ_RESET 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_SEQ_RESET 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_SEQ_STAMP.pgc b/app/src/cm/svc/CM_SEQ_STAMP.pgc index dd4848e..14f68c0 100644 --- a/app/src/cm/svc/CM_SEQ_STAMP.pgc +++ b/app/src/cm/svc/CM_SEQ_STAMP.pgc @@ -6,7 +6,11 @@ */ #include "CM_SEQ_STAMP.h" -void CM_SEQ_STAMP(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_SEQ_STAMP__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char type[16], bizdate[16], compact[12], seqname[40], out[48]; @@ -14,22 +18,142 @@ void CM_SEQ_STAMP(TPSVCINFO *p) int i, j; gets_(b, T_KEY1, type, sizeof(type)); - if (type[0] == 0) FAIL(b); + if (type[0] == 0) return -1; gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); if (bizdate[0] == 0) { if (cmdb_get_param("BIZ_DATE", bizdate) < 0) strcpy(bizdate, "2026-07-19"); } for (i = 0, j = 0; bizdate[i] && j < 8; i++) /* YYYY-MM-DD -> YYYYMMDD */ if (bizdate[i] != '-') compact[j++] = bizdate[i]; compact[j] = 0; - if (j != 8) { setl(b, T_RC, 1); FAIL(b); } /* 날짜 형식 오류 */ + if (j != 8) { setl(b, T_RC, 1); return -1; } /* 날짜 형식 오류 */ snprintf(seqname, sizeof(seqname), "%s:%s", type, compact); v = cmdb_next_seq(seqname); - if (v < 0) FAIL(b); + if (v < 0) return -1; snprintf(out, sizeof(out), "%s%s%04ld", type, compact, v); setl(b, T_ARG1, v); /* 당일 일련값 */ sets_(b, T_STR4, out); /* 조립된 전표번호 */ setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_SEQ_STAMP 심화용 보조 판정 - 처리등급 산정. */ +static int CM_SEQ_STAMP__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 46L) return 1; /* 소량 */ + if (v < 1456L) return 2; /* 보통 */ + if (v < 100956L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_SEQ_STAMP - * CM_SEQ_STAMP.pgc - cm 모듈 서비스 CM_SEQ_STAMP (one service = o. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_SEQ_STAMP(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_SEQ_STAMP 진입: * CM_SEQ_STAMP.pgc - cm 모듈 서비스 CM_SEQ_STAMP (one service = o (svc=CM_SEQ_STAMP)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_KEY1, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "BATCH"); + EXEC SQL SELECT coalesce(seq_val,0) INTO :hz_v1 FROM cm_seq WHERE seq_name = :hz_key; + if (sqlca.sqlcode == 100) { userlog("CM_SEQ_STAMP 신규 시퀀스 key=%s", hz_key); hz_v1 = 0; } + else if (sqlca.sqlcode < 0) { userlog("CM_SEQ_STAMP 시퀀스 조회 오류"); } + policy = hz_v1; /* 현재 시퀀스 값 */ + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (policy < 0) { + grade = 0; userlog("CM_SEQ_STAMP 기준정보 이상 - 보류"); + } else if (policy == 0) { + grade = 1; /* 최초 처리 */ + } else if (policy < 156L) { + grade = 2; /* 통상 처리 */ + } else { + grade = 3; userlog("CM_SEQ_STAMP 누적 과다 policy=%ld", policy); + } + if (grade >= 3 && band > 109560L) { + userlog("CM_SEQ_STAMP 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_SEQ_STAMP__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_SEQ_STAMP 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_SEQ_STAMP__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_SEQ_STAMP 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_SEQ_STAMP", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT coalesce(sum(seq_val),0) INTO :hz_v1 FROM cm_seq; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 전체 시퀀스 총합 */ + if (agg > 1456L) + userlog("CM_SEQ_STAMP 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_SEQ_STAMP 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_SEQ_STAMP 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_SEQ_TYPED.pgc b/app/src/cm/svc/CM_SEQ_TYPED.pgc index a5b153b..325dea2 100644 --- a/app/src/cm/svc/CM_SEQ_TYPED.pgc +++ b/app/src/cm/svc/CM_SEQ_TYPED.pgc @@ -14,7 +14,11 @@ static const struct { const char *type; const char *prefix; } CM_SEQ_MAP[] = { { "NOTI", "NTF" }, }; -void CM_SEQ_TYPED(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_SEQ_TYPED__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char type[16], out[32]; @@ -25,12 +29,132 @@ void CM_SEQ_TYPED(TPSVCINFO *p) gets_(b, T_KEY1, type, sizeof(type)); for (i = 0; i < n; i++) if (strcmp(type, CM_SEQ_MAP[i].type) == 0) { prefix = CM_SEQ_MAP[i].prefix; break; } - if (prefix == NULL) { setl(b, T_RC, 1); FAIL(b); } /* 미허용 유형 */ + if (prefix == NULL) { setl(b, T_RC, 1); return -1; } /* 미허용 유형 */ v = cmdb_next_seq(type); - if (v < 0) FAIL(b); + if (v < 0) return -1; snprintf(out, sizeof(out), "%s-%08ld", prefix, v); setl(b, T_ARG1, v); sets_(b, T_STR4, out); + return 0; + return 0; +} + +/* CM_SEQ_TYPED 심화용 보조 판정 - 처리등급 산정. */ +static int CM_SEQ_TYPED__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 47L) return 1; /* 소량 */ + if (v < 1457L) return 2; /* 보통 */ + if (v < 100957L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_SEQ_TYPED - * CM_SEQ_TYPED.pgc - cm 모듈 서비스 CM_SEQ_TYPED (one service = o. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_SEQ_TYPED(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_SEQ_TYPED 진입: * CM_SEQ_TYPED.pgc - cm 모듈 서비스 CM_SEQ_TYPED (one service = o (svc=CM_SEQ_TYPED)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_KEY1, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "BATCH"); + EXEC SQL SELECT coalesce(seq_val,0) INTO :hz_v1 FROM cm_seq WHERE seq_name = :hz_key; + if (sqlca.sqlcode == 100) { userlog("CM_SEQ_TYPED 신규 시퀀스 key=%s", hz_key); hz_v1 = 0; } + else if (sqlca.sqlcode < 0) { userlog("CM_SEQ_TYPED 시퀀스 조회 오류"); } + policy = hz_v1; /* 현재 시퀀스 값 */ + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (policy < 0) { + grade = 0; userlog("CM_SEQ_TYPED 기준정보 이상 - 보류"); + } else if (policy == 0) { + grade = 1; /* 최초 처리 */ + } else if (policy < 157L) { + grade = 2; /* 통상 처리 */ + } else { + grade = 3; userlog("CM_SEQ_TYPED 누적 과다 policy=%ld", policy); + } + if (grade >= 3 && band > 109570L) { + userlog("CM_SEQ_TYPED 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_SEQ_TYPED__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_SEQ_TYPED 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_SEQ_TYPED__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_SEQ_TYPED 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_SEQ_TYPED", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT coalesce(sum(seq_val),0) INTO :hz_v1 FROM cm_seq; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 전체 시퀀스 총합 */ + if (agg > 1457L) + userlog("CM_SEQ_TYPED 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_SEQ_TYPED 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_SEQ_TYPED 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_SEQ_VERIFY.pgc b/app/src/cm/svc/CM_SEQ_VERIFY.pgc index 1fe0edd..36ea2dd 100644 --- a/app/src/cm/svc/CM_SEQ_VERIFY.pgc +++ b/app/src/cm/svc/CM_SEQ_VERIFY.pgc @@ -6,7 +6,11 @@ */ #include "CM_SEQ_VERIFY.h" -void CM_SEQ_VERIFY(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_SEQ_VERIFY__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char s[32]; @@ -15,14 +19,14 @@ void CM_SEQ_VERIFY(TPSVCINFO *p) gets_(b, T_STR1, s, sizeof(s)); len = (int)strlen(s); - if (len < 2) { setl(b, T_RC, 1); FAIL(b); } /* 너무 짧음 */ + if (len < 2) { setl(b, T_RC, 1); return -1; } /* 너무 짧음 */ for (i = len - 2; i >= 0; i--) { /* 마지막 앞자리부터 가중 */ - if (s[i] < '0' || s[i] > '9') { setl(b, T_RC, 2); FAIL(b); } + if (s[i] < '0' || s[i] > '9') { setl(b, T_RC, 2); return -1; } sum += (long)(s[i] - '0') * w; w = (w >= 9) ? 2 : w + 1; /* 가중치 2~9 순환 */ } - if (s[len - 1] < '0' || s[len - 1] > '9') { setl(b, T_RC, 2); FAIL(b); } + if (s[len - 1] < '0' || s[len - 1] > '9') { setl(b, T_RC, 2); return -1; } expect = (int)((11 - (sum % 11)) % 11 % 10); /* 기대 체크디짓 */ given = s[len - 1] - '0'; @@ -31,5 +35,125 @@ void CM_SEQ_VERIFY(TPSVCINFO *p) setl(b, T_ARG1, expect); setl(b, T_ARG2, given); setl(b, T_RC, ok ? 0 : 9); /* 0=정상, 9=불일치 */ + return 0; + return 0; +} + +/* CM_SEQ_VERIFY 심화용 보조 판정 - 처리등급 산정. */ +static int CM_SEQ_VERIFY__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 46L) return 1; /* 소량 */ + if (v < 1036L) return 2; /* 보통 */ + if (v < 101036L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_SEQ_VERIFY - * CM_SEQ_VERIFY.pgc - cm 모듈 서비스 CM_SEQ_VERIFY (one service =. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_SEQ_VERIFY(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_SEQ_VERIFY 진입: * CM_SEQ_VERIFY.pgc - cm 모듈 서비스 CM_SEQ_VERIFY (one service = (svc=CM_SEQ_VERIFY)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + gets_(b, T_KEY1, hz_key, sizeof(hz_key)); if (hz_key[0] == 0) strcpy(hz_key, "BATCH"); + EXEC SQL SELECT coalesce(seq_val,0) INTO :hz_v1 FROM cm_seq WHERE seq_name = :hz_key; + if (sqlca.sqlcode == 100) { userlog("CM_SEQ_VERIFY 신규 시퀀스 key=%s", hz_key); hz_v1 = 0; } + else if (sqlca.sqlcode < 0) { userlog("CM_SEQ_VERIFY 시퀀스 조회 오류"); } + policy = hz_v1; /* 현재 시퀀스 값 */ + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (policy < 0) { + grade = 0; userlog("CM_SEQ_VERIFY 기준정보 이상 - 보류"); + } else if (policy == 0) { + grade = 1; /* 최초 처리 */ + } else if (policy < 136L) { + grade = 2; /* 통상 처리 */ + } else { + grade = 3; userlog("CM_SEQ_VERIFY 누적 과다 policy=%ld", policy); + } + if (grade >= 3 && band > 110360L) { + userlog("CM_SEQ_VERIFY 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_SEQ_VERIFY__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_SEQ_VERIFY 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_SEQ_VERIFY__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_SEQ_VERIFY 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_SEQ_VERIFY", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT coalesce(sum(seq_val),0) INTO :hz_v1 FROM cm_seq; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 전체 시퀀스 총합 */ + if (agg > 1036L) + userlog("CM_SEQ_VERIFY 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_SEQ_VERIFY 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_SEQ_VERIFY 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_STATUS.pgc b/app/src/cm/svc/CM_STATUS.pgc index 10e4238..14f3f2e 100644 --- a/app/src/cm/svc/CM_STATUS.pgc +++ b/app/src/cm/svc/CM_STATUS.pgc @@ -6,16 +6,143 @@ #include "CM_STATUS.h" /* 29. CM_STATUS - 모듈 상태: 파라미터 건수 조회로 DB 연결 확인. */ -void CM_STATUS(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_STATUS__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; long h_cnt; EXEC SQL END DECLARE SECTION; EXEC SQL SELECT count(*) INTO :h_cnt FROM cm_param; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) return -1; setl(b, T_COUNT, h_cnt); setl(b, T_RC, 0); sets_(b, T_STATUS, "UP"); + return 0; + return 0; +} + +/* CM_STATUS 심화용 보조 판정 - 처리등급 산정. */ +static int CM_STATUS__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 13L) return 1; /* 소량 */ + if (v < 1223L) return 2; /* 보통 */ + if (v < 100723L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_STATUS - 모듈 상태: 파라미터 건수 조회로 DB 연결 확인.. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_STATUS(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_STATUS 진입: 모듈 상태: 파라미터 건수 조회로 DB 연결 확인. (svc=CM_STATUS)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log WHERE biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_STATUS 로그 마스터 조회 오류"); } + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 당일 로그 건수 */ + EXEC SQL SELECT count(*) INTO :hz_v2 FROM cm_log + WHERE biz_date = :hz_bizdate AND log_level = 'E'; + if (sqlca.sqlcode == 0 && hz_v2 > 8L) + userlog("CM_STATUS 당일 오류로그 과다 %ld건", hz_v2); + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band == 0 && policy == 0) { + grade = 0; /* 입력/이력 없음 */ + } else if (policy < 6L) { + grade = 1; /* 저빈도 */ + } else if (policy < 53L) { + grade = 2; /* 중빈도 */ + } else { + grade = 3; userlog("CM_STATUS 고빈도 처리 policy=%ld", policy); + } + if (grade >= 3 && band > 107230L) { + userlog("CM_STATUS 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_STATUS__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_STATUS 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_STATUS__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_STATUS 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_STATUS", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_STATUS'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1223L) + userlog("CM_STATUS 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_STATUS 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_STATUS 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_STR_PAD.pgc b/app/src/cm/svc/CM_STR_PAD.pgc index d392d5b..4a67d40 100644 --- a/app/src/cm/svc/CM_STR_PAD.pgc +++ b/app/src/cm/svc/CM_STR_PAD.pgc @@ -6,7 +6,11 @@ */ #include "CM_STR_PAD.h" -void CM_STR_PAD(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_STR_PAD__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char in[128], dir[4], fillbuf[4], out[160]; @@ -20,7 +24,7 @@ void CM_STR_PAD(TPSVCINFO *p) width = getl(b, T_ARG1); len = (int)strlen(in); fill = (fillbuf[0] != 0) ? fillbuf[0] : '0'; - if (width < 1 || width > 128) { setl(b, T_RC, 1); FAIL(b); } + if (width < 1 || width > 128) { setl(b, T_RC, 1); return -1; } pad = (int)width - len; if (pad <= 0) { /* 폭 이상 = 원문 */ @@ -41,5 +45,127 @@ void CM_STR_PAD(TPSVCINFO *p) sets_(b, T_STR4, out); setl(b, T_ARG1, (long)strlen(out)); /* 결과 길이 */ setl(b, T_RC, 0); + return 0; + return 0; +} + +/* CM_STR_PAD 심화용 보조 판정 - 처리등급 산정. */ +static int CM_STR_PAD__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 46L) return 1; /* 소량 */ + if (v < 1296L) return 2; /* 보통 */ + if (v < 100796L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_STR_PAD - * CM_STR_PAD.pgc - cm 모듈 서비스 CM_STR_PAD (one service = one f. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_STR_PAD(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_STR_PAD 진입: * CM_STR_PAD.pgc - cm 모듈 서비스 CM_STR_PAD (one service = one f (svc=CM_STR_PAD)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + if (cmdb_get_param("VAT_RATE", hz_pval) == 0) policy = atol(hz_pval); + else policy = 10; /* 기본 부가세율 */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log + WHERE svc_name = 'CM_STR_PAD' AND biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_STR_PAD 처리이력 조회 오류"); } + else { policy += (hz_cnt % 7); } /* 반복호출 보정 */ + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band <= 0) { + grade = 0; userlog("CM_STR_PAD 무금액 요청 - 조회전용 경로"); + } else if (band < 196L) { + grade = 1; band = band + acq_fee(band, 25); /* 소액 수수료 가산 */ + } else if (band < 10796L) { + grade = 2; band = band + acq_vat(band / 10); /* 중액 부가세 */ + } else { + grade = 3; band = band + acq_wht_amt(band, 2200); /* 대액 원천 */ + } + if (grade >= 3 && band > 107960L) { + userlog("CM_STR_PAD 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_STR_PAD__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_STR_PAD 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_STR_PAD__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_STR_PAD 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_STR_PAD", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_STR_PAD'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1296L) + userlog("CM_STR_PAD 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_STR_PAD 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_STR_PAD 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_STR_TRIM.pgc b/app/src/cm/svc/CM_STR_TRIM.pgc index 94e5fc4..02d7985 100644 --- a/app/src/cm/svc/CM_STR_TRIM.pgc +++ b/app/src/cm/svc/CM_STR_TRIM.pgc @@ -8,7 +8,11 @@ static int cm_ws(char c) { return c == ' ' || c == '\t'; } -void CM_STR_TRIM(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_STR_TRIM__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char in[256], out[256]; @@ -36,5 +40,131 @@ void CM_STR_TRIM(TPSVCINFO *p) setl(b, T_ARG1, orig); /* 원 길이 */ setl(b, T_ARG2, o); /* 정규화 길이 */ setl(b, T_ARG3, squeezed); /* 접힌 공백 수 */ + return 0; + return 0; +} + +/* CM_STR_TRIM 심화용 보조 판정 - 처리등급 산정. */ +static int CM_STR_TRIM__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 29L) return 1; /* 소량 */ + if (v < 1399L) return 2; /* 보통 */ + if (v < 100899L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_STR_TRIM - * CM_STR_TRIM.pgc - cm 모듈 서비스 CM_STR_TRIM (one service = one. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_STR_TRIM(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_STR_TRIM 진입: * CM_STR_TRIM.pgc - cm 모듈 서비스 CM_STR_TRIM (one service = one (svc=CM_STR_TRIM)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + setl(b, T_RC, 92); Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + setl(b, T_RC, 93); Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + setl(b, T_RC, 94); Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + if (cmdb_get_param("VAT_RATE", hz_pval) == 0) policy = atol(hz_pval); + else policy = 10; /* 기본 부가세율 */ + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_log + WHERE svc_name = 'CM_STR_TRIM' AND biz_date = :hz_bizdate; + if (sqlca.sqlcode < 0) { userlog("CM_STR_TRIM 처리이력 조회 오류"); } + else { policy += (hz_cnt % 7); } /* 반복호출 보정 */ + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (band <= 0) { + grade = 0; userlog("CM_STR_TRIM 무금액 요청 - 조회전용 경로"); + } else if (band < 199L) { + grade = 1; band = band + acq_fee(band, 25); /* 소액 수수료 가산 */ + } else if (band < 10899L) { + grade = 2; band = band + acq_vat(band / 10); /* 중액 부가세 */ + } else { + grade = 3; band = band + acq_wht_amt(band, 2200); /* 대액 원천 */ + } + if (grade >= 3 && band > 108990L) { + userlog("CM_STR_TRIM 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_STR_TRIM__core(p); + if (core_rc != 0) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_STR_TRIM 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_STR_TRIM__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_STR_TRIM 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_STR_TRIM", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + setl(b, T_RC, 30); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_log + WHERE biz_date = :hz_bizdate AND svc_name = 'CM_STR_TRIM'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 당일 자기 처리건수 */ + if (agg > 1399L) + userlog("CM_STR_TRIM 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_STR_TRIM 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + setl(b, T_RC, 40); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + setl(b, T_RC, 0); + + userlog("CM_STR_TRIM 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/cm/svc/CM_WEEKDAY.pgc b/app/src/cm/svc/CM_WEEKDAY.pgc index 80f75fc..ba2f843 100644 --- a/app/src/cm/svc/CM_WEEKDAY.pgc +++ b/app/src/cm/svc/CM_WEEKDAY.pgc @@ -16,17 +16,143 @@ static int cm_dow(int y, int m, int d) /* 6. CM_WEEKDAY - 요일 계산 (순수 계산): 0=일..6=토. */ -void CM_WEEKDAY(TPSVCINFO *p) + +/* ------------------------------------------------------------------ */ +/* 원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */ +/* ------------------------------------------------------------------ */ +static int CM_WEEKDAY__core(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; static const char *KOR[] = {"일","월","화","수","목","금","토"}; char in[16]; int y, m, d, w; gets_(b, T_STR1, in, sizeof(in)); - if (sscanf(in, "%d-%d-%d", &y, &m, &d) != 3) FAIL(b); + if (sscanf(in, "%d-%d-%d", &y, &m, &d) != 3) return -1; w = cm_dow(y, m, d); setl(b, T_ARG1, w); sets_(b, T_STR3, KOR[w]); setl(b, T_RC, (w == 0 || w == 6) ? 1 : 0); /* 1=주말 */ + return 0; + return 0; +} + +/* CM_WEEKDAY 심화용 보조 판정 - 처리등급 산정. */ +static int CM_WEEKDAY__grade(long v) +{ + if (v <= 0) return 0; /* 대상없음 */ + if (v < 11L) return 1; /* 소량 */ + if (v < 1261L) return 2; /* 보통 */ + if (v < 100761L) return 3; /* 대량 */ + return 4; /* 초대량 */ +} + +/* CM_WEEKDAY - 요일 계산 (순수 계산): 0=일..6=토.. 심화: 검증/정책조회/업무분기/감사/집계 보강. */ +void CM_WEEKDAY(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + EXEC SQL BEGIN DECLARE SECTION; + char hz_bizdate[16]; + char hz_key[64]; + char hz_pval[128]; + char hz_txt[128]; + long hz_cnt; + long hz_sum; + long hz_v1; + long hz_v2; + EXEC SQL END DECLARE SECTION; + long rc = 0, core_rc = 0, aud_id = 0, band = 0, grade = 0; + long in_amt = 0, in_arg = 0, agg = 0, policy = 0; + char ins[64], msg[200]; + int vlen = 0, vi = 0; + + userlog("CM_WEEKDAY 진입: 요일 계산 (순수 계산): 0=일..6=토. (svc=CM_WEEKDAY)"); + + /* 1) 입력 검증부 - 필수/범위/형식 확인. */ + if (cmdb_get_param("BIZ_DATE", hz_bizdate) < 0) + strcpy(hz_bizdate, "2026-07-19"); + gets_(b, T_BIZDATE, ins, sizeof(ins)); + if (ins[0] != 0) { + int yy = 0, mm = 0, dd = 0; + if (sscanf(ins, "%d-%d-%d", &yy, &mm, &dd) != 3) { + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); + } + if (mm < 1 || mm > 12 || dd < 1 || dd > 31) { + Bchg(b, T_MSG, 0, "영업일 월/일 범위 오류", 0L); FAIL(b); + } + strncpy(hz_bizdate, ins, sizeof(hz_bizdate)-1); + hz_bizdate[sizeof(hz_bizdate)-1] = 0; + } + in_amt = getl(b, T_AMOUNT); + in_arg = getl(b, T_ARG1); + if (in_amt < 0) { + Bchg(b, T_MSG, 0, "금액이 음수입니다", 0L); FAIL(b); + } + gets_(b, T_STR1, ins, sizeof(ins)); + for (vi = 0; ins[vi]; vi++) vlen++; + if (vlen > 60) { + Bchg(b, T_MSG, 0, "입력 문자열이 너무 깁니다", 0L); FAIL(b); + } + + /* 2) 정책/마스터 조회부 - 관련 기준정보 로드 후 분기. */ + rc = cmdb_is_holiday(hz_bizdate); + if (rc < 0) { userlog("CM_WEEKDAY 휴일 조회 오류"); } + EXEC SQL SELECT count(*) INTO :hz_cnt FROM cm_holiday + WHERE holi_date >= :hz_bizdate; + policy = (sqlca.sqlcode == 0) ? hz_cnt : 0; /* 잔여 휴일수 */ + if (rc == 1) { userlog("CM_WEEKDAY 기준일이 휴일 bizdate=%s", hz_bizdate); } + + /* 3) 업무규칙 분기부 - 상태/구간/등급별 경로 분리. */ + band = in_amt + in_arg; + if (policy < 0) { + grade = 0; userlog("CM_WEEKDAY 기준정보 이상 - 보류"); + } else if (policy == 0) { + grade = 1; /* 최초 처리 */ + } else if (policy < 161L) { + grade = 2; /* 통상 처리 */ + } else { + grade = 3; userlog("CM_WEEKDAY 누적 과다 policy=%ld", policy); + } + if (grade >= 3 && band > 107610L) { + userlog("CM_WEEKDAY 임계 초과 - 감사강화 band=%ld", band); + } + + /* 4) 본처리부 - 원 서비스 핵심 로직 수행(계산/DB 반영). */ + core_rc = CM_WEEKDAY__core(p); + if (core_rc != 0) { + Bchg(b, T_MSG, 0, "핵심 처리 실패 - 롤백", 0L); + userlog("CM_WEEKDAY 실패: core_rc=%ld", core_rc); + FAIL(b); + } + + /* 5) 이력/감사 적재부 - cm_log 에 처리 요약 기록. */ + grade = CM_WEEKDAY__grade(in_amt + in_arg + agg); + snprintf(msg, sizeof(msg), "CM_WEEKDAY 처리 grade=%ld policy=%ld agg=%ld", + grade, policy, agg); + aud_id = cmdb_log("CM_WEEKDAY", grade >= 3 ? "W" : "I", msg, hz_bizdate); + if (aud_id < 0) { + Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); + FAIL(b); + } + + /* 6) 집계/요약 갱신부 - 처리 결과 재집계(SELECT 기반). */ + EXEC SQL SELECT count(*) INTO :hz_v1 FROM cm_holiday + WHERE holi_date BETWEEN :hz_bizdate AND '2026-12-31'; + agg = (sqlca.sqlcode == 0) ? hz_v1 : 0; /* 연말까지 휴일수 */ + if (agg > 1261L) + userlog("CM_WEEKDAY 집계 임계 agg=%ld", agg); + + /* 7) 예외/보상부 - 집계 재조회 실패시 전체 롤백. */ + if (sqlca.sqlcode < 0) { + userlog("CM_WEEKDAY 집계 조회 오류 sqlcode=%d", sqlca.sqlcode); + Bchg(b, T_MSG, 0, "집계 재조회 오류", 0L); + FAIL(b); + } + + /* 8) 응답부 - 결과 필드 채움. */ + setl(b, T_ARG8, aud_id); + setl(b, T_ARG7, agg); + setl(b, T_ARG6, grade); + + userlog("CM_WEEKDAY 완료: aud_id=%ld grade=%ld agg=%ld", aud_id, grade, agg); OK(b); } diff --git a/app/src/lg/svc/LG_ACCRUEDPAY.pgc b/app/src/lg/svc/LG_ACCRUEDPAY.pgc index e702132..9268e59 100644 --- a/app/src/lg/svc/LG_ACCRUEDPAY.pgc +++ b/app/src/lg/svc/LG_ACCRUEDPAY.pgc @@ -1,7 +1,9 @@ /* - * LG_ACCRUEDPAY.pgc - lg 모듈 서비스 LG_ACCRUEDPAY (one service = one file). + * LG_ACCRUEDPAY.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_ACCRUEDPAY. * 미지급비용 계상: 미지급수수료비용(D)/미지급금(C)을 균형 계상. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_ACCRUEDPAY.h" @@ -10,6 +12,85 @@ void LG_ACCRUEDPAY(TPSVCINFO *p) UBFH *b = (UBFH *)p->data; char bizdate[16]; long vid, amount; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_ACCRUEDPAY"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "대상액 음수 불가", 0L); + userlog("LG_ACCRUEDPAY 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_ACCRUEDPAY 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_ACCRUEDPAY 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_ACCRUEDPAY 라인집계 조회 경고 [%d]", sqlca.sqlcode); + if (gx_mid[0]) { + EXEC SQL SELECT coalesce(mdr_bps,0), coalesce(daily_limit,0), coalesce(status,'?') + INTO :gx_mbps, :gx_lim, :gx_mst FROM merchant WHERE merchant_id = :gx_mid; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_ACCRUEDPAY 가맹점 미등록 mid=%s (정책 기본값)", gx_mid); + } + EXEC SQL SELECT coalesce(balance,0) INTO :gx_bal + FROM lg_merch_bal WHERE merchant_id = :gx_mid AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_bal = 0; + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(CASH_RECOV/RETAINED) */ + userlog("LG_ACCRUEDPAY 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "CLOSED") == 0) { + userlog("LG_ACCRUEDPAY 해지가맹점 mid=%s - 잔액정리 경로", gx_mid); + } else if (gx_mbps > 30) { + gx_rc = 1; /* 고 MDR 가맹점 - 수수료 배분 재검토 */ + } + if (gx_lim > 0 && gx_amt > gx_lim) { + userlog("LG_ACCRUEDPAY 일한도 초과소지 amt=%ld limit=%ld", gx_amt, gx_lim); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_ACCRUEDPAY 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); amount = getl(b, T_AMOUNT); if (amount <= 0) { Bchg(b, T_MSG, 0, "계상액 없음", 0L); FAIL(b); } @@ -20,6 +101,36 @@ void LG_ACCRUEDPAY(TPSVCINFO *p) lgdb_insert_line(vid, "ACCRUED_PAY", "C", amount, "", bizdate, "posted") < 0) FAIL(b); setl(b, T_ID1, vid); userlog("LG_ACCRUEDPAY vid=%ld 미지급 %ld", vid, amount); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_ACCRUEDPAY band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_ACCRUEDPAY 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + if (gx_mid[0]) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0) + INTO :gx_dr, :gx_cr FROM lg_ledger_line + WHERE merchant_id = :gx_mid AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_bal = gx_dr - gx_cr; + } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_ACCRUEDPAY 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_ACCRUEDRECV.pgc b/app/src/lg/svc/LG_ACCRUEDRECV.pgc index abc29d1..7162b4d 100644 --- a/app/src/lg/svc/LG_ACCRUEDRECV.pgc +++ b/app/src/lg/svc/LG_ACCRUEDRECV.pgc @@ -1,7 +1,9 @@ /* - * LG_ACCRUEDRECV.pgc - lg 모듈 서비스 LG_ACCRUEDRECV (one service = one file). + * LG_ACCRUEDRECV.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_ACCRUEDRECV. * 미수수익 계상: 현재 RECV 잔액을 참조해 미수수익(D)/이자수익(C)을 계상. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_ACCRUEDRECV.h" @@ -14,6 +16,74 @@ void LG_ACCRUEDRECV(TPSVCINFO *p) char h_bizdate[16]; long h_bal; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_ACCRUEDRECV"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_ACCRUEDRECV 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_ACCRUEDRECV 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_ACCRUEDRECV 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_ACCRUEDRECV 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(BADDEBT_EXP/UNEARNED_REV) */ + userlog("LG_ACCRUEDRECV 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_ACCRUEDRECV 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_ACCRUEDRECV 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); rate = getl(b, T_COUNT); if (rate <= 0) rate = 50; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; @@ -29,6 +99,32 @@ void LG_ACCRUEDRECV(TPSVCINFO *p) lgdb_insert_line(vid, "INTEREST_INC", "C", accr, "", bizdate, "posted") < 0) FAIL(b); setl(b, T_ID1, vid); setl(b, T_NET, accr); userlog("LG_ACCRUEDRECV vid=%ld base %ld @%ldbp = %ld", vid, base, rate, accr); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_ACCRUEDRECV band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_ACCRUEDRECV 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_ACCRUEDRECV 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_ACCRUEREV.pgc b/app/src/lg/svc/LG_ACCRUEREV.pgc index 16322eb..0d97c0e 100644 --- a/app/src/lg/svc/LG_ACCRUEREV.pgc +++ b/app/src/lg/svc/LG_ACCRUEREV.pgc @@ -1,8 +1,10 @@ /* - * LG_ACCRUEREV.pgc - lg 모듈 서비스 LG_ACCRUEREV (one service = one file). + * LG_ACCRUEREV.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_ACCRUEREV. * 계상 환입 분개: 기준일(T_BIZDATE) 이전의 미결 '계상' 전표를 커서로 찾아 * 각각 차/대변 반전 환입 전표를 만들고 원전표를 X 처리한다 (자동 환입). - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_ACCRUEREV.h" @@ -15,6 +17,74 @@ void LG_ACCRUEREV(TPSVCINFO *p) char h_bizdate[16]; long h_vid, h_nvid; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_ACCRUEREV"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_ACCRUEREV 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_ACCRUEREV 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_ACCRUEREV 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_ACCRUEREV 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(CASH_RECOV/SUSP) */ + userlog("LG_ACCRUEREV 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_ACCRUEREV 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_ACCRUEREV 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; @@ -50,6 +120,32 @@ void LG_ACCRUEREV(TPSVCINFO *p) setl(b, T_COUNT, reversed); setl(b, T_ID2, lastnew); userlog("LG_ACCRUEREV 기준 %s 이전 계상 %ld건 환입", bizdate, reversed); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_ACCRUEREV band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_ACCRUEREV 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_ACCRUEREV 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_ACCTGROUP.pgc b/app/src/lg/svc/LG_ACCTGROUP.pgc index f59f63d..8a837ba 100644 --- a/app/src/lg/svc/LG_ACCTGROUP.pgc +++ b/app/src/lg/svc/LG_ACCTGROUP.pgc @@ -1,7 +1,9 @@ /* - * LG_ACCTGROUP.pgc - lg 모듈 서비스 LG_ACCTGROUP (one service = one file). + * LG_ACCTGROUP.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_ACCTGROUP. * 계정그룹별 집계: 계정과목 접두어(prefix)로 묶어 그룹별 차대 합계를 순회. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_ACCTGROUP.h" @@ -14,6 +16,79 @@ void LG_ACCTGROUP(TPSVCINFO *p) char h_bizdate[16], h_grp[32]; long h_dr, h_cr; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_ACCTGROUP"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "분개액 음수 불가", 0L); + userlog("LG_ACCTGROUP 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_ACCTGROUP 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_ACCTGROUP 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_ACCTGROUP 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(debit_total,0), coalesce(credit_total,0), coalesce(balance,0) + INTO :gx_prev, :gx_amt, :gx_bal FROM lg_account_bal + WHERE acct = 'RECV' AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_prev = 0; gx_amt = 0; gx_bal = 0; } + EXEC SQL SELECT coalesce(count(DISTINCT acct),0) INTO :gx_max + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_max = 0; + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(RECV/SUSP) */ + userlog("LG_ACCTGROUP 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + gx_diff = gx_dr - gx_cr; + if (gx_diff != 0) { + userlog("LG_ACCTGROUP 당일 대차 불균형 diff=%ld", gx_diff); + } else if (gx_max == 0) { + gx_rc = 3; /* 무거래일 */ + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_ACCTGROUP 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; EXEC SQL DECLARE ag_cur CURSOR FOR @@ -34,6 +109,32 @@ void LG_ACCTGROUP(TPSVCINFO *p) setl(b, T_COUNT, groups); setl(b, T_AMT2, tot_dr); setl(b, T_AMT3, tot_cr); setl(b, T_DELTA, tot_dr - tot_cr); userlog("LG_ACCTGROUP %s 그룹 %ld개 차변합 %ld 대변합 %ld", bizdate, groups, tot_dr, tot_cr); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_ACCTGROUP band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_ACCTGROUP 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(debit_total),0), coalesce(sum(credit_total),0) + INTO :gx_dr, :gx_cr FROM lg_account_bal WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_diff = gx_dr - gx_cr; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_ACCTGROUP 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_ACCTHIST.pgc b/app/src/lg/svc/LG_ACCTHIST.pgc index e82aed2..04ddf46 100644 --- a/app/src/lg/svc/LG_ACCTHIST.pgc +++ b/app/src/lg/svc/LG_ACCTHIST.pgc @@ -1,8 +1,10 @@ /* - * LG_ACCTHIST.pgc - lg 모듈 서비스 LG_ACCTHIST (one service = one file). + * LG_ACCTHIST.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_ACCTHIST. * 계정 잔액 이력 조회: [T_BIZDATE ~ T_STR1] 구간의 계정잔액(lg_account_bal) * 행을 커서로 훑어 행수·최종 잔액·최소/최대 잔액을 반환한다 (추이 분석용). - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_ACCTHIST.h" @@ -15,6 +17,79 @@ void LG_ACCTHIST(TPSVCINFO *p) char h_acct[32], h_from[16], h_to[16]; long h_bal; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_ACCTHIST"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "금액 음수 불가", 0L); + userlog("LG_ACCTHIST 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_ACCTHIST 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_ACCTHIST 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_ACCTHIST 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(debit_total,0), coalesce(credit_total,0), coalesce(balance,0) + INTO :gx_prev, :gx_amt, :gx_bal FROM lg_account_bal + WHERE acct = 'RECV' AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_prev = 0; gx_amt = 0; gx_bal = 0; } + EXEC SQL SELECT coalesce(count(DISTINCT acct),0) INTO :gx_max + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_max = 0; + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(FEE_INC/ADJ) */ + userlog("LG_ACCTHIST 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + gx_diff = gx_dr - gx_cr; + if (gx_diff != 0) { + userlog("LG_ACCTHIST 당일 대차 불균형 diff=%ld", gx_diff); + } else if (gx_max == 0) { + gx_rc = 3; /* 무거래일 */ + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_ACCTHIST 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_KEY1, acct, sizeof(acct)); if (acct[0] == 0) strcpy(acct, "RECV"); gets_(b, T_BIZDATE, dfrom, sizeof(dfrom)); @@ -44,6 +119,32 @@ void LG_ACCTHIST(TPSVCINFO *p) setl(b, T_COUNT, rows); setl(b, T_AMT2, last); setl(b, T_AMT3, lo); setl(b, T_AMT4, hi); userlog("LG_ACCTHIST %s [%s~%s] %ld일 최종 %ld (min %ld / max %ld)", acct, dfrom, dto, rows, last, lo, hi); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_ACCTHIST band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_ACCTHIST 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(debit_total),0), coalesce(sum(credit_total),0) + INTO :gx_dr, :gx_cr FROM lg_account_bal WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_diff = gx_dr - gx_cr; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_ACCTHIST 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_ACCTLEDGER.pgc b/app/src/lg/svc/LG_ACCTLEDGER.pgc index 7646c42..a29d3da 100644 --- a/app/src/lg/svc/LG_ACCTLEDGER.pgc +++ b/app/src/lg/svc/LG_ACCTLEDGER.pgc @@ -1,8 +1,10 @@ /* - * LG_ACCTLEDGER.pgc - lg 모듈 서비스 LG_ACCTLEDGER (one service = one file). + * LG_ACCTLEDGER.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_ACCTLEDGER. * 계정과목별 원장조회: 지정 계정/영업일의 라인을 커서로 훑어 차변/대변 * 건수·합계와 최종 라인 ID 를 돌려준다 (복식부기: 차변-대변 = 잔액). - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_ACCTLEDGER.h" @@ -15,6 +17,82 @@ void LG_ACCTLEDGER(TPSVCINFO *p) char h_acct[32], h_bizdate[16], h_dc[4]; long h_line, h_amt; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_ACCTLEDGER"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "분개액 음수 불가", 0L); + userlog("LG_ACCTLEDGER 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_ACCTLEDGER 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_ACCTLEDGER 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_ACCTLEDGER 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(debit_total,0), coalesce(credit_total,0), coalesce(balance,0) + INTO :gx_prev, :gx_amt, :gx_bal FROM lg_account_bal + WHERE acct = 'RECV' AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_prev = 0; gx_amt = 0; gx_bal = 0; } + EXEC SQL SELECT coalesce(count(DISTINCT acct),0) INTO :gx_max + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_max = 0; + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(FEE_INC/RECV) */ + userlog("LG_ACCTLEDGER 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + gx_diff = gx_dr - gx_cr; + if (gx_diff != 0) { + userlog("LG_ACCTLEDGER 당일 대차 불균형 diff=%ld", gx_diff); + } else if (gx_max == 0) { + gx_rc = 3; /* 무거래일 */ + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_ACCTLEDGER 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_KEY1, acct, sizeof(acct)); if (acct[0] == 0) strcpy(acct, "RECV"); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); @@ -41,6 +119,32 @@ void LG_ACCTLEDGER(TPSVCINFO *p) setl(b, T_AMT2, dsum); setl(b, T_AMT3, csum); setl(b, T_DELTA, dsum - csum); setl(b, T_ID1, lastid); userlog("LG_ACCTLEDGER %s %s 차변 %ld건/%ld 대변 %ld건/%ld", acct, bizdate, dcnt, dsum, ccnt, csum); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_ACCTLEDGER band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_ACCTLEDGER 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(debit_total),0), coalesce(sum(credit_total),0) + INTO :gx_dr, :gx_cr FROM lg_account_bal WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_diff = gx_dr - gx_cr; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_ACCTLEDGER 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_ACCTPIVOT.pgc b/app/src/lg/svc/LG_ACCTPIVOT.pgc index 4cd51fe..0505661 100644 --- a/app/src/lg/svc/LG_ACCTPIVOT.pgc +++ b/app/src/lg/svc/LG_ACCTPIVOT.pgc @@ -1,7 +1,9 @@ /* - * LG_ACCTPIVOT.pgc - lg 모듈 서비스 LG_ACCTPIVOT (one service = one file). + * LG_ACCTPIVOT.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_ACCTPIVOT. * 계정x일자 피벗 잔액: 기간 내 일자별 순액을 누적해 누적잔액과 최고점을 산출. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_ACCTPIVOT.h" @@ -14,6 +16,82 @@ void LG_ACCTPIVOT(TPSVCINFO *p) char h_acct[32], h_from[16], h_end[16], h_day[16]; long h_net; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_ACCTPIVOT"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "분개액 음수 불가", 0L); + userlog("LG_ACCTPIVOT 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_ACCTPIVOT 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_ACCTPIVOT 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_ACCTPIVOT 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(debit_total,0), coalesce(credit_total,0), coalesce(balance,0) + INTO :gx_prev, :gx_amt, :gx_bal FROM lg_account_bal + WHERE acct = 'RECV' AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_prev = 0; gx_amt = 0; gx_bal = 0; } + EXEC SQL SELECT coalesce(count(DISTINCT acct),0) INTO :gx_max + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_max = 0; + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(MDR_EXP/CASH_RECOV) */ + userlog("LG_ACCTPIVOT 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + gx_diff = gx_dr - gx_cr; + if (gx_diff != 0) { + userlog("LG_ACCTPIVOT 당일 대차 불균형 diff=%ld", gx_diff); + } else if (gx_max == 0) { + gx_rc = 3; /* 무거래일 */ + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_ACCTPIVOT 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_KEY1, acct, sizeof(acct)); if (acct[0] == 0) strcpy(acct, "RECV"); gets_(b, T_BIZDATE, endd, sizeof(endd)); gets_(b, T_STR1, fromd, sizeof(fromd)); if (fromd[0] == 0) strcpy(fromd, endd); @@ -39,6 +117,32 @@ void LG_ACCTPIVOT(TPSVCINFO *p) EXEC SQL CLOSE ap_cur; setl(b, T_COUNT, days); setl(b, T_NET, running); setl(b, T_AMT2, peak); userlog("LG_ACCTPIVOT %s %s~%s %ld일 잔액 %ld 최고 %ld", acct, fromd, endd, days, running, peak); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_ACCTPIVOT band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_ACCTPIVOT 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(debit_total),0), coalesce(sum(credit_total),0) + INTO :gx_dr, :gx_cr FROM lg_account_bal WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_diff = gx_dr - gx_cr; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_ACCTPIVOT 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_ACCTRANGE.pgc b/app/src/lg/svc/LG_ACCTRANGE.pgc index ac9d3e1..ea4bdcd 100644 --- a/app/src/lg/svc/LG_ACCTRANGE.pgc +++ b/app/src/lg/svc/LG_ACCTRANGE.pgc @@ -1,8 +1,10 @@ /* - * LG_ACCTRANGE.pgc - lg 모듈 서비스 LG_ACCTRANGE (one service = one file). + * LG_ACCTRANGE.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_ACCTRANGE. * 계정과목 기간별 원장집계: [T_BIZDATE ~ T_STR1] 구간의 차변/대변 합계와 * 라인 건수를 단문 집계로 산출하고 잔액(차변-대변)을 반환한다. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_ACCTRANGE.h" @@ -14,6 +16,79 @@ void LG_ACCTRANGE(TPSVCINFO *p) char h_acct[32], h_from[16], h_to[16]; long h_dr, h_cr, h_cnt; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_ACCTRANGE"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_ACCTRANGE 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_ACCTRANGE 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_ACCTRANGE 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_ACCTRANGE 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(debit_total,0), coalesce(credit_total,0), coalesce(balance,0) + INTO :gx_prev, :gx_amt, :gx_bal FROM lg_account_bal + WHERE acct = 'RECV' AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_prev = 0; gx_amt = 0; gx_bal = 0; } + EXEC SQL SELECT coalesce(count(DISTINCT acct),0) INTO :gx_max + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_max = 0; + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(ACCRUED_PAY/MERCH_PAY) */ + userlog("LG_ACCTRANGE 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + gx_diff = gx_dr - gx_cr; + if (gx_diff != 0) { + userlog("LG_ACCTRANGE 당일 대차 불균형 diff=%ld", gx_diff); + } else if (gx_max == 0) { + gx_rc = 3; /* 무거래일 */ + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_ACCTRANGE 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_KEY1, acct, sizeof(acct)); if (acct[0] == 0) strcpy(acct, "RECV"); gets_(b, T_BIZDATE, dfrom, sizeof(dfrom)); @@ -32,6 +107,32 @@ void LG_ACCTRANGE(TPSVCINFO *p) setl(b, T_AMT2, h_dr); setl(b, T_AMT3, h_cr); setl(b, T_COUNT, h_cnt); setl(b, T_DELTA, h_dr - h_cr); userlog("LG_ACCTRANGE %s [%s~%s] 잔액=%ld", acct, dfrom, dto, h_dr - h_cr); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_ACCTRANGE band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_ACCTRANGE 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(debit_total),0), coalesce(sum(credit_total),0) + INTO :gx_dr, :gx_cr FROM lg_account_bal WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_diff = gx_dr - gx_cr; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_ACCTRANGE 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_ACCTSUM.pgc b/app/src/lg/svc/LG_ACCTSUM.pgc index 1c54fb2..696cea9 100644 --- a/app/src/lg/svc/LG_ACCTSUM.pgc +++ b/app/src/lg/svc/LG_ACCTSUM.pgc @@ -1,8 +1,9 @@ /* - * LG_ACCTSUM.pgc - lg 모듈 서비스 LG_ACCTSUM (one service = one file). + * LG_ACCTSUM.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_ACCTSUM. * 계정 잔액 재집계(dbio) 후 잔액 반환. - * 이전 인라인 lg_svr.pgc 에서 분리; 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). - * lg_svr 가 tpsvrinit 에서 tpadvertise 로 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_ACCTSUM.h" @@ -11,11 +12,113 @@ void LG_ACCTSUM(TPSVCINFO *p) UBFH *b = (UBFH *)p->data; char acct[32], bizdate[16]; long bal = 0; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_ACCTSUM"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "대상액 음수 불가", 0L); + userlog("LG_ACCTSUM 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_ACCTSUM 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_ACCTSUM 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_ACCTSUM 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(debit_total,0), coalesce(credit_total,0), coalesce(balance,0) + INTO :gx_prev, :gx_amt, :gx_bal FROM lg_account_bal + WHERE acct = 'RECV' AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_prev = 0; gx_amt = 0; gx_bal = 0; } + EXEC SQL SELECT coalesce(count(DISTINCT acct),0) INTO :gx_max + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_max = 0; + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(ACCRUED_PAY/ALLOWANCE) */ + userlog("LG_ACCTSUM 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + gx_diff = gx_dr - gx_cr; + if (gx_diff != 0) { + userlog("LG_ACCTSUM 당일 대차 불균형 diff=%ld", gx_diff); + } else if (gx_max == 0) { + gx_rc = 3; /* 무거래일 */ + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_ACCTSUM 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_KEY1, acct, sizeof(acct)); if (acct[0] == 0) strcpy(acct, "RECV"); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); if (lgdb_recalc_acct_bal(acct, bizdate) < 0) FAIL(b); if (lgdb_get_acct_bal(acct, bizdate, &bal) < 0) FAIL(b); setl(b, T_NET, bal); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_ACCTSUM band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_ACCTSUM 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(debit_total),0), coalesce(sum(credit_total),0) + INTO :gx_dr, :gx_cr FROM lg_account_bal WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_diff = gx_dr - gx_cr; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_ACCTSUM 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_ACCTTOPN.pgc b/app/src/lg/svc/LG_ACCTTOPN.pgc index 3246573..e0263ff 100644 --- a/app/src/lg/svc/LG_ACCTTOPN.pgc +++ b/app/src/lg/svc/LG_ACCTTOPN.pgc @@ -1,7 +1,9 @@ /* - * LG_ACCTTOPN.pgc - lg 모듈 서비스 LG_ACCTTOPN (one service = one file). + * LG_ACCTTOPN.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_ACCTTOPN. * 상위 N 계정 잔액: 계정별 순액 절대값 내림차순으로 상위 N개를 훑어 1위를 반환. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_ACCTTOPN.h" @@ -14,6 +16,79 @@ void LG_ACCTTOPN(TPSVCINFO *p) char h_bizdate[16], h_acct[32]; long h_net; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_ACCTTOPN"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "대상액 음수 불가", 0L); + userlog("LG_ACCTTOPN 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_ACCTTOPN 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_ACCTTOPN 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_ACCTTOPN 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(debit_total,0), coalesce(credit_total,0), coalesce(balance,0) + INTO :gx_prev, :gx_amt, :gx_bal FROM lg_account_bal + WHERE acct = 'RECV' AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_prev = 0; gx_amt = 0; gx_bal = 0; } + EXEC SQL SELECT coalesce(count(DISTINCT acct),0) INTO :gx_max + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_max = 0; + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(PREPAID/BADDEBT_RCV) */ + userlog("LG_ACCTTOPN 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + gx_diff = gx_dr - gx_cr; + if (gx_diff != 0) { + userlog("LG_ACCTTOPN 당일 대차 불균형 diff=%ld", gx_diff); + } else if (gx_max == 0) { + gx_rc = 3; /* 무거래일 */ + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_ACCTTOPN 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); n = getl(b, T_COUNT); if (n <= 0) n = 5; gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; @@ -36,6 +111,32 @@ void LG_ACCTTOPN(TPSVCINFO *p) Bchg(b, T_KEY1, 0, topacct, 0L); setl(b, T_COUNT, seen); setl(b, T_NET, topval); userlog("LG_ACCTTOPN %s 상위 %ld개 1위 %s %ld", bizdate, seen, topacct, topval); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_ACCTTOPN band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_ACCTTOPN 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(debit_total),0), coalesce(sum(credit_total),0) + INTO :gx_dr, :gx_cr FROM lg_account_bal WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_diff = gx_dr - gx_cr; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_ACCTTOPN 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_ACQPOST.pgc b/app/src/lg/svc/LG_ACQPOST.pgc index 14b730d..c498221 100644 --- a/app/src/lg/svc/LG_ACQPOST.pgc +++ b/app/src/lg/svc/LG_ACQPOST.pgc @@ -1,8 +1,9 @@ /* - * LG_ACQPOST.pgc - lg 모듈 서비스 LG_ACQPOST (one service = one file). + * LG_ACQPOST.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_ACQPOST. * 매입 전기: 차변 RECV=amount, 대변 MERCH_PAY=net + FEE_INC=fee. - * 이전 인라인 lg_svr.pgc 에서 분리; 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). - * lg_svr 가 tpsvrinit 에서 tpadvertise 로 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_ACQPOST.h" @@ -11,6 +12,84 @@ void LG_ACQPOST(TPSVCINFO *p) UBFH *b = (UBFH *)p->data; char bizdate[16], merch[64]; long vid, amount, fee, net; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_ACQPOST"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "금액 음수 불가", 0L); + userlog("LG_ACQPOST 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_ACQPOST 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_ACQPOST 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_ACQPOST 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(*),0), coalesce(max(voucher_id),0) + INTO :gx_cnt, :gx_max FROM lg_voucher WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + if (gx_in_id > 0) { + EXEC SQL SELECT coalesce(status,'?'), coalesce(ref_id,0) + INTO :gx_mst, :gx_prev FROM lg_voucher WHERE voucher_id = :gx_in_id; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_ACQPOST 전표 미존재 vid=%ld", gx_in_id); + } + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(FEE_INC/ADJ) */ + userlog("LG_ACQPOST 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "S") == 0) { + userlog("LG_ACQPOST 마감연계(S) 전표 - 정정은 역분개 권고", gx_svc); + } else if (gx_mst[0] && strcmp(gx_mst, "X") == 0) { + gx_rc = 2; /* 취소 전표 - 재개 필요 */ + } else if (gx_dr != gx_cr) { + gx_diff = gx_dr - gx_cr; + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_ACQPOST 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); gets_(b, T_MERCHANT, merch, sizeof(merch)); amount = getl(b, T_AMOUNT); @@ -25,6 +104,31 @@ void LG_ACQPOST(TPSVCINFO *p) if (lgdb_insert_line(vid, "FEE_INC", "C", fee, merch, bizdate, "posted") < 0) FAIL(b); setl(b, T_ID1, vid); userlog("LG_ACQPOST vid=%ld amount=%ld net=%ld fee=%ld", vid, amount, net, fee); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_ACQPOST band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_ACQPOST 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0) INTO :gx_cnt + FROM lg_voucher WHERE biz_date = :gx_bd AND status = 'P'; + if (sqlca.sqlcode == 100) gx_cnt = 0; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_ACQPOST 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_ADJUST.pgc b/app/src/lg/svc/LG_ADJUST.pgc index eb06371..999a83b 100644 --- a/app/src/lg/svc/LG_ADJUST.pgc +++ b/app/src/lg/svc/LG_ADJUST.pgc @@ -1,8 +1,9 @@ /* - * LG_ADJUST.pgc - lg 모듈 서비스 LG_ADJUST (one service = one file). + * LG_ADJUST.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_ADJUST. * 조정 전표: 부호에 따라 차변/대변 단일 분개. - * 이전 인라인 lg_svr.pgc 에서 분리; 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). - * lg_svr 가 tpsvrinit 에서 tpadvertise 로 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_ADJUST.h" @@ -12,6 +13,74 @@ void LG_ADJUST(TPSVCINFO *p) char acct[32], bizdate[16], merch[64]; long vid, delta, amt; const char *dc; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_ADJUST"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_ADJUST 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_ADJUST 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_ADJUST 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_ADJUST 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(BADDEBT_EXP/BADDEBT_RCV) */ + userlog("LG_ADJUST 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_ADJUST 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_ADJUST 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_KEY1, acct, sizeof(acct)); if (acct[0] == 0) strcpy(acct, "ADJ"); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); gets_(b, T_MERCHANT, merch, sizeof(merch)); @@ -23,6 +92,32 @@ void LG_ADJUST(TPSVCINFO *p) if (lgdb_insert_voucher(vid, "ADJ", getl(b, T_PURCHASE_ID), "P", bizdate, "조정분개") < 0) FAIL(b); if (lgdb_insert_line(vid, acct, dc, amt, merch, bizdate, "posted") < 0) FAIL(b); setl(b, T_ID1, vid); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_ADJUST band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_ADJUST 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_ADJUST 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_ALLOWANCE.pgc b/app/src/lg/svc/LG_ALLOWANCE.pgc index 1ddad8d..b9ac9db 100644 --- a/app/src/lg/svc/LG_ALLOWANCE.pgc +++ b/app/src/lg/svc/LG_ALLOWANCE.pgc @@ -1,7 +1,9 @@ /* - * LG_ALLOWANCE.pgc - lg 모듈 서비스 LG_ALLOWANCE (one service = one file). + * LG_ALLOWANCE.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_ALLOWANCE. * 대손충당금 설정: RECV 잔액의 일정률을 대손상각비(D)/대손충당금(C)으로 설정. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_ALLOWANCE.h" @@ -14,6 +16,74 @@ void LG_ALLOWANCE(TPSVCINFO *p) char h_bizdate[16]; long h_bal; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_ALLOWANCE"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "분개액 음수 불가", 0L); + userlog("LG_ALLOWANCE 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_ALLOWANCE 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_ALLOWANCE 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_ALLOWANCE 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(CASH/RECV) */ + userlog("LG_ALLOWANCE 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_ALLOWANCE 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_ALLOWANCE 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); rate = getl(b, T_COUNT); if (rate <= 0) rate = 100; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; @@ -29,6 +99,32 @@ void LG_ALLOWANCE(TPSVCINFO *p) lgdb_insert_line(vid, "BADDEBT_ALLOW", "C", allow, "", bizdate, "posted") < 0) FAIL(b); setl(b, T_ID1, vid); setl(b, T_NET, allow); setl(b, T_AMT2, base); userlog("LG_ALLOWANCE vid=%ld RECV %ld @%ldbp = %ld", vid, base, rate, allow); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_ALLOWANCE band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_ALLOWANCE 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_ALLOWANCE 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_ALLOWRECALC.pgc b/app/src/lg/svc/LG_ALLOWRECALC.pgc index 72d8a11..f493dd6 100644 --- a/app/src/lg/svc/LG_ALLOWRECALC.pgc +++ b/app/src/lg/svc/LG_ALLOWRECALC.pgc @@ -1,7 +1,9 @@ /* - * LG_ALLOWRECALC.pgc - lg 모듈 서비스 LG_ALLOWRECALC (one service = one file). + * LG_ALLOWRECALC.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_ALLOWRECALC. * 충당금 재산정: 필요 충당금(RECV x 율)과 현재 충당금 잔액 차이를 조정 전기. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_ALLOWRECALC.h" @@ -14,6 +16,74 @@ void LG_ALLOWRECALC(TPSVCINFO *p) char h_bizdate[16]; long h_recv, h_allow; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_ALLOWRECALC"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "대상액 음수 불가", 0L); + userlog("LG_ALLOWRECALC 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_ALLOWRECALC 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_ALLOWRECALC 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_ALLOWRECALC 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(BADDEBT_RCV/SUSP) */ + userlog("LG_ALLOWRECALC 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_ALLOWRECALC 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_ALLOWRECALC 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); rate = getl(b, T_COUNT); if (rate <= 0) rate = 100; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; @@ -39,6 +109,32 @@ void LG_ALLOWRECALC(TPSVCINFO *p) } setl(b, T_ID1, vid); setl(b, T_DELTA, delta); setl(b, T_AMT2, required); userlog("LG_ALLOWRECALC vid=%ld 필요 %ld 현재 %ld 조정 %ld", vid, required, current, delta); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_ALLOWRECALC band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_ALLOWRECALC 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_ALLOWRECALC 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_APPLY.pgc b/app/src/lg/svc/LG_APPLY.pgc index a869801..a174b6b 100644 --- a/app/src/lg/svc/LG_APPLY.pgc +++ b/app/src/lg/svc/LG_APPLY.pgc @@ -1,8 +1,9 @@ /* - * LG_APPLY.pgc - lg 모듈 서비스 LG_APPLY (one service = one file). + * LG_APPLY.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_APPLY. * 전표 전기확정(P): 라인들의 계정잔액을 잔액테이블에 반영. - * 이전 인라인 lg_svr.pgc 에서 분리; 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). - * lg_svr 가 tpsvrinit 에서 tpadvertise 로 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_APPLY.h" @@ -14,6 +15,74 @@ void LG_APPLY(TPSVCINFO *p) long h_vid; char h_bizdate[16]; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_APPLY"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "금액 음수 불가", 0L); + userlog("LG_APPLY 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_APPLY 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_APPLY 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_APPLY 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(ADJ/BADDEBT_EXP) */ + userlog("LG_APPLY 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_APPLY 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_APPLY 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); h_vid = getl(b, T_ID1); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; @@ -33,6 +102,32 @@ void LG_APPLY(TPSVCINFO *p) if (sqlca.sqlcode < 0) FAIL(b); if (lgdb_update_voucher_status(h_vid, "P") < 0) FAIL(b); Bchg(b, T_STATUS, 0, "P", 0L); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_APPLY band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_APPLY 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_APPLY 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_APPRDELEG.pgc b/app/src/lg/svc/LG_APPRDELEG.pgc index 045003f..4c875e7 100644 --- a/app/src/lg/svc/LG_APPRDELEG.pgc +++ b/app/src/lg/svc/LG_APPRDELEG.pgc @@ -1,7 +1,9 @@ /* - * LG_APPRDELEG.pgc - lg 모듈 서비스 LG_APPRDELEG (one service = one file). + * LG_APPRDELEG.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_APPRDELEG. * 승인 위임: 작성(D) 전표의 승인 권한을 대리인에게 위임(메모 누적). - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_APPRDELEG.h" @@ -14,6 +16,87 @@ void LG_APPRDELEG(TPSVCINFO *p) long h_vid; char h_deleg[64]; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_APPRDELEG"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "대상액 음수 불가", 0L); + userlog("LG_APPRDELEG 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_APPRDELEG 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_APPRDELEG 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_APPRDELEG 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(*),0), coalesce(max(voucher_id),0) + INTO :gx_cnt, :gx_max FROM lg_voucher WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + if (gx_in_id > 0) { + EXEC SQL SELECT coalesce(status,'?'), coalesce(ref_id,0) + INTO :gx_mst, :gx_prev FROM lg_voucher WHERE voucher_id = :gx_in_id; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_APPRDELEG 전표 미존재 vid=%ld", gx_in_id); + } + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(BADDEBT_EXP/RECV_ACCR) */ + userlog("LG_APPRDELEG 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "S") == 0) { + userlog("LG_APPRDELEG 마감연계(S) 전표 - 정정은 역분개 권고", gx_svc); + } else if (gx_mst[0] && strcmp(gx_mst, "X") == 0) { + gx_rc = 2; /* 취소 전표 - 재개 필요 */ + } else if (gx_dr != gx_cr) { + gx_diff = gx_dr - gx_cr; + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_APPRDELEG 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); vid = getl(b, T_ID1); gets_(b, T_STR1, deleg, sizeof(deleg)); if (deleg[0] == 0) strcpy(deleg, "대리인"); if (lgdb_get_voucher_status(vid, st) < 0) { Bchg(b, T_MSG, 0, "전표 없음", 0L); FAIL(b); } @@ -25,6 +108,31 @@ void LG_APPRDELEG(TPSVCINFO *p) if (sqlca.sqlcode < 0) FAIL(b); setl(b, T_ID1, vid); userlog("LG_APPRDELEG vid=%ld -> %s 위임", vid, deleg); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_APPRDELEG band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_APPRDELEG 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0) INTO :gx_cnt + FROM lg_voucher WHERE biz_date = :gx_bd AND status = 'P'; + if (sqlca.sqlcode == 100) gx_cnt = 0; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_APPRDELEG 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_APPRHIST.pgc b/app/src/lg/svc/LG_APPRHIST.pgc index ae5f5fc..52d79c9 100644 --- a/app/src/lg/svc/LG_APPRHIST.pgc +++ b/app/src/lg/svc/LG_APPRHIST.pgc @@ -1,7 +1,9 @@ /* - * LG_APPRHIST.pgc - lg 모듈 서비스 LG_APPRHIST (one service = one file). + * LG_APPRHIST.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_APPRHIST. * 승인 이력 조회: 당일 전기(P) 완료된 전표 수와 차변 총액을 집계. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_APPRHIST.h" @@ -13,6 +15,84 @@ void LG_APPRHIST(TPSVCINFO *p) char h_bizdate[16]; long h_cnt, h_amt; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_APPRHIST"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "금액 음수 불가", 0L); + userlog("LG_APPRHIST 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_APPRHIST 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_APPRHIST 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_APPRHIST 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(*),0), coalesce(max(voucher_id),0) + INTO :gx_cnt, :gx_max FROM lg_voucher WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + if (gx_in_id > 0) { + EXEC SQL SELECT coalesce(status,'?'), coalesce(ref_id,0) + INTO :gx_mst, :gx_prev FROM lg_voucher WHERE voucher_id = :gx_in_id; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_APPRHIST 전표 미존재 vid=%ld", gx_in_id); + } + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(RECV_ACCR/UNEARNED_REV) */ + userlog("LG_APPRHIST 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "S") == 0) { + userlog("LG_APPRHIST 마감연계(S) 전표 - 정정은 역분개 권고", gx_svc); + } else if (gx_mst[0] && strcmp(gx_mst, "X") == 0) { + gx_rc = 2; /* 취소 전표 - 재개 필요 */ + } else if (gx_dr != gx_cr) { + gx_diff = gx_dr - gx_cr; + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_APPRHIST 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; EXEC SQL SELECT count(DISTINCT v.voucher_id), @@ -23,6 +103,31 @@ void LG_APPRHIST(TPSVCINFO *p) if (sqlca.sqlcode < 0) FAIL(b); setl(b, T_COUNT, h_cnt); setl(b, T_AMT2, h_amt); userlog("LG_APPRHIST %s 승인전표 %ld건 차변 %ld", bizdate, h_cnt, h_amt); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_APPRHIST band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_APPRHIST 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0) INTO :gx_cnt + FROM lg_voucher WHERE biz_date = :gx_bd AND status = 'P'; + if (sqlca.sqlcode == 100) gx_cnt = 0; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_APPRHIST 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_APPROVE.pgc b/app/src/lg/svc/LG_APPROVE.pgc index c47eb75..3e28061 100644 --- a/app/src/lg/svc/LG_APPROVE.pgc +++ b/app/src/lg/svc/LG_APPROVE.pgc @@ -1,8 +1,10 @@ /* - * LG_APPROVE.pgc - lg 모듈 서비스 LG_APPROVE (one service = one file). + * LG_APPROVE.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_APPROVE. * 전표 승인 워크플로: 작성(D) 전표만 대상으로 차변합=대변합 균형을 검증한 뒤 * P(전기)로 승격한다. 불균형 전표는 승인 거부 (복식부기 원칙 강제). - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_APPROVE.h" @@ -14,6 +16,84 @@ void LG_APPROVE(TPSVCINFO *p) EXEC SQL BEGIN DECLARE SECTION; long h_vid, h_dr, h_cr; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_APPROVE"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "분개액 음수 불가", 0L); + userlog("LG_APPROVE 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_APPROVE 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_APPROVE 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_APPROVE 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(*),0), coalesce(max(voucher_id),0) + INTO :gx_cnt, :gx_max FROM lg_voucher WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + if (gx_in_id > 0) { + EXEC SQL SELECT coalesce(status,'?'), coalesce(ref_id,0) + INTO :gx_mst, :gx_prev FROM lg_voucher WHERE voucher_id = :gx_in_id; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_APPROVE 전표 미존재 vid=%ld", gx_in_id); + } + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(RECV/ADJ) */ + userlog("LG_APPROVE 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "S") == 0) { + userlog("LG_APPROVE 마감연계(S) 전표 - 정정은 역분개 권고", gx_svc); + } else if (gx_mst[0] && strcmp(gx_mst, "X") == 0) { + gx_rc = 2; /* 취소 전표 - 재개 필요 */ + } else if (gx_dr != gx_cr) { + gx_diff = gx_dr - gx_cr; + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_APPROVE 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); if (lgdb_get_voucher_status(vid, st) < 0) { Bchg(b, T_MSG, 0, "전표 없음", 0L); FAIL(b); } if (st[0] != 'D') { Bchg(b, T_MSG, 0, "작성 상태 전표만 승인 가능", 0L); FAIL(b); } @@ -33,6 +113,31 @@ void LG_APPROVE(TPSVCINFO *p) setl(b, T_AMT2, h_dr); Bchg(b, T_STATUS, 0, "P", 0L); userlog("LG_APPROVE vid=%ld 승인 (차=대=%ld 균형 확인)", vid, h_dr); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_APPROVE band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_APPROVE 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0) INTO :gx_cnt + FROM lg_voucher WHERE biz_date = :gx_bd AND status = 'P'; + if (sqlca.sqlcode == 100) gx_cnt = 0; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_APPROVE 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_APPRQUEUE.pgc b/app/src/lg/svc/LG_APPRQUEUE.pgc index 4ae9347..8643126 100644 --- a/app/src/lg/svc/LG_APPRQUEUE.pgc +++ b/app/src/lg/svc/LG_APPRQUEUE.pgc @@ -1,8 +1,10 @@ /* - * LG_APPRQUEUE.pgc - lg 모듈 서비스 LG_APPRQUEUE (one service = one file). + * LG_APPRQUEUE.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_APPRQUEUE. * 전표 승인 대기 큐 조회: 미결(D) 전표의 대기 건수와 전표 ID 범위(최소/최대), * 균형/불균형 대기 건수를 나누어 반환한다 (승인 워크플로 대시보드용). - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_APPRQUEUE.h" @@ -12,6 +14,87 @@ void LG_APPRQUEUE(TPSVCINFO *p) EXEC SQL BEGIN DECLARE SECTION; long h_total, h_minv, h_maxv, h_balanced; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_APPRQUEUE"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "금액 음수 불가", 0L); + userlog("LG_APPRQUEUE 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_APPRQUEUE 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_APPRQUEUE 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_APPRQUEUE 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(*),0), coalesce(max(voucher_id),0) + INTO :gx_cnt, :gx_max FROM lg_voucher WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + if (gx_in_id > 0) { + EXEC SQL SELECT coalesce(status,'?'), coalesce(ref_id,0) + INTO :gx_mst, :gx_prev FROM lg_voucher WHERE voucher_id = :gx_in_id; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_APPRQUEUE 전표 미존재 vid=%ld", gx_in_id); + } + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(CASH/CASH_RECOV) */ + userlog("LG_APPRQUEUE 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "S") == 0) { + userlog("LG_APPRQUEUE 마감연계(S) 전표 - 정정은 역분개 권고", gx_svc); + } else if (gx_mst[0] && strcmp(gx_mst, "X") == 0) { + gx_rc = 2; /* 취소 전표 - 재개 필요 */ + } else if (gx_dr != gx_cr) { + gx_diff = gx_dr - gx_cr; + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_APPRQUEUE 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); EXEC SQL SELECT count(*), coalesce(min(voucher_id), 0), coalesce(max(voucher_id), 0) INTO :h_total, :h_minv, :h_maxv FROM lg_voucher WHERE status = 'D'; @@ -30,6 +113,31 @@ void LG_APPRQUEUE(TPSVCINFO *p) setl(b, T_ID1, h_minv); setl(b, T_ID2, h_maxv); setl(b, T_ARG1, h_balanced); setl(b, T_ARG2, h_total - h_balanced); userlog("LG_APPRQUEUE 대기 %ld건 (균형 %ld / 불균형 %ld)", h_total, h_balanced, h_total - h_balanced); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_APPRQUEUE band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_APPRQUEUE 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0) INTO :gx_cnt + FROM lg_voucher WHERE biz_date = :gx_bd AND status = 'P'; + if (sqlca.sqlcode == 100) gx_cnt = 0; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_APPRQUEUE 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_AUDITSCAN.pgc b/app/src/lg/svc/LG_AUDITSCAN.pgc index 6b60dc0..2bd56b8 100644 --- a/app/src/lg/svc/LG_AUDITSCAN.pgc +++ b/app/src/lg/svc/LG_AUDITSCAN.pgc @@ -1,7 +1,9 @@ /* - * LG_AUDITSCAN.pgc - lg 모듈 서비스 LG_AUDITSCAN (one service = one file). + * LG_AUDITSCAN.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_AUDITSCAN. * 원장 감사 스캔: 취소(X) 전표인데 라인이 여전히 posted 인 불일치와 고아 라인을 탐지. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_AUDITSCAN.h" @@ -13,6 +15,74 @@ void LG_AUDITSCAN(TPSVCINFO *p) char h_bizdate[16]; long h_bad, h_orphan; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_AUDITSCAN"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_AUDITSCAN 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_AUDITSCAN 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_AUDITSCAN 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_AUDITSCAN 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(BADDEBT_EXP/UNEARNED_REV) */ + userlog("LG_AUDITSCAN 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_AUDITSCAN 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_AUDITSCAN 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); 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_bad @@ -27,6 +97,32 @@ void LG_AUDITSCAN(TPSVCINFO *p) setl(b, T_COUNT, h_bad + h_orphan); Bchg(b, T_STATUS, 0, (h_bad + h_orphan == 0) ? "CLEAN" : "FINDINGS", 0L); userlog("LG_AUDITSCAN %s 취소불일치 %ld 고아 %ld", bizdate, h_bad, h_orphan); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_AUDITSCAN band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_AUDITSCAN 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_AUDITSCAN 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_BADDEBT.pgc b/app/src/lg/svc/LG_BADDEBT.pgc index 07729c5..7669134 100644 --- a/app/src/lg/svc/LG_BADDEBT.pgc +++ b/app/src/lg/svc/LG_BADDEBT.pgc @@ -1,8 +1,10 @@ /* - * LG_BADDEBT.pgc - lg 모듈 서비스 LG_BADDEBT (one service = one file). + * LG_BADDEBT.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_BADDEBT. * 대손 처리 분개: 가맹점(T_MERCHANT) 채권 T_AMOUNT 를 상각 — * D:BADDEBT_EXP / C:RECV 균형 전표를 전기하고 가맹점 잔액을 재산출한다. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_BADDEBT.h" @@ -11,6 +13,77 @@ void LG_BADDEBT(TPSVCINFO *p) UBFH *b = (UBFH *)p->data; char merch[64], bizdate[16], reason[64], memo[96]; long vid, amount = getl(b, T_AMOUNT); + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_BADDEBT"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_BADDEBT 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_BADDEBT 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_BADDEBT 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_BADDEBT 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(ALLOWANCE/PREPAID) */ + userlog("LG_BADDEBT 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_BADDEBT 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_BADDEBT 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); @@ -30,6 +103,32 @@ void LG_BADDEBT(TPSVCINFO *p) setl(b, T_ID1, vid); Bchg(b, T_STATUS, 0, "P", 0L); userlog("LG_BADDEBT vid=%ld %s 대손 %ld 상각 (차=대 균형)", vid, merch, amount); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_BADDEBT band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_BADDEBT 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_BADDEBT 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_BADDEBTRCV.pgc b/app/src/lg/svc/LG_BADDEBTRCV.pgc index 86fec69..10a4223 100644 --- a/app/src/lg/svc/LG_BADDEBTRCV.pgc +++ b/app/src/lg/svc/LG_BADDEBTRCV.pgc @@ -1,8 +1,10 @@ /* - * LG_BADDEBTRCV.pgc - lg 모듈 서비스 LG_BADDEBTRCV (one service = one file). + * LG_BADDEBTRCV.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_BADDEBTRCV. * 대손 회수(환입) 분개: 상각했던 채권이 회수되면 D:CASH / C:BADDEBT_RCV 로 * 환입 전기한다. 회수액이 기존 상각 누계(D BADDEBT_EXP 합)를 넘으면 거부. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_BADDEBTRCV.h" @@ -15,6 +17,74 @@ void LG_BADDEBTRCV(TPSVCINFO *p) char h_merch[64]; long h_written; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_BADDEBTRCV"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "대상액 음수 불가", 0L); + userlog("LG_BADDEBTRCV 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_BADDEBTRCV 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_BADDEBTRCV 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_BADDEBTRCV 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(PREPAID/MERCH_PAY) */ + userlog("LG_BADDEBTRCV 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_BADDEBTRCV 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_BADDEBTRCV 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); @@ -40,6 +110,32 @@ void LG_BADDEBTRCV(TPSVCINFO *p) setl(b, T_ID1, vid); setl(b, T_AMT2, h_written - amount); userlog("LG_BADDEBTRCV vid=%ld %s 회수 %ld (잔여 상각 %ld)", vid, merch, amount, h_written - amount); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_BADDEBTRCV band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_BADDEBTRCV 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_BADDEBTRCV 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_BALCERT.pgc b/app/src/lg/svc/LG_BALCERT.pgc index 3dd6ced..76b0163 100644 --- a/app/src/lg/svc/LG_BALCERT.pgc +++ b/app/src/lg/svc/LG_BALCERT.pgc @@ -1,7 +1,9 @@ /* - * LG_BALCERT.pgc - lg 모듈 서비스 LG_BALCERT (one service = one file). + * LG_BALCERT.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_BALCERT. * 잔액증명서 발급: 계정 차변/대변을 재계산해 계정잔액에 upsert 하고 잔액을 증명. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_BALCERT.h" @@ -14,6 +16,79 @@ void LG_BALCERT(TPSVCINFO *p) char h_acct[32], h_bizdate[16]; long h_dr, h_cr; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_BALCERT"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_BALCERT 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_BALCERT 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_BALCERT 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_BALCERT 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(debit_total,0), coalesce(credit_total,0), coalesce(balance,0) + INTO :gx_prev, :gx_amt, :gx_bal FROM lg_account_bal + WHERE acct = 'RECV' AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_prev = 0; gx_amt = 0; gx_bal = 0; } + EXEC SQL SELECT coalesce(count(DISTINCT acct),0) INTO :gx_max + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_max = 0; + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(BADDEBT_RCV/PREPAID) */ + userlog("LG_BALCERT 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + gx_diff = gx_dr - gx_cr; + if (gx_diff != 0) { + userlog("LG_BALCERT 당일 대차 불균형 diff=%ld", gx_diff); + } else if (gx_max == 0) { + gx_rc = 3; /* 무거래일 */ + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_BALCERT 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_KEY1, acct, sizeof(acct)); if (acct[0] == 0) strcpy(acct, "RECV"); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_acct, acct, sizeof(h_acct)-1); h_acct[sizeof(h_acct)-1] = 0; @@ -28,6 +103,32 @@ void LG_BALCERT(TPSVCINFO *p) setl(b, T_AMT2, h_dr); setl(b, T_AMT3, h_cr); setl(b, T_NET, bal); Bchg(b, T_STATUS, 0, "CERTIFIED", 0L); userlog("LG_BALCERT %s %s 잔액 %ld 증명", acct, bizdate, bal); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_BALCERT band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_BALCERT 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(debit_total),0), coalesce(sum(credit_total),0) + INTO :gx_dr, :gx_cr FROM lg_account_bal WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_diff = gx_dr - gx_cr; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_BALCERT 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_BALCHK.pgc b/app/src/lg/svc/LG_BALCHK.pgc index 835c06f..d560ad7 100644 --- a/app/src/lg/svc/LG_BALCHK.pgc +++ b/app/src/lg/svc/LG_BALCHK.pgc @@ -1,8 +1,9 @@ /* - * LG_BALCHK.pgc - lg 모듈 서비스 LG_BALCHK (one service = one file). + * LG_BALCHK.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_BALCHK. * 전표 단위 대차 균형 점검 (차변합 vs 대변합). - * 이전 인라인 lg_svr.pgc 에서 분리; 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). - * lg_svr 가 tpsvrinit 에서 tpadvertise 로 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_BALCHK.h" @@ -12,6 +13,79 @@ void LG_BALCHK(TPSVCINFO *p) EXEC SQL BEGIN DECLARE SECTION; long h_vid, h_dr, h_cr; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_BALCHK"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_BALCHK 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_BALCHK 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_BALCHK 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_BALCHK 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(debit_total,0), coalesce(credit_total,0), coalesce(balance,0) + INTO :gx_prev, :gx_amt, :gx_bal FROM lg_account_bal + WHERE acct = 'RECV' AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_prev = 0; gx_amt = 0; gx_bal = 0; } + EXEC SQL SELECT coalesce(count(DISTINCT acct),0) INTO :gx_max + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_max = 0; + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(RETAINED/ADJ) */ + userlog("LG_BALCHK 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + gx_diff = gx_dr - gx_cr; + if (gx_diff != 0) { + userlog("LG_BALCHK 당일 대차 불균형 diff=%ld", gx_diff); + } else if (gx_max == 0) { + gx_rc = 3; /* 무거래일 */ + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_BALCHK 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); h_vid = getl(b, T_ID1); EXEC SQL SELECT coalesce(sum(CASE WHEN dc = 'D' THEN amount ELSE 0 END), 0), coalesce(sum(CASE WHEN dc = 'C' THEN amount ELSE 0 END), 0) @@ -22,6 +96,32 @@ void LG_BALCHK(TPSVCINFO *p) setl(b, T_RC, (h_dr == h_cr) ? 0 : 1); userlog("LG_BALCHK vid=%ld 차변=%ld 대변=%ld %s", h_vid, h_dr, h_cr, (h_dr == h_cr) ? "균형" : "불균형"); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_BALCHK band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_BALCHK 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(debit_total),0), coalesce(sum(credit_total),0) + INTO :gx_dr, :gx_cr FROM lg_account_bal WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_diff = gx_dr - gx_cr; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_BALCHK 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_BALPROOF.pgc b/app/src/lg/svc/LG_BALPROOF.pgc index 63132e3..53cdf05 100644 --- a/app/src/lg/svc/LG_BALPROOF.pgc +++ b/app/src/lg/svc/LG_BALPROOF.pgc @@ -1,8 +1,10 @@ /* - * LG_BALPROOF.pgc - lg 모듈 서비스 LG_BALPROOF (one service = one file). + * LG_BALPROOF.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_BALPROOF. * 잔액 증명: 계정잔액 테이블(lg_account_bal)의 기록 잔액과 원장 라인에서 * 재계산한 잔액(차변-대변)을 대조해 증명서 판정(OK/NG)과 오차를 반환한다. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_BALPROOF.h" @@ -15,6 +17,79 @@ void LG_BALPROOF(TPSVCINFO *p) char h_acct[32], h_bizdate[16]; long h_bal, h_dr, h_cr; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_BALPROOF"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "분개액 음수 불가", 0L); + userlog("LG_BALPROOF 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_BALPROOF 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_BALPROOF 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_BALPROOF 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(debit_total,0), coalesce(credit_total,0), coalesce(balance,0) + INTO :gx_prev, :gx_amt, :gx_bal FROM lg_account_bal + WHERE acct = 'RECV' AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_prev = 0; gx_amt = 0; gx_bal = 0; } + EXEC SQL SELECT coalesce(count(DISTINCT acct),0) INTO :gx_max + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_max = 0; + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(RECV/SUSP) */ + userlog("LG_BALPROOF 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + gx_diff = gx_dr - gx_cr; + if (gx_diff != 0) { + userlog("LG_BALPROOF 당일 대차 불균형 diff=%ld", gx_diff); + } else if (gx_max == 0) { + gx_rc = 3; /* 무거래일 */ + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_BALPROOF 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_KEY1, acct, sizeof(acct)); if (acct[0] == 0) strcpy(acct, "RECV"); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); @@ -38,6 +113,32 @@ void LG_BALPROOF(TPSVCINFO *p) setl(b, T_DELTA, recorded - recomputed); Bchg(b, T_STATUS, 0, (recorded == recomputed) ? "OK" : "NG", 0L); userlog("LG_BALPROOF %s %s 기록 %ld vs 재계산 %ld", acct, bizdate, recorded, recomputed); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_BALPROOF band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_BALPROOF 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(debit_total),0), coalesce(sum(credit_total),0) + INTO :gx_dr, :gx_cr FROM lg_account_bal WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_diff = gx_dr - gx_cr; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_BALPROOF 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_BATCHAPPROVE.pgc b/app/src/lg/svc/LG_BATCHAPPROVE.pgc index e68e827..7fcab75 100644 --- a/app/src/lg/svc/LG_BATCHAPPROVE.pgc +++ b/app/src/lg/svc/LG_BATCHAPPROVE.pgc @@ -1,7 +1,9 @@ /* - * LG_BATCHAPPROVE.pgc - lg 모듈 서비스 LG_BATCHAPPROVE (one service = one file). + * LG_BATCHAPPROVE.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_BATCHAPPROVE. * 일괄 승인: 당일 작성(D) 전표를 순회하며 차대 균형이 맞는 것만 전기(P) 승인. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_BATCHAPPROVE.h" @@ -14,6 +16,84 @@ void LG_BATCHAPPROVE(TPSVCINFO *p) char h_bizdate[16]; long h_vid, h_dr, h_cr; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_BATCHAPPROVE"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "금액 음수 불가", 0L); + userlog("LG_BATCHAPPROVE 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_BATCHAPPROVE 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_BATCHAPPROVE 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_BATCHAPPROVE 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(*),0), coalesce(max(voucher_id),0) + INTO :gx_cnt, :gx_max FROM lg_voucher WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + if (gx_in_id > 0) { + EXEC SQL SELECT coalesce(status,'?'), coalesce(ref_id,0) + INTO :gx_mst, :gx_prev FROM lg_voucher WHERE voucher_id = :gx_in_id; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_BATCHAPPROVE 전표 미존재 vid=%ld", gx_in_id); + } + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(RECV/MERCH_PAY) */ + userlog("LG_BATCHAPPROVE 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "S") == 0) { + userlog("LG_BATCHAPPROVE 마감연계(S) 전표 - 정정은 역분개 권고", gx_svc); + } else if (gx_mst[0] && strcmp(gx_mst, "X") == 0) { + gx_rc = 2; /* 취소 전표 - 재개 필요 */ + } else if (gx_dr != gx_cr) { + gx_diff = gx_dr - gx_cr; + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_BATCHAPPROVE 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; EXEC SQL DECLARE ba_cur CURSOR FOR @@ -40,6 +120,31 @@ void LG_BATCHAPPROVE(TPSVCINFO *p) EXEC SQL CLOSE ba_cur; setl(b, T_COUNT, approved); setl(b, T_ARG1, rejected); userlog("LG_BATCHAPPROVE %s 승인 %ld 반려 %ld", bizdate, approved, rejected); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_BATCHAPPROVE band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_BATCHAPPROVE 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0) INTO :gx_cnt + FROM lg_voucher WHERE biz_date = :gx_bd AND status = 'P'; + if (sqlca.sqlcode == 100) gx_cnt = 0; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_BATCHAPPROVE 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_BIGLINE.pgc b/app/src/lg/svc/LG_BIGLINE.pgc index fceab47..8cf8c04 100644 --- a/app/src/lg/svc/LG_BIGLINE.pgc +++ b/app/src/lg/svc/LG_BIGLINE.pgc @@ -1,8 +1,10 @@ /* - * LG_BIGLINE.pgc - lg 모듈 서비스 LG_BIGLINE (one service = one file). + * LG_BIGLINE.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_BIGLINE. * 고액 분개 조회: 임계금액(T_AMOUNT, 기본 1000만) 이상 라인의 건수·합계와 * 최고액 라인의 ID/전표/계정을 반환한다 (고액 이상거래 모니터링). - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_BIGLINE.h" @@ -15,6 +17,74 @@ void LG_BIGLINE(TPSVCINFO *p) char h_bizdate[16], h_acct[32]; long h_thresh, h_cnt, h_sum, h_lid, h_vid, h_top; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_BIGLINE"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_BIGLINE 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_BIGLINE 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_BIGLINE 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_BIGLINE 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(ACCRUED_PAY/UNEARNED_REV) */ + userlog("LG_BIGLINE 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_BIGLINE 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_BIGLINE 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); if (thresh <= 0) thresh = 10000000; @@ -39,6 +109,32 @@ void LG_BIGLINE(TPSVCINFO *p) setl(b, T_ID1, h_lid); setl(b, T_ID2, h_vid); setl(b, T_AMT2, h_top); Bchg(b, T_KEY1, 0, h_acct, 0L); userlog("LG_BIGLINE %s 임계 %ld 이상 %ld건 최고 %ld(line %ld)", bizdate, thresh, h_cnt, h_top, h_lid); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_BIGLINE band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_BIGLINE 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_BIGLINE 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_BLACKADJUST.pgc b/app/src/lg/svc/LG_BLACKADJUST.pgc index b7cfee1..42e8ebc 100644 --- a/app/src/lg/svc/LG_BLACKADJUST.pgc +++ b/app/src/lg/svc/LG_BLACKADJUST.pgc @@ -1,7 +1,9 @@ /* - * LG_BLACKADJUST.pgc - lg 모듈 서비스 LG_BLACKADJUST (one service = one file). + * LG_BLACKADJUST.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_BLACKADJUST. * 흑색 정정분개: 과소계상분을 양수(흑색) 분개 쌍으로 추가 보정. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_BLACKADJUST.h" @@ -10,6 +12,87 @@ void LG_BLACKADJUST(TPSVCINFO *p) UBFH *b = (UBFH *)p->data; char acct[32], bizdate[16]; long src, vid, mag; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_BLACKADJUST"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "대상액 음수 불가", 0L); + userlog("LG_BLACKADJUST 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_BLACKADJUST 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_BLACKADJUST 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_BLACKADJUST 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(*),0), coalesce(max(voucher_id),0) + INTO :gx_cnt, :gx_max FROM lg_voucher WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + if (gx_in_id > 0) { + EXEC SQL SELECT coalesce(status,'?'), coalesce(ref_id,0) + INTO :gx_mst, :gx_prev FROM lg_voucher WHERE voucher_id = :gx_in_id; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_BLACKADJUST 전표 미존재 vid=%ld", gx_in_id); + } + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(RETAINED/ADJ) */ + userlog("LG_BLACKADJUST 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "S") == 0) { + userlog("LG_BLACKADJUST 마감연계(S) 전표 - 정정은 역분개 권고", gx_svc); + } else if (gx_mst[0] && strcmp(gx_mst, "X") == 0) { + gx_rc = 2; /* 취소 전표 - 재개 필요 */ + } else if (gx_dr != gx_cr) { + gx_diff = gx_dr - gx_cr; + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_BLACKADJUST 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_KEY1, acct, sizeof(acct)); if (acct[0] == 0) strcpy(acct, "RECV"); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); src = getl(b, T_ID1); @@ -21,6 +104,31 @@ void LG_BLACKADJUST(TPSVCINFO *p) lgdb_insert_line(vid, acct, "C", mag, "", bizdate, "posted") < 0) FAIL(b); setl(b, T_ID2, vid); setl(b, T_DELTA, mag); userlog("LG_BLACKADJUST 원전표 %ld 흑색 %s +%ld vid=%ld", src, acct, mag, vid); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_BLACKADJUST band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_BLACKADJUST 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0) INTO :gx_cnt + FROM lg_voucher WHERE biz_date = :gx_bd AND status = 'P'; + if (sqlca.sqlcode == 100) gx_cnt = 0; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_BLACKADJUST 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_BLACKENTRY.pgc b/app/src/lg/svc/LG_BLACKENTRY.pgc index dbf940a..ea1e60c 100644 --- a/app/src/lg/svc/LG_BLACKENTRY.pgc +++ b/app/src/lg/svc/LG_BLACKENTRY.pgc @@ -1,8 +1,10 @@ /* - * LG_BLACKENTRY.pgc - lg 모듈 서비스 LG_BLACKENTRY (one service = one file). + * LG_BLACKENTRY.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_BLACKENTRY. * 흑색 정정 분개: 원전표(T_ID1)를 반전 상쇄한 뒤 정정 금액(T_AMOUNT)으로 * 원전표의 대표 차변/대변 계정을 재분개한다 (두 전표 모두 차변합=대변합). - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_BLACKENTRY.h" @@ -16,6 +18,84 @@ void LG_BLACKENTRY(TPSVCINFO *p) char h_bizdate[16], h_dacct[32], h_cacct[32]; long h_orig, h_rvid; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_BLACKENTRY"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "분개액 음수 불가", 0L); + userlog("LG_BLACKENTRY 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_BLACKENTRY 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_BLACKENTRY 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_BLACKENTRY 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(*),0), coalesce(max(voucher_id),0) + INTO :gx_cnt, :gx_max FROM lg_voucher WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + if (gx_in_id > 0) { + EXEC SQL SELECT coalesce(status,'?'), coalesce(ref_id,0) + INTO :gx_mst, :gx_prev FROM lg_voucher WHERE voucher_id = :gx_in_id; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_BLACKENTRY 전표 미존재 vid=%ld", gx_in_id); + } + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(MERCH_PAY/RECV) */ + userlog("LG_BLACKENTRY 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "S") == 0) { + userlog("LG_BLACKENTRY 마감연계(S) 전표 - 정정은 역분개 권고", gx_svc); + } else if (gx_mst[0] && strcmp(gx_mst, "X") == 0) { + gx_rc = 2; /* 취소 전표 - 재개 필요 */ + } else if (gx_dr != gx_cr) { + gx_diff = gx_dr - gx_cr; + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_BLACKENTRY 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); gets_(b, T_MERCHANT, merch, sizeof(merch)); @@ -52,6 +132,31 @@ void LG_BLACKENTRY(TPSVCINFO *p) setl(b, T_ID2, nvid); setl(b, T_ARG1, rvid); userlog("LG_BLACKENTRY %ld 취소(%ld) 후 재분개(%ld) %ld", orig, rvid, nvid, newamt); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_BLACKENTRY band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_BLACKENTRY 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0) INTO :gx_cnt + FROM lg_voucher WHERE biz_date = :gx_bd AND status = 'P'; + if (sqlca.sqlcode == 100) gx_cnt = 0; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_BLACKENTRY 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_BSSUMMARY.pgc b/app/src/lg/svc/LG_BSSUMMARY.pgc index 3d3439b..37c3ddc 100644 --- a/app/src/lg/svc/LG_BSSUMMARY.pgc +++ b/app/src/lg/svc/LG_BSSUMMARY.pgc @@ -1,7 +1,9 @@ /* - * LG_BSSUMMARY.pgc - lg 모듈 서비스 LG_BSSUMMARY (one service = one file). + * LG_BSSUMMARY.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_BSSUMMARY. * 재무상태 요약: 자산(RECV/CASH/PREPAID) - 부채(MERCH_PAY/UNEARNED_REV) = 자본. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_BSSUMMARY.h" @@ -13,6 +15,74 @@ void LG_BSSUMMARY(TPSVCINFO *p) char h_bizdate[16]; long h_asset, h_liab; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_BSSUMMARY"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_BSSUMMARY 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_BSSUMMARY 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_BSSUMMARY 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_BSSUMMARY 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(BADDEBT_EXP/UNEARNED_REV) */ + userlog("LG_BSSUMMARY 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_BSSUMMARY 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_BSSUMMARY 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); 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 @@ -23,6 +93,32 @@ void LG_BSSUMMARY(TPSVCINFO *p) setl(b, T_AMT2, h_asset); setl(b, T_AMT3, h_liab); setl(b, T_NET, h_asset - h_liab); userlog("LG_BSSUMMARY %s 자산 %ld 부채 %ld 자본 %ld", bizdate, h_asset, h_liab, h_asset - h_liab); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_BSSUMMARY band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_BSSUMMARY 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_BSSUMMARY 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_CARRY.pgc b/app/src/lg/svc/LG_CARRY.pgc index 88cbd14..2b0c531 100644 --- a/app/src/lg/svc/LG_CARRY.pgc +++ b/app/src/lg/svc/LG_CARRY.pgc @@ -1,8 +1,9 @@ /* - * LG_CARRY.pgc - lg 모듈 서비스 LG_CARRY (one service = one file). + * LG_CARRY.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_CARRY. * 이월: 당일 계정잔액을 익영업일 개시잔액으로 이월(upsert). - * 이전 인라인 lg_svr.pgc 에서 분리; 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). - * lg_svr 가 tpsvrinit 에서 tpadvertise 로 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_CARRY.h" @@ -14,6 +15,82 @@ void LG_CARRY(TPSVCINFO *p) char h_bizdate[16], h_next[16]; long h_cnt; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_CARRY"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "금액 음수 불가", 0L); + userlog("LG_CARRY 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_CARRY 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_CARRY 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_CARRY 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(debit_total,0), coalesce(credit_total,0), coalesce(balance,0) + INTO :gx_prev, :gx_amt, :gx_bal FROM lg_account_bal + WHERE acct = 'RECV' AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_prev = 0; gx_amt = 0; gx_bal = 0; } + EXEC SQL SELECT coalesce(count(DISTINCT acct),0) INTO :gx_max + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_max = 0; + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(CASH/CASH_RECOV) */ + userlog("LG_CARRY 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + gx_diff = gx_dr - gx_cr; + if (gx_diff != 0) { + userlog("LG_CARRY 당일 대차 불균형 diff=%ld", gx_diff); + } else if (gx_max == 0) { + gx_rc = 3; /* 무거래일 */ + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_CARRY 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); if (acq_next_bizday(bizdate, nextday) < 0) strcpy(nextday, bizdate); @@ -32,6 +109,32 @@ void LG_CARRY(TPSVCINFO *p) h_cnt = sqlca.sqlerrd[2]; setl(b, T_COUNT, h_cnt); userlog("LG_CARRY %s -> %s 계정 %ld건 이월", bizdate, nextday, h_cnt); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_CARRY band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_CARRY 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(debit_total),0), coalesce(sum(credit_total),0) + INTO :gx_dr, :gx_cr FROM lg_account_bal WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_diff = gx_dr - gx_cr; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_CARRY 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_CLOSECHK.pgc b/app/src/lg/svc/LG_CLOSECHK.pgc index 0b44b16..975199a 100644 --- a/app/src/lg/svc/LG_CLOSECHK.pgc +++ b/app/src/lg/svc/LG_CLOSECHK.pgc @@ -1,8 +1,10 @@ /* - * LG_CLOSECHK.pgc - lg 모듈 서비스 LG_CLOSECHK (one service = one file). + * LG_CLOSECHK.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_CLOSECHK. * 결산 체크리스트: 당일 (1)미결 전표 (2)불균형 전표 (3)0원 라인 * (4)시산표 미적재 계정을 각각 점검해 4항목 카운트와 종합 판정을 반환한다. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_CLOSECHK.h" @@ -15,6 +17,74 @@ void LG_CLOSECHK(TPSVCINFO *p) char h_bizdate[16]; long h_draft, h_imbal, h_zero, h_notb; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_CLOSECHK"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "분개액 음수 불가", 0L); + userlog("LG_CLOSECHK 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_CLOSECHK 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_CLOSECHK 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_CLOSECHK 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(CASH/MDR_EXP) */ + userlog("LG_CLOSECHK 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_CLOSECHK 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_CLOSECHK 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; @@ -47,6 +117,32 @@ void LG_CLOSECHK(TPSVCINFO *p) setl(b, T_RC, fails); Bchg(b, T_STATUS, 0, (fails == 0) ? "OK" : "NG", 0L); userlog("LG_CLOSECHK %s 미결=%ld 불균형=%ld 0원=%ld 시산누락=%ld", bizdate, h_draft, h_imbal, h_zero, h_notb); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_CLOSECHK band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_CLOSECHK 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_CLOSECHK 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_CLOSELIST.pgc b/app/src/lg/svc/LG_CLOSELIST.pgc index ba48713..9427a40 100644 --- a/app/src/lg/svc/LG_CLOSELIST.pgc +++ b/app/src/lg/svc/LG_CLOSELIST.pgc @@ -1,7 +1,9 @@ /* - * LG_CLOSELIST.pgc - lg 모듈 서비스 LG_CLOSELIST (one service = one file). + * LG_CLOSELIST.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_CLOSELIST. * 결산 체크리스트: 미결전표/시산표 불균형/무분개 전표를 집계해 마감 준비도를 판정. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_CLOSELIST.h" @@ -10,6 +12,74 @@ void LG_CLOSELIST(TPSVCINFO *p) UBFH *b = (UBFH *)p->data; char bizdate[16]; long drafts, imbal, nolines, ready; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_CLOSELIST"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_CLOSELIST 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_CLOSELIST 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_CLOSELIST 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_CLOSELIST 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(BADDEBT_RCV/RECV) */ + userlog("LG_CLOSELIST 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_CLOSELIST 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_CLOSELIST 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); drafts = lgqdb_draft_count(); if (drafts < 0) FAIL(b); @@ -23,6 +93,32 @@ void LG_CLOSELIST(TPSVCINFO *p) Bchg(b, T_STATUS, 0, ready ? "READY" : "NOTREADY", 0L); userlog("LG_CLOSELIST %s 미결 %ld 불균형 %ld 무분개 %ld ready=%ld", bizdate, drafts, imbal, nolines, ready); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_CLOSELIST band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_CLOSELIST 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_CLOSELIST 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_CLOSE_LINK.pgc b/app/src/lg/svc/LG_CLOSE_LINK.pgc index 557c72a..6ba3bd7 100644 --- a/app/src/lg/svc/LG_CLOSE_LINK.pgc +++ b/app/src/lg/svc/LG_CLOSE_LINK.pgc @@ -1,8 +1,9 @@ /* - * LG_CLOSE_LINK.pgc - lg 모듈 서비스 LG_CLOSE_LINK (one service = one file). + * LG_CLOSE_LINK.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_CLOSE_LINK. * 마감연계: 전표 라인 closed + 전표 상태 S(마감연계). - * 이전 인라인 lg_svr.pgc 에서 분리; 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). - * lg_svr 가 tpsvrinit 에서 tpadvertise 로 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_CLOSE_LINK.h" @@ -12,6 +13,74 @@ void LG_CLOSE_LINK(TPSVCINFO *p) EXEC SQL BEGIN DECLARE SECTION; long h_vid; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_CLOSE_LINK"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "분개액 음수 불가", 0L); + userlog("LG_CLOSE_LINK 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_CLOSE_LINK 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_CLOSE_LINK 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_CLOSE_LINK 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(ADJ/BADDEBT_EXP) */ + userlog("LG_CLOSE_LINK 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_CLOSE_LINK 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_CLOSE_LINK 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); h_vid = getl(b, T_ID1); EXEC SQL UPDATE lg_ledger_line SET status = 'closed' WHERE voucher_id = :h_vid AND status = 'posted'; @@ -19,6 +88,32 @@ void LG_CLOSE_LINK(TPSVCINFO *p) setl(b, T_COUNT, sqlca.sqlerrd[2]); if (lgdb_update_voucher_status(h_vid, "S") < 0) FAIL(b); Bchg(b, T_STATUS, 0, "S", 0L); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_CLOSE_LINK band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_CLOSE_LINK 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_CLOSE_LINK 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_CONFIRM.pgc b/app/src/lg/svc/LG_CONFIRM.pgc index 6f47052..caf59b9 100644 --- a/app/src/lg/svc/LG_CONFIRM.pgc +++ b/app/src/lg/svc/LG_CONFIRM.pgc @@ -1,8 +1,9 @@ /* - * LG_CONFIRM.pgc - lg 모듈 서비스 LG_CONFIRM (one service = one file). + * LG_CONFIRM.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_CONFIRM. * 전표 확정: 대차 균형이면 P로 확정, 아니면 실패. - * 이전 인라인 lg_svr.pgc 에서 분리; 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). - * lg_svr 가 tpsvrinit 에서 tpadvertise 로 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_CONFIRM.h" @@ -12,6 +13,87 @@ void LG_CONFIRM(TPSVCINFO *p) EXEC SQL BEGIN DECLARE SECTION; long h_vid, h_dr, h_cr; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_CONFIRM"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "분개액 음수 불가", 0L); + userlog("LG_CONFIRM 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_CONFIRM 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_CONFIRM 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_CONFIRM 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(*),0), coalesce(max(voucher_id),0) + INTO :gx_cnt, :gx_max FROM lg_voucher WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + if (gx_in_id > 0) { + EXEC SQL SELECT coalesce(status,'?'), coalesce(ref_id,0) + INTO :gx_mst, :gx_prev FROM lg_voucher WHERE voucher_id = :gx_in_id; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_CONFIRM 전표 미존재 vid=%ld", gx_in_id); + } + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(MERCH_PAY/RETAINED) */ + userlog("LG_CONFIRM 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "S") == 0) { + userlog("LG_CONFIRM 마감연계(S) 전표 - 정정은 역분개 권고", gx_svc); + } else if (gx_mst[0] && strcmp(gx_mst, "X") == 0) { + gx_rc = 2; /* 취소 전표 - 재개 필요 */ + } else if (gx_dr != gx_cr) { + gx_diff = gx_dr - gx_cr; + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_CONFIRM 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); h_vid = getl(b, T_ID1); EXEC SQL SELECT coalesce(sum(CASE WHEN dc = 'D' THEN amount ELSE 0 END), 0), coalesce(sum(CASE WHEN dc = 'C' THEN amount ELSE 0 END), 0) @@ -25,6 +107,31 @@ void LG_CONFIRM(TPSVCINFO *p) if (lgdb_update_voucher_status(h_vid, "P") < 0) FAIL(b); setl(b, T_RC, 0); Bchg(b, T_STATUS, 0, "P", 0L); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_CONFIRM band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_CONFIRM 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0) INTO :gx_cnt + FROM lg_voucher WHERE biz_date = :gx_bd AND status = 'P'; + if (sqlca.sqlcode == 100) gx_cnt = 0; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_CONFIRM 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_CORPLEDGER.pgc b/app/src/lg/svc/LG_CORPLEDGER.pgc index d041352..6164588 100644 --- a/app/src/lg/svc/LG_CORPLEDGER.pgc +++ b/app/src/lg/svc/LG_CORPLEDGER.pgc @@ -1,7 +1,9 @@ /* - * LG_CORPLEDGER.pgc - lg 모듈 서비스 LG_CORPLEDGER (one service = one file). + * LG_CORPLEDGER.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_CORPLEDGER. * 법인(가맹점)별 원장 집계: 가맹점 원장 라인의 차변/대변 잔액을 산출. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_CORPLEDGER.h" @@ -13,6 +15,85 @@ void LG_CORPLEDGER(TPSVCINFO *p) char h_corp[64], h_bizdate[16]; long h_dr, h_cr; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_CORPLEDGER"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_CORPLEDGER 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_CORPLEDGER 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_CORPLEDGER 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_CORPLEDGER 라인집계 조회 경고 [%d]", sqlca.sqlcode); + if (gx_mid[0]) { + EXEC SQL SELECT coalesce(mdr_bps,0), coalesce(daily_limit,0), coalesce(status,'?') + INTO :gx_mbps, :gx_lim, :gx_mst FROM merchant WHERE merchant_id = :gx_mid; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_CORPLEDGER 가맹점 미등록 mid=%s (정책 기본값)", gx_mid); + } + EXEC SQL SELECT coalesce(balance,0) INTO :gx_bal + FROM lg_merch_bal WHERE merchant_id = :gx_mid AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_bal = 0; + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(ALLOWANCE/SUSP) */ + userlog("LG_CORPLEDGER 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "CLOSED") == 0) { + userlog("LG_CORPLEDGER 해지가맹점 mid=%s - 잔액정리 경로", gx_mid); + } else if (gx_mbps > 30) { + gx_rc = 1; /* 고 MDR 가맹점 - 수수료 배분 재검토 */ + } + if (gx_lim > 0 && gx_amt > gx_lim) { + userlog("LG_CORPLEDGER 일한도 초과소지 amt=%ld limit=%ld", gx_amt, gx_lim); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_CORPLEDGER 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_MERCHANT, corp, sizeof(corp)); if (corp[0] == 0) strcpy(corp, "M0001"); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_corp, corp, sizeof(h_corp)-1); h_corp[sizeof(h_corp)-1] = 0; @@ -24,6 +105,36 @@ void LG_CORPLEDGER(TPSVCINFO *p) if (sqlca.sqlcode < 0) FAIL(b); setl(b, T_AMT2, h_dr); setl(b, T_AMT3, h_cr); setl(b, T_NET, h_dr - h_cr); userlog("LG_CORPLEDGER %s %s 잔액 %ld", corp, bizdate, h_dr - h_cr); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_CORPLEDGER band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_CORPLEDGER 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + if (gx_mid[0]) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0) + INTO :gx_dr, :gx_cr FROM lg_ledger_line + WHERE merchant_id = :gx_mid AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_bal = gx_dr - gx_cr; + } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_CORPLEDGER 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_CORPSUM.pgc b/app/src/lg/svc/LG_CORPSUM.pgc index 8b9cfe7..00994ce 100644 --- a/app/src/lg/svc/LG_CORPSUM.pgc +++ b/app/src/lg/svc/LG_CORPSUM.pgc @@ -1,7 +1,9 @@ /* - * LG_CORPSUM.pgc - lg 모듈 서비스 LG_CORPSUM (one service = one file). + * LG_CORPSUM.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_CORPSUM. * 법인별 순액 집계: 가맹점별 (차변-대변) 순액의 절대값 합과 법인 수를 순회. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_CORPSUM.h" @@ -14,6 +16,85 @@ void LG_CORPSUM(TPSVCINFO *p) char h_bizdate[16], h_corp[64]; long h_net; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_CORPSUM"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "대상액 음수 불가", 0L); + userlog("LG_CORPSUM 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_CORPSUM 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_CORPSUM 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_CORPSUM 라인집계 조회 경고 [%d]", sqlca.sqlcode); + if (gx_mid[0]) { + EXEC SQL SELECT coalesce(mdr_bps,0), coalesce(daily_limit,0), coalesce(status,'?') + INTO :gx_mbps, :gx_lim, :gx_mst FROM merchant WHERE merchant_id = :gx_mid; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_CORPSUM 가맹점 미등록 mid=%s (정책 기본값)", gx_mid); + } + EXEC SQL SELECT coalesce(balance,0) INTO :gx_bal + FROM lg_merch_bal WHERE merchant_id = :gx_mid AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_bal = 0; + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(BADDEBT_RCV/ADJ) */ + userlog("LG_CORPSUM 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "CLOSED") == 0) { + userlog("LG_CORPSUM 해지가맹점 mid=%s - 잔액정리 경로", gx_mid); + } else if (gx_mbps > 30) { + gx_rc = 1; /* 고 MDR 가맹점 - 수수료 배분 재검토 */ + } + if (gx_lim > 0 && gx_amt > gx_lim) { + userlog("LG_CORPSUM 일한도 초과소지 amt=%ld limit=%ld", gx_amt, gx_lim); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_CORPSUM 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; EXEC SQL DECLARE cs_cur CURSOR FOR @@ -33,6 +114,36 @@ void LG_CORPSUM(TPSVCINFO *p) EXEC SQL CLOSE cs_cur; setl(b, T_COUNT, corps); setl(b, T_NET, net_abs); userlog("LG_CORPSUM %s 법인 %ld개 순액절대합 %ld", bizdate, corps, net_abs); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_CORPSUM band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_CORPSUM 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + if (gx_mid[0]) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0) + INTO :gx_dr, :gx_cr FROM lg_ledger_line + WHERE merchant_id = :gx_mid AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_bal = gx_dr - gx_cr; + } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_CORPSUM 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_CORRECT.pgc b/app/src/lg/svc/LG_CORRECT.pgc index 0f8023e..5a20c3f 100644 --- a/app/src/lg/svc/LG_CORRECT.pgc +++ b/app/src/lg/svc/LG_CORRECT.pgc @@ -1,8 +1,9 @@ /* - * LG_CORRECT.pgc - lg 모듈 서비스 LG_CORRECT (one service = one file). + * LG_CORRECT.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_CORRECT. * 원장 라인 금액 정정. - * 이전 인라인 lg_svr.pgc 에서 분리; 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). - * lg_svr 가 tpsvrinit 에서 tpadvertise 로 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_CORRECT.h" @@ -12,10 +13,107 @@ void LG_CORRECT(TPSVCINFO *p) EXEC SQL BEGIN DECLARE SECTION; long h_line, h_amount; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_CORRECT"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "금액 음수 불가", 0L); + userlog("LG_CORRECT 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_CORRECT 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_CORRECT 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_CORRECT 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(SUSP/ALLOWANCE) */ + userlog("LG_CORRECT 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_CORRECT 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_CORRECT 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); h_line = getl(b, T_ID1); h_amount = getl(b, T_AMOUNT); EXEC SQL UPDATE lg_ledger_line SET amount = :h_amount WHERE line_id = :h_line; if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_CORRECT band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_CORRECT 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_CORRECT 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_CORRPAIR.pgc b/app/src/lg/svc/LG_CORRPAIR.pgc index ec5c6cf..ff0b873 100644 --- a/app/src/lg/svc/LG_CORRPAIR.pgc +++ b/app/src/lg/svc/LG_CORRPAIR.pgc @@ -1,7 +1,9 @@ /* - * LG_CORRPAIR.pgc - lg 모듈 서비스 LG_CORRPAIR (one service = one file). + * LG_CORRPAIR.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_CORRPAIR. * 정정 짝분개: 잘못 분류된 계정을 올바른 계정으로 재분류(D 신계정/C 구계정). - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_CORRPAIR.h" @@ -10,6 +12,74 @@ void LG_CORRPAIR(TPSVCINFO *p) UBFH *b = (UBFH *)p->data; char from_acct[32], to_acct[32], bizdate[16], merch[64]; long vid, amount; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_CORRPAIR"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "대상액 음수 불가", 0L); + userlog("LG_CORRPAIR 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_CORRPAIR 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_CORRPAIR 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_CORRPAIR 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(RETAINED/PREPAID) */ + userlog("LG_CORRPAIR 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_CORRPAIR 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_CORRPAIR 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_KEY1, from_acct, sizeof(from_acct)); if (from_acct[0] == 0) strcpy(from_acct, "SUSPENSE"); gets_(b, T_KEY2, to_acct, sizeof(to_acct)); if (to_acct[0] == 0) strcpy(to_acct, "RECV"); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); @@ -22,6 +92,32 @@ void LG_CORRPAIR(TPSVCINFO *p) lgdb_insert_line(vid, from_acct, "C", amount, merch, bizdate, "posted") < 0) FAIL(b); setl(b, T_ID1, vid); userlog("LG_CORRPAIR %s -> %s %ld vid=%ld", from_acct, to_acct, amount, vid); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_CORRPAIR band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_CORRPAIR 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_CORRPAIR 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_CREDIT.pgc b/app/src/lg/svc/LG_CREDIT.pgc index e6da59f..9c20eb6 100644 --- a/app/src/lg/svc/LG_CREDIT.pgc +++ b/app/src/lg/svc/LG_CREDIT.pgc @@ -1,8 +1,9 @@ /* - * LG_CREDIT.pgc - lg 모듈 서비스 LG_CREDIT (one service = one file). + * LG_CREDIT.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_CREDIT. * 기존 전표에 대변(C) 분개 1줄 추가. - * 이전 인라인 lg_svr.pgc 에서 분리; 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). - * lg_svr 가 tpsvrinit 에서 tpadvertise 로 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_CREDIT.h" @@ -11,11 +12,114 @@ void LG_CREDIT(TPSVCINFO *p) UBFH *b = (UBFH *)p->data; char acct[32], bizdate[16], merch[64]; long vid = getl(b, T_ID1), amount = getl(b, T_AMOUNT); + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_CREDIT"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "대상액 음수 불가", 0L); + userlog("LG_CREDIT 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_CREDIT 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_CREDIT 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_CREDIT 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(*),0), coalesce(max(voucher_id),0) + INTO :gx_cnt, :gx_max FROM lg_voucher WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + if (gx_in_id > 0) { + EXEC SQL SELECT coalesce(status,'?'), coalesce(ref_id,0) + INTO :gx_mst, :gx_prev FROM lg_voucher WHERE voucher_id = :gx_in_id; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_CREDIT 전표 미존재 vid=%ld", gx_in_id); + } + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(BADDEBT_RCV/FEE_INC) */ + userlog("LG_CREDIT 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "S") == 0) { + userlog("LG_CREDIT 마감연계(S) 전표 - 정정은 역분개 권고", gx_svc); + } else if (gx_mst[0] && strcmp(gx_mst, "X") == 0) { + gx_rc = 2; /* 취소 전표 - 재개 필요 */ + } else if (gx_dr != gx_cr) { + gx_diff = gx_dr - gx_cr; + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_CREDIT 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_KEY1, acct, sizeof(acct)); if (acct[0] == 0) strcpy(acct, "MERCH_PAY"); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); gets_(b, T_MERCHANT, merch, sizeof(merch)); if (lgdb_insert_line(vid, acct, "C", amount, merch, bizdate, "posted") < 0) FAIL(b); userlog("LG_CREDIT vid=%ld 대변 %s %ld", vid, acct, amount); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_CREDIT band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_CREDIT 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0) INTO :gx_cnt + FROM lg_voucher WHERE biz_date = :gx_bd AND status = 'P'; + if (sqlca.sqlcode == 100) gx_cnt = 0; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_CREDIT 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_DAILYSUM.pgc b/app/src/lg/svc/LG_DAILYSUM.pgc index 98a09a7..2bb2f2f 100644 --- a/app/src/lg/svc/LG_DAILYSUM.pgc +++ b/app/src/lg/svc/LG_DAILYSUM.pgc @@ -1,8 +1,9 @@ /* - * LG_DAILYSUM.pgc - lg 모듈 서비스 LG_DAILYSUM (one service = one file). + * LG_DAILYSUM.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_DAILYSUM. * 당일 계정별 잔액 일괄 재집계(전 계정 upsert). - * 이전 인라인 lg_svr.pgc 에서 분리; 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). - * lg_svr 가 tpsvrinit 에서 tpadvertise 로 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_DAILYSUM.h" @@ -14,6 +15,74 @@ void LG_DAILYSUM(TPSVCINFO *p) char h_bizdate[16]; long h_cnt; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_DAILYSUM"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "금액 음수 불가", 0L); + userlog("LG_DAILYSUM 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_DAILYSUM 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_DAILYSUM 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_DAILYSUM 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(SUSP/RECV_ACCR) */ + userlog("LG_DAILYSUM 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_DAILYSUM 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_DAILYSUM 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; @@ -33,6 +102,32 @@ void LG_DAILYSUM(TPSVCINFO *p) h_cnt = sqlca.sqlerrd[2]; setl(b, T_COUNT, h_cnt); userlog("LG_DAILYSUM %s 계정 %ld건 집계", bizdate, h_cnt); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_DAILYSUM band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_DAILYSUM 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_DAILYSUM 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_DAYCLOSE.pgc b/app/src/lg/svc/LG_DAYCLOSE.pgc index 14e770a..af981e9 100644 --- a/app/src/lg/svc/LG_DAYCLOSE.pgc +++ b/app/src/lg/svc/LG_DAYCLOSE.pgc @@ -1,8 +1,10 @@ /* - * LG_DAYCLOSE.pgc - lg 모듈 서비스 LG_DAYCLOSE (one service = one file). + * LG_DAYCLOSE.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_DAYCLOSE. * 일마감: 당일 균형(차변합=대변합) 미결 전표를 일괄 P 승격한 뒤, 당일 사용된 * 계정과목을 커서로 돌며 계정잔액을 원장 라인 기준으로 재계산(dbio)한다. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_DAYCLOSE.h" @@ -14,6 +16,84 @@ void LG_DAYCLOSE(TPSVCINFO *p) EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16], h_acct[32]; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_DAYCLOSE"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "분개액 음수 불가", 0L); + userlog("LG_DAYCLOSE 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_DAYCLOSE 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_DAYCLOSE 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_DAYCLOSE 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(*),0), coalesce(max(voucher_id),0) + INTO :gx_cnt, :gx_max FROM lg_voucher WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + if (gx_in_id > 0) { + EXEC SQL SELECT coalesce(status,'?'), coalesce(ref_id,0) + INTO :gx_mst, :gx_prev FROM lg_voucher WHERE voucher_id = :gx_in_id; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_DAYCLOSE 전표 미존재 vid=%ld", gx_in_id); + } + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(MDR_EXP/RECV_ACCR) */ + userlog("LG_DAYCLOSE 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "S") == 0) { + userlog("LG_DAYCLOSE 마감연계(S) 전표 - 정정은 역분개 권고", gx_svc); + } else if (gx_mst[0] && strcmp(gx_mst, "X") == 0) { + gx_rc = 2; /* 취소 전표 - 재개 필요 */ + } else if (gx_dr != gx_cr) { + gx_diff = gx_dr - gx_cr; + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_DAYCLOSE 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; @@ -42,6 +122,31 @@ void LG_DAYCLOSE(TPSVCINFO *p) setl(b, T_ARG1, promoted); setl(b, T_COUNT, naccts); Bchg(b, T_STATUS, 0, "P", 0L); userlog("LG_DAYCLOSE %s 전표 %ld건 승격, 계정 %ld개 잔액 재계산", bizdate, promoted, naccts); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_DAYCLOSE band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_DAYCLOSE 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0) INTO :gx_cnt + FROM lg_voucher WHERE biz_date = :gx_bd AND status = 'P'; + if (sqlca.sqlcode == 100) gx_cnt = 0; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_DAYCLOSE 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_DEBIT.pgc b/app/src/lg/svc/LG_DEBIT.pgc index 5ce891a..a1d1928 100644 --- a/app/src/lg/svc/LG_DEBIT.pgc +++ b/app/src/lg/svc/LG_DEBIT.pgc @@ -1,8 +1,9 @@ /* - * LG_DEBIT.pgc - lg 모듈 서비스 LG_DEBIT (one service = one file). + * LG_DEBIT.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_DEBIT. * 기존 전표에 차변(D) 분개 1줄 추가. - * 이전 인라인 lg_svr.pgc 에서 분리; 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). - * lg_svr 가 tpsvrinit 에서 tpadvertise 로 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_DEBIT.h" @@ -11,11 +12,105 @@ void LG_DEBIT(TPSVCINFO *p) UBFH *b = (UBFH *)p->data; char acct[32], bizdate[16], merch[64]; long vid = getl(b, T_ID1), amount = getl(b, T_AMOUNT); + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_DEBIT"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "금액 음수 불가", 0L); + userlog("LG_DEBIT 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_DEBIT 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_DEBIT 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_DEBIT 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(RECV_ACCR/CASH) */ + userlog("LG_DEBIT 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_DEBIT 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_DEBIT 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_KEY1, acct, sizeof(acct)); if (acct[0] == 0) strcpy(acct, "RECV"); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); gets_(b, T_MERCHANT, merch, sizeof(merch)); if (lgdb_insert_line(vid, acct, "D", amount, merch, bizdate, "posted") < 0) FAIL(b); userlog("LG_DEBIT vid=%ld 차변 %s %ld", vid, acct, amount); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_DEBIT band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_DEBIT 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_DEBIT 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_DEPTLEDGER.pgc b/app/src/lg/svc/LG_DEPTLEDGER.pgc index 76751ec..ab7abd6 100644 --- a/app/src/lg/svc/LG_DEPTLEDGER.pgc +++ b/app/src/lg/svc/LG_DEPTLEDGER.pgc @@ -1,7 +1,9 @@ /* - * LG_DEPTLEDGER.pgc - lg 모듈 서비스 LG_DEPTLEDGER (one service = one file). + * LG_DEPTLEDGER.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_DEPTLEDGER. * 부문(전표유형)별 원장 조회: 유형별 차변/대변 합계와 분개 건수를 집계. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_DEPTLEDGER.h" @@ -13,6 +15,79 @@ void LG_DEPTLEDGER(TPSVCINFO *p) char h_vtype[16], h_bizdate[16]; long h_dr, h_cr, h_lines; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_DEPTLEDGER"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "금액 음수 불가", 0L); + userlog("LG_DEPTLEDGER 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_DEPTLEDGER 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_DEPTLEDGER 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_DEPTLEDGER 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(debit_total,0), coalesce(credit_total,0), coalesce(balance,0) + INTO :gx_prev, :gx_amt, :gx_bal FROM lg_account_bal + WHERE acct = 'RECV' AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_prev = 0; gx_amt = 0; gx_bal = 0; } + EXEC SQL SELECT coalesce(count(DISTINCT acct),0) INTO :gx_max + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_max = 0; + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(MERCH_PAY/RECV) */ + userlog("LG_DEPTLEDGER 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + gx_diff = gx_dr - gx_cr; + if (gx_diff != 0) { + userlog("LG_DEPTLEDGER 당일 대차 불균형 diff=%ld", gx_diff); + } else if (gx_max == 0) { + gx_rc = 3; /* 무거래일 */ + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_DEPTLEDGER 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_KEY1, vtype, sizeof(vtype)); if (vtype[0] == 0) strcpy(vtype, "POST"); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_vtype, vtype, sizeof(h_vtype)-1); h_vtype[sizeof(h_vtype)-1] = 0; @@ -27,6 +102,32 @@ void LG_DEPTLEDGER(TPSVCINFO *p) setl(b, T_AMT2, h_dr); setl(b, T_AMT3, h_cr); setl(b, T_DELTA, h_dr - h_cr); setl(b, T_COUNT, h_lines); userlog("LG_DEPTLEDGER %s %s 차변 %ld 대변 %ld", vtype, bizdate, h_dr, h_cr); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_DEPTLEDGER band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_DEPTLEDGER 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(debit_total),0), coalesce(sum(credit_total),0) + INTO :gx_dr, :gx_cr FROM lg_account_bal WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_diff = gx_dr - gx_cr; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_DEPTLEDGER 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_DEPTPL.pgc b/app/src/lg/svc/LG_DEPTPL.pgc index f3b8ebd..b158eb2 100644 --- a/app/src/lg/svc/LG_DEPTPL.pgc +++ b/app/src/lg/svc/LG_DEPTPL.pgc @@ -1,8 +1,10 @@ /* - * LG_DEPTPL.pgc - lg 모듈 서비스 LG_DEPTPL (one service = one file). + * LG_DEPTPL.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_DEPTPL. * 부문별 손익 조회: 당일 매입을 업종/부문(category)별로 묶어 수수료 손익을 * 커서로 집계 — 부문 수, 총수수료, 최대 손익 부문명/금액을 반환한다. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_DEPTPL.h" @@ -15,6 +17,74 @@ void LG_DEPTPL(TPSVCINFO *p) char h_bizdate[16], h_cat[32]; long h_fee, h_cnt; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_DEPTPL"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_DEPTPL 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_DEPTPL 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_DEPTPL 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_DEPTPL 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(UNEARNED_REV/ACCRUED_PAY) */ + userlog("LG_DEPTPL 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_DEPTPL 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_DEPTPL 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; @@ -41,6 +111,32 @@ void LG_DEPTPL(TPSVCINFO *p) setl(b, T_AMT2, bestfee < 0 ? 0 : bestfee); Bchg(b, T_CATEGORY, 0, best, 0L); userlog("LG_DEPTPL %s 부문 %ld개 총수수료 %ld 최대부문 %s", bizdate, depts, totfee, best); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_DEPTPL band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_DEPTPL 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_DEPTPL 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_DEPTRANGE.pgc b/app/src/lg/svc/LG_DEPTRANGE.pgc index 82e210e..41f0f44 100644 --- a/app/src/lg/svc/LG_DEPTRANGE.pgc +++ b/app/src/lg/svc/LG_DEPTRANGE.pgc @@ -1,7 +1,9 @@ /* - * LG_DEPTRANGE.pgc - lg 모듈 서비스 LG_DEPTRANGE (one service = one file). + * LG_DEPTRANGE.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_DEPTRANGE. * 부문 기간 금액범위: 전표유형의 기간 내 분개 최소/최대 금액과 건수를 조회. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_DEPTRANGE.h" @@ -13,6 +15,82 @@ void LG_DEPTRANGE(TPSVCINFO *p) char h_type[16], h_from[16], h_end[16]; long h_min, h_max, h_cnt; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_DEPTRANGE"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "금액 음수 불가", 0L); + userlog("LG_DEPTRANGE 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_DEPTRANGE 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_DEPTRANGE 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_DEPTRANGE 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(debit_total,0), coalesce(credit_total,0), coalesce(balance,0) + INTO :gx_prev, :gx_amt, :gx_bal FROM lg_account_bal + WHERE acct = 'RECV' AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_prev = 0; gx_amt = 0; gx_bal = 0; } + EXEC SQL SELECT coalesce(count(DISTINCT acct),0) INTO :gx_max + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_max = 0; + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(ADJ/FEE_INC) */ + userlog("LG_DEPTRANGE 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + gx_diff = gx_dr - gx_cr; + if (gx_diff != 0) { + userlog("LG_DEPTRANGE 당일 대차 불균형 diff=%ld", gx_diff); + } else if (gx_max == 0) { + gx_rc = 3; /* 무거래일 */ + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_DEPTRANGE 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_KEY1, vtype, sizeof(vtype)); if (vtype[0] == 0) strcpy(vtype, "POST"); gets_(b, T_BIZDATE, endd, sizeof(endd)); gets_(b, T_STR1, fromd, sizeof(fromd)); if (fromd[0] == 0) strcpy(fromd, endd); @@ -27,6 +105,32 @@ void LG_DEPTRANGE(TPSVCINFO *p) setl(b, T_AMT2, h_min); setl(b, T_AMT3, h_max); setl(b, T_COUNT, h_cnt); setl(b, T_DELTA, h_max - h_min); userlog("LG_DEPTRANGE %s %s~%s 최소 %ld 최대 %ld", vtype, fromd, endd, h_min, h_max); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_DEPTRANGE band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_DEPTRANGE 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(debit_total),0), coalesce(sum(credit_total),0) + INTO :gx_dr, :gx_cr FROM lg_account_bal WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_diff = gx_dr - gx_cr; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_DEPTRANGE 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_DEPTSUM.pgc b/app/src/lg/svc/LG_DEPTSUM.pgc index 3c5b2b9..45c40ff 100644 --- a/app/src/lg/svc/LG_DEPTSUM.pgc +++ b/app/src/lg/svc/LG_DEPTSUM.pgc @@ -1,7 +1,9 @@ /* - * LG_DEPTSUM.pgc - lg 모듈 서비스 LG_DEPTSUM (one service = one file). + * LG_DEPTSUM.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_DEPTSUM. * 부문별 차변 합계 최대 유형 탐색: 전표유형별 차변합을 순회해 최대 부문을 선별. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_DEPTSUM.h" @@ -14,6 +16,74 @@ void LG_DEPTSUM(TPSVCINFO *p) char h_bizdate[16], h_type[16]; long h_dr; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_DEPTSUM"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "분개액 음수 불가", 0L); + userlog("LG_DEPTSUM 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_DEPTSUM 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_DEPTSUM 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_DEPTSUM 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(ADJ/UNEARNED_REV) */ + userlog("LG_DEPTSUM 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_DEPTSUM 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_DEPTSUM 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; EXEC SQL DECLARE ds_cur CURSOR FOR @@ -33,6 +103,32 @@ void LG_DEPTSUM(TPSVCINFO *p) Bchg(b, T_KEY1, 0, maxtype, 0L); setl(b, T_AMT2, total); setl(b, T_AMT3, (maxval < 0) ? 0 : maxval); userlog("LG_DEPTSUM %s 최대부문 %s 차변 %ld", bizdate, maxtype, maxval); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_DEPTSUM band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_DEPTSUM 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_DEPTSUM 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_DIFFCHK.pgc b/app/src/lg/svc/LG_DIFFCHK.pgc index 64bc872..811c6d9 100644 --- a/app/src/lg/svc/LG_DIFFCHK.pgc +++ b/app/src/lg/svc/LG_DIFFCHK.pgc @@ -1,8 +1,9 @@ /* - * LG_DIFFCHK.pgc - lg 모듈 서비스 LG_DIFFCHK (one service = one file). + * LG_DIFFCHK.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_DIFFCHK. * 당일 전체 대차 차액 점검(총차변-총대변). - * 이전 인라인 lg_svr.pgc 에서 분리; 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). - * lg_svr 가 tpsvrinit 에서 tpadvertise 로 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_DIFFCHK.h" @@ -14,6 +15,82 @@ void LG_DIFFCHK(TPSVCINFO *p) long h_dr, h_cr; char h_bizdate[16]; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_DIFFCHK"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "분개액 음수 불가", 0L); + userlog("LG_DIFFCHK 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_DIFFCHK 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_DIFFCHK 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_DIFFCHK 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(debit_total,0), coalesce(credit_total,0), coalesce(balance,0) + INTO :gx_prev, :gx_amt, :gx_bal FROM lg_account_bal + WHERE acct = 'RECV' AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_prev = 0; gx_amt = 0; gx_bal = 0; } + EXEC SQL SELECT coalesce(count(DISTINCT acct),0) INTO :gx_max + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_max = 0; + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(SUSP/RECV_ACCR) */ + userlog("LG_DIFFCHK 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + gx_diff = gx_dr - gx_cr; + if (gx_diff != 0) { + userlog("LG_DIFFCHK 당일 대차 불균형 diff=%ld", gx_diff); + } else if (gx_max == 0) { + gx_rc = 3; /* 무거래일 */ + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_DIFFCHK 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; EXEC SQL SELECT coalesce(sum(CASE WHEN dc = 'D' THEN amount ELSE 0 END), 0), @@ -24,6 +101,32 @@ void LG_DIFFCHK(TPSVCINFO *p) setl(b, T_AMT3, h_cr); setl(b, T_DELTA, h_dr - h_cr); setl(b, T_RC, (h_dr == h_cr) ? 0 : 1); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_DIFFCHK band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_DIFFCHK 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(debit_total),0), coalesce(sum(credit_total),0) + INTO :gx_dr, :gx_cr FROM lg_account_bal WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_diff = gx_dr - gx_cr; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_DIFFCHK 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_DOUBLE.pgc b/app/src/lg/svc/LG_DOUBLE.pgc index fc95a80..38ba5fc 100644 --- a/app/src/lg/svc/LG_DOUBLE.pgc +++ b/app/src/lg/svc/LG_DOUBLE.pgc @@ -1,8 +1,9 @@ /* - * LG_DOUBLE.pgc - lg 모듈 서비스 LG_DOUBLE (one service = one file). + * LG_DOUBLE.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_DOUBLE. * 신규 전표 즉시 전기(posted): 차변/대변 + 양 계정잔액 반영. - * 이전 인라인 lg_svr.pgc 에서 분리; 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). - * lg_svr 가 tpsvrinit 에서 tpadvertise 로 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_DOUBLE.h" @@ -11,6 +12,74 @@ void LG_DOUBLE(TPSVCINFO *p) UBFH *b = (UBFH *)p->data; char dacct[32], cacct[32], bizdate[16], merch[64]; long vid, amount; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_DOUBLE"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_DOUBLE 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_DOUBLE 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_DOUBLE 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_DOUBLE 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(BADDEBT_EXP/UNEARNED_REV) */ + userlog("LG_DOUBLE 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_DOUBLE 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_DOUBLE 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_KEY1, dacct, sizeof(dacct)); if (dacct[0] == 0) strcpy(dacct, "RECV"); gets_(b, T_KEY2, cacct, sizeof(cacct)); if (cacct[0] == 0) strcpy(cacct, "MERCH_PAY"); @@ -28,6 +97,32 @@ void LG_DOUBLE(TPSVCINFO *p) setl(b, T_ID1, vid); Bchg(b, T_STATUS, 0, "P", 0L); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_DOUBLE band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_DOUBLE 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_DOUBLE 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_DUPSCAN.pgc b/app/src/lg/svc/LG_DUPSCAN.pgc index 3826a76..a39bef0 100644 --- a/app/src/lg/svc/LG_DUPSCAN.pgc +++ b/app/src/lg/svc/LG_DUPSCAN.pgc @@ -1,9 +1,10 @@ /* - * LG_DUPSCAN.pgc - lg 모듈 서비스 LG_DUPSCAN (one service = one file). + * LG_DUPSCAN.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_DUPSCAN. * 중복 분개 스캔: 당일 동일 (전표, 계정, 차대, 금액) 조합이 2건 이상인 중복 * 그룹을 GROUP BY/HAVING 으로 찾아 그룹 수·중복 초과 라인 수·최대 중복 - * 전표를 반환한다 (이중기표 탐지). - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_DUPSCAN.h" @@ -16,6 +17,74 @@ void LG_DUPSCAN(TPSVCINFO *p) char h_bizdate[16]; long h_vid, h_n; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_DUPSCAN"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "분개액 음수 불가", 0L); + userlog("LG_DUPSCAN 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_DUPSCAN 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_DUPSCAN 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_DUPSCAN 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(RECV/ADJ) */ + userlog("LG_DUPSCAN 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_DUPSCAN 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_DUPSCAN 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; @@ -42,6 +111,32 @@ void LG_DUPSCAN(TPSVCINFO *p) setl(b, T_ID1, worstvid); setl(b, T_ARG2, worstn); Bchg(b, T_STATUS, 0, (groups == 0) ? "OK" : "NG", 0L); userlog("LG_DUPSCAN %s 중복그룹 %ld 초과라인 %ld 최다 vid=%ld(%ld건)", bizdate, groups, extra, worstvid, worstn); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_DUPSCAN band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_DUPSCAN 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_DUPSCAN 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_EQUITYMOVE.pgc b/app/src/lg/svc/LG_EQUITYMOVE.pgc index 954833b..903ff76 100644 --- a/app/src/lg/svc/LG_EQUITYMOVE.pgc +++ b/app/src/lg/svc/LG_EQUITYMOVE.pgc @@ -1,7 +1,9 @@ /* - * LG_EQUITYMOVE.pgc - lg 모듈 서비스 LG_EQUITYMOVE (one service = one file). + * LG_EQUITYMOVE.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_EQUITYMOVE. * 자본 변동: 기간 내 이익잉여금(RETAINED) 대변순액 변동을 집계. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_EQUITYMOVE.h" @@ -13,6 +15,74 @@ void LG_EQUITYMOVE(TPSVCINFO *p) char h_from[16], h_end[16]; long h_move, h_cnt; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_EQUITYMOVE"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_EQUITYMOVE 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_EQUITYMOVE 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_EQUITYMOVE 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_EQUITYMOVE 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(ACCRUED_PAY/MERCH_PAY) */ + userlog("LG_EQUITYMOVE 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_EQUITYMOVE 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_EQUITYMOVE 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, endd, sizeof(endd)); gets_(b, T_STR1, fromd, sizeof(fromd)); if (fromd[0] == 0) strcpy(fromd, endd); strncpy(h_from, fromd, sizeof(h_from)-1); h_from[sizeof(h_from)-1] = 0; @@ -24,6 +94,32 @@ void LG_EQUITYMOVE(TPSVCINFO *p) setl(b, T_NET, h_move); setl(b, T_COUNT, h_cnt); Bchg(b, T_STATUS, 0, (h_move >= 0) ? "UP" : "DOWN", 0L); userlog("LG_EQUITYMOVE %s~%s 자본변동 %ld (%ld건)", fromd, endd, h_move, h_cnt); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_EQUITYMOVE band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_EQUITYMOVE 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_EQUITYMOVE 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_EXPENSEROLL.pgc b/app/src/lg/svc/LG_EXPENSEROLL.pgc index 990d9b8..bd41510 100644 --- a/app/src/lg/svc/LG_EXPENSEROLL.pgc +++ b/app/src/lg/svc/LG_EXPENSEROLL.pgc @@ -1,7 +1,9 @@ /* - * LG_EXPENSEROLL.pgc - lg 모듈 서비스 LG_EXPENSEROLL (one service = one file). + * LG_EXPENSEROLL.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_EXPENSEROLL. * 비용 롤업: 마감일까지 비용계정(_EXP)별 차변순액을 순회해 총비용을 집계. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_EXPENSEROLL.h" @@ -14,6 +16,79 @@ void LG_EXPENSEROLL(TPSVCINFO *p) char h_end[16], h_acct[32]; long h_net; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_EXPENSEROLL"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_EXPENSEROLL 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_EXPENSEROLL 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_EXPENSEROLL 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_EXPENSEROLL 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(debit_total,0), coalesce(credit_total,0), coalesce(balance,0) + INTO :gx_prev, :gx_amt, :gx_bal FROM lg_account_bal + WHERE acct = 'RECV' AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_prev = 0; gx_amt = 0; gx_bal = 0; } + EXEC SQL SELECT coalesce(count(DISTINCT acct),0) INTO :gx_max + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_max = 0; + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(PREPAID/CASH) */ + userlog("LG_EXPENSEROLL 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + gx_diff = gx_dr - gx_cr; + if (gx_diff != 0) { + userlog("LG_EXPENSEROLL 당일 대차 불균형 diff=%ld", gx_diff); + } else if (gx_max == 0) { + gx_rc = 3; /* 무거래일 */ + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_EXPENSEROLL 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, endd, sizeof(endd)); strncpy(h_end, endd, sizeof(h_end)-1); h_end[sizeof(h_end)-1] = 0; EXEC SQL DECLARE er_cur CURSOR FOR @@ -31,6 +106,32 @@ void LG_EXPENSEROLL(TPSVCINFO *p) EXEC SQL CLOSE er_cur; setl(b, T_COUNT, accts); setl(b, T_NET, total); userlog("LG_EXPENSEROLL %s 비용계정 %ld개 총비용 %ld", endd, accts, total); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_EXPENSEROLL band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_EXPENSEROLL 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(debit_total),0), coalesce(sum(credit_total),0) + INTO :gx_dr, :gx_cr FROM lg_account_bal WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_diff = gx_dr - gx_cr; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_EXPENSEROLL 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_FEEACCRUE.pgc b/app/src/lg/svc/LG_FEEACCRUE.pgc index 70e96b0..33070f6 100644 --- a/app/src/lg/svc/LG_FEEACCRUE.pgc +++ b/app/src/lg/svc/LG_FEEACCRUE.pgc @@ -1,8 +1,10 @@ /* - * LG_FEEACCRUE.pgc - lg 모듈 서비스 LG_FEEACCRUE (one service = one file). + * LG_FEEACCRUE.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_FEEACCRUE. * 수수료 미수 계상: 대사완료(M)했지만 아직 미정산인 매입의 수수료를 * D:FEE_RECV / C:FEE_INC 미결(D) 전표로 발생주의 계상한다 (승인 후 전기). - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_FEEACCRUE.h" @@ -15,6 +17,74 @@ void LG_FEEACCRUE(TPSVCINFO *p) char h_bizdate[16]; long h_fee; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_FEEACCRUE"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "분개액 음수 불가", 0L); + userlog("LG_FEEACCRUE 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_FEEACCRUE 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_FEEACCRUE 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_FEEACCRUE 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(MERCH_PAY/MDR_EXP) */ + userlog("LG_FEEACCRUE 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_FEEACCRUE 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_FEEACCRUE 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; @@ -35,6 +105,32 @@ void LG_FEEACCRUE(TPSVCINFO *p) setl(b, T_ID1, vid); setl(b, T_FEE, accrue); Bchg(b, T_STATUS, 0, "D", 0L); userlog("LG_FEEACCRUE vid=%ld 미수수수료 %ld 계상(미결)", vid, accrue); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_FEEACCRUE band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_FEEACCRUE 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_FEEACCRUE 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_FEEPOST.pgc b/app/src/lg/svc/LG_FEEPOST.pgc index caf39ce..a3bdf37 100644 --- a/app/src/lg/svc/LG_FEEPOST.pgc +++ b/app/src/lg/svc/LG_FEEPOST.pgc @@ -1,8 +1,9 @@ /* - * LG_FEEPOST.pgc - lg 모듈 서비스 LG_FEEPOST (one service = one file). + * LG_FEEPOST.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_FEEPOST. * 수수료수익 전기: 차변 RECV=fee, 대변 FEE_INC=fee. - * 이전 인라인 lg_svr.pgc 에서 분리; 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). - * lg_svr 가 tpsvrinit 에서 tpadvertise 로 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_FEEPOST.h" @@ -11,6 +12,84 @@ void LG_FEEPOST(TPSVCINFO *p) UBFH *b = (UBFH *)p->data; char bizdate[16], merch[64]; long vid, fee; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_FEEPOST"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "분개액 음수 불가", 0L); + userlog("LG_FEEPOST 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_FEEPOST 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_FEEPOST 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_FEEPOST 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(*),0), coalesce(max(voucher_id),0) + INTO :gx_cnt, :gx_max FROM lg_voucher WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + if (gx_in_id > 0) { + EXEC SQL SELECT coalesce(status,'?'), coalesce(ref_id,0) + INTO :gx_mst, :gx_prev FROM lg_voucher WHERE voucher_id = :gx_in_id; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_FEEPOST 전표 미존재 vid=%ld", gx_in_id); + } + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(CASH/BADDEBT_EXP) */ + userlog("LG_FEEPOST 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "S") == 0) { + userlog("LG_FEEPOST 마감연계(S) 전표 - 정정은 역분개 권고", gx_svc); + } else if (gx_mst[0] && strcmp(gx_mst, "X") == 0) { + gx_rc = 2; /* 취소 전표 - 재개 필요 */ + } else if (gx_dr != gx_cr) { + gx_diff = gx_dr - gx_cr; + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_FEEPOST 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); gets_(b, T_MERCHANT, merch, sizeof(merch)); fee = getl(b, T_FEE); @@ -20,6 +99,31 @@ void LG_FEEPOST(TPSVCINFO *p) if (lgdb_insert_line(vid, "RECV", "D", fee, merch, bizdate, "posted") < 0) FAIL(b); if (lgdb_insert_line(vid, "FEE_INC", "C", fee, merch, bizdate, "posted") < 0) FAIL(b); setl(b, T_ID1, vid); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_FEEPOST band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_FEEPOST 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0) INTO :gx_cnt + FROM lg_voucher WHERE biz_date = :gx_bd AND status = 'P'; + if (sqlca.sqlcode == 100) gx_cnt = 0; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_FEEPOST 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_FEERECOG.pgc b/app/src/lg/svc/LG_FEERECOG.pgc index 0f7ef9c..4fd53bc 100644 --- a/app/src/lg/svc/LG_FEERECOG.pgc +++ b/app/src/lg/svc/LG_FEERECOG.pgc @@ -1,8 +1,10 @@ /* - * LG_FEERECOG.pgc - lg 모듈 서비스 LG_FEERECOG (one service = one file). + * LG_FEERECOG.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_FEERECOG. * 수수료 수익 인식: 당일 정산완료(S) 매입 수수료 합계를 * D:RECV / C:FEE_INC 균형 전표(FEE)로 전기하고 양 계정잔액에 반영한다. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_FEERECOG.h" @@ -15,6 +17,74 @@ void LG_FEERECOG(TPSVCINFO *p) char h_bizdate[16]; long h_fee, h_n; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_FEERECOG"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_FEERECOG 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_FEERECOG 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_FEERECOG 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_FEERECOG 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(CASH_RECOV/BADDEBT_RCV) */ + userlog("LG_FEERECOG 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_FEERECOG 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_FEERECOG 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; @@ -37,6 +107,32 @@ void LG_FEERECOG(TPSVCINFO *p) setl(b, T_ID1, vid); setl(b, T_FEE, fee); setl(b, T_COUNT, h_n); Bchg(b, T_STATUS, 0, "P", 0L); userlog("LG_FEERECOG vid=%ld %s 수수료수익 %ld (%ld건, 차=대 균형)", vid, bizdate, fee, h_n); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_FEERECOG band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_FEERECOG 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_FEERECOG 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_FEESPLIT.pgc b/app/src/lg/svc/LG_FEESPLIT.pgc index 1724282..b46170d 100644 --- a/app/src/lg/svc/LG_FEESPLIT.pgc +++ b/app/src/lg/svc/LG_FEESPLIT.pgc @@ -1,9 +1,10 @@ /* - * LG_FEESPLIT.pgc - lg 모듈 서비스 LG_FEESPLIT (one service = one file). + * LG_FEESPLIT.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_FEESPLIT. * 수수료 부문 배부: 당일 FEE_INC 대변 합계를 배부율(T_ARG1 bps)로 부문 A/B 에 * 나눠 D:FEE_INC / C:FEE_DEPT_A + C:FEE_DEPT_B 3라인 균형 전표로 배부한다. - * 나머지(단수차)는 B 부문에 귀속시켜 차변합=대변합을 보장한다. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_FEESPLIT.h" @@ -17,6 +18,74 @@ void LG_FEESPLIT(TPSVCINFO *p) char h_bizdate[16]; long h_total; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_FEESPLIT"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "분개액 음수 불가", 0L); + userlog("LG_FEESPLIT 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_FEESPLIT 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_FEESPLIT 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_FEESPLIT 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(CASH/PREPAID) */ + userlog("LG_FEESPLIT 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_FEESPLIT 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_FEESPLIT 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); if (bps <= 0 || bps >= 10000) bps = 5000; /* 기본 50:50 */ @@ -42,6 +111,32 @@ void LG_FEESPLIT(TPSVCINFO *p) setl(b, T_ID1, vid); setl(b, T_GROSS, total); setl(b, T_AMT2, parta); setl(b, T_AMT3, partb); userlog("LG_FEESPLIT vid=%ld 총 %ld -> A %ld / B %ld (bps=%ld, 균형)", vid, total, parta, partb, bps); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_FEESPLIT band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_FEESPLIT 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_FEESPLIT 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_HALFCLOSE.pgc b/app/src/lg/svc/LG_HALFCLOSE.pgc index 25d2b67..a68e42e 100644 --- a/app/src/lg/svc/LG_HALFCLOSE.pgc +++ b/app/src/lg/svc/LG_HALFCLOSE.pgc @@ -1,7 +1,9 @@ /* - * LG_HALFCLOSE.pgc - lg 모듈 서비스 LG_HALFCLOSE (one service = one file). + * LG_HALFCLOSE.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_HALFCLOSE. * 반기 결산 분개: 반기 FEE_INC 순액을 이익잉여금(RETAINED)으로 균형 대체. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_HALFCLOSE.h" @@ -14,6 +16,74 @@ void LG_HALFCLOSE(TPSVCINFO *p) char h_from[16], h_end[16]; long h_net; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_HALFCLOSE"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_HALFCLOSE 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_HALFCLOSE 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_HALFCLOSE 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_HALFCLOSE 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(BADDEBT_EXP/BADDEBT_RCV) */ + userlog("LG_HALFCLOSE 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_HALFCLOSE 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_HALFCLOSE 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, endd, sizeof(endd)); gets_(b, T_STR1, fromd, sizeof(fromd)); if (fromd[0] == 0) strcpy(fromd, endd); strncpy(h_from, fromd, sizeof(h_from)-1); h_from[sizeof(h_from)-1] = 0; @@ -37,6 +107,32 @@ void LG_HALFCLOSE(TPSVCINFO *p) } setl(b, T_ID1, vid); setl(b, T_NET, net); userlog("LG_HALFCLOSE %s~%s 반기순익 %ld vid=%ld", fromd, endd, net, vid); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_HALFCLOSE band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_HALFCLOSE 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_HALFCLOSE 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_INCOMEROLL.pgc b/app/src/lg/svc/LG_INCOMEROLL.pgc index c5315c1..1b4239e 100644 --- a/app/src/lg/svc/LG_INCOMEROLL.pgc +++ b/app/src/lg/svc/LG_INCOMEROLL.pgc @@ -1,7 +1,9 @@ /* - * LG_INCOMEROLL.pgc - lg 모듈 서비스 LG_INCOMEROLL (one service = one file). + * LG_INCOMEROLL.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_INCOMEROLL. * 수익 롤업: 마감일까지 수익계정(_INC)별 대변순액을 순회해 총수익을 집계. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_INCOMEROLL.h" @@ -14,6 +16,79 @@ void LG_INCOMEROLL(TPSVCINFO *p) char h_end[16], h_acct[32]; long h_net; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_INCOMEROLL"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "금액 음수 불가", 0L); + userlog("LG_INCOMEROLL 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_INCOMEROLL 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_INCOMEROLL 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_INCOMEROLL 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(debit_total,0), coalesce(credit_total,0), coalesce(balance,0) + INTO :gx_prev, :gx_amt, :gx_bal FROM lg_account_bal + WHERE acct = 'RECV' AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_prev = 0; gx_amt = 0; gx_bal = 0; } + EXEC SQL SELECT coalesce(count(DISTINCT acct),0) INTO :gx_max + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_max = 0; + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(SUSP/CASH) */ + userlog("LG_INCOMEROLL 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + gx_diff = gx_dr - gx_cr; + if (gx_diff != 0) { + userlog("LG_INCOMEROLL 당일 대차 불균형 diff=%ld", gx_diff); + } else if (gx_max == 0) { + gx_rc = 3; /* 무거래일 */ + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_INCOMEROLL 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, endd, sizeof(endd)); strncpy(h_end, endd, sizeof(h_end)-1); h_end[sizeof(h_end)-1] = 0; EXEC SQL DECLARE ir_cur CURSOR FOR @@ -31,6 +106,32 @@ void LG_INCOMEROLL(TPSVCINFO *p) EXEC SQL CLOSE ir_cur; setl(b, T_COUNT, accts); setl(b, T_NET, total); userlog("LG_INCOMEROLL %s 수익계정 %ld개 총수익 %ld", endd, accts, total); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_INCOMEROLL band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_INCOMEROLL 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(debit_total),0), coalesce(sum(credit_total),0) + INTO :gx_dr, :gx_cr FROM lg_account_bal WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_diff = gx_dr - gx_cr; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_INCOMEROLL 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_INQUIRY.pgc b/app/src/lg/svc/LG_INQUIRY.pgc index b460a50..0e07417 100644 --- a/app/src/lg/svc/LG_INQUIRY.pgc +++ b/app/src/lg/svc/LG_INQUIRY.pgc @@ -1,8 +1,9 @@ /* - * LG_INQUIRY.pgc - lg 모듈 서비스 LG_INQUIRY (one service = one file). + * LG_INQUIRY.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_INQUIRY. * 전표 상태 조회(dbio). - * 이전 인라인 lg_svr.pgc 에서 분리; 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). - * lg_svr 가 tpsvrinit 에서 tpadvertise 로 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_INQUIRY.h" @@ -11,8 +12,102 @@ void LG_INQUIRY(TPSVCINFO *p) UBFH *b = (UBFH *)p->data; long vid = getl(b, T_ID1); char st[4]; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_INQUIRY"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "분개액 음수 불가", 0L); + userlog("LG_INQUIRY 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_INQUIRY 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_INQUIRY 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_INQUIRY 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(FEE_INC/PREPAID) */ + userlog("LG_INQUIRY 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_INQUIRY 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_INQUIRY 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); if (lgdb_get_voucher_status(vid, st) < 0) FAIL(b); Bchg(b, T_STATUS, 0, st, 0L); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_INQUIRY band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_INQUIRY 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_INQUIRY 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_JOURNAL.pgc b/app/src/lg/svc/LG_JOURNAL.pgc index 43b6b7b..b6701d0 100644 --- a/app/src/lg/svc/LG_JOURNAL.pgc +++ b/app/src/lg/svc/LG_JOURNAL.pgc @@ -1,8 +1,9 @@ /* - * LG_JOURNAL.pgc - lg 모듈 서비스 LG_JOURNAL (one service = one file). + * LG_JOURNAL.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_JOURNAL. * 당일 분개(원장 라인) 건수 조회. - * 이전 인라인 lg_svr.pgc 에서 분리; 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). - * lg_svr 가 tpsvrinit 에서 tpadvertise 로 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_JOURNAL.h" @@ -14,11 +15,105 @@ void LG_JOURNAL(TPSVCINFO *p) long h_cnt; char h_bizdate[16]; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_JOURNAL"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_JOURNAL 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_JOURNAL 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_JOURNAL 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_JOURNAL 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(CASH_RECOV/RETAINED) */ + userlog("LG_JOURNAL 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_JOURNAL 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_JOURNAL 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; EXEC SQL SELECT count(*) INTO :h_cnt FROM lg_ledger_line WHERE biz_date = :h_bizdate; if (sqlca.sqlcode < 0) FAIL(b); setl(b, T_COUNT, h_cnt); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_JOURNAL band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_JOURNAL 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_JOURNAL 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_LEDGSETTLE.pgc b/app/src/lg/svc/LG_LEDGSETTLE.pgc index 5cdb262..650860c 100644 --- a/app/src/lg/svc/LG_LEDGSETTLE.pgc +++ b/app/src/lg/svc/LG_LEDGSETTLE.pgc @@ -1,7 +1,9 @@ /* - * LG_LEDGSETTLE.pgc - lg 모듈 서비스 LG_LEDGSETTLE (one service = one file). + * LG_LEDGSETTLE.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_LEDGSETTLE. * 원장-정산 대사: base 원장(ledger) SETTLE 합계와 원장라인 지급 합계를 대조. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_LEDGSETTLE.h" @@ -13,6 +15,85 @@ void LG_LEDGSETTLE(TPSVCINFO *p) char h_bizdate[16]; long h_base, h_line; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_LEDGSETTLE"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "금액 음수 불가", 0L); + userlog("LG_LEDGSETTLE 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_LEDGSETTLE 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_LEDGSETTLE 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_LEDGSETTLE 라인집계 조회 경고 [%d]", sqlca.sqlcode); + if (gx_mid[0]) { + EXEC SQL SELECT coalesce(mdr_bps,0), coalesce(daily_limit,0), coalesce(status,'?') + INTO :gx_mbps, :gx_lim, :gx_mst FROM merchant WHERE merchant_id = :gx_mid; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_LEDGSETTLE 가맹점 미등록 mid=%s (정책 기본값)", gx_mid); + } + EXEC SQL SELECT coalesce(balance,0) INTO :gx_bal + FROM lg_merch_bal WHERE merchant_id = :gx_mid AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_bal = 0; + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(RECV/MERCH_PAY) */ + userlog("LG_LEDGSETTLE 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "CLOSED") == 0) { + userlog("LG_LEDGSETTLE 해지가맹점 mid=%s - 잔액정리 경로", gx_mid); + } else if (gx_mbps > 30) { + gx_rc = 1; /* 고 MDR 가맹점 - 수수료 배분 재검토 */ + } + if (gx_lim > 0 && gx_amt > gx_lim) { + userlog("LG_LEDGSETTLE 일한도 초과소지 amt=%ld limit=%ld", gx_amt, gx_lim); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_LEDGSETTLE 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; EXEC SQL SELECT coalesce(sum(amount), 0) INTO :h_base FROM ledger @@ -25,6 +106,36 @@ void LG_LEDGSETTLE(TPSVCINFO *p) setl(b, T_DELTA, h_base - h_line); Bchg(b, T_STATUS, 0, (h_base == h_line) ? "OK" : "NG", 0L); userlog("LG_LEDGSETTLE %s base %ld line %ld", bizdate, h_base, h_line); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_LEDGSETTLE band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_LEDGSETTLE 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + if (gx_mid[0]) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0) + INTO :gx_dr, :gx_cr FROM lg_ledger_line + WHERE merchant_id = :gx_mid AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_bal = gx_dr - gx_cr; + } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_LEDGSETTLE 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_LINECOUNT.pgc b/app/src/lg/svc/LG_LINECOUNT.pgc index b7c9e71..7659b3d 100644 --- a/app/src/lg/svc/LG_LINECOUNT.pgc +++ b/app/src/lg/svc/LG_LINECOUNT.pgc @@ -1,8 +1,10 @@ /* - * LG_LINECOUNT.pgc - lg 모듈 서비스 LG_LINECOUNT (one service = one file). + * LG_LINECOUNT.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_LINECOUNT. * 원장 라인 집계: 당일 라인의 posted/closed 상태별 건수, 관련 전표 수, * 전표당 평균 라인 수(x100 고정소수)를 반환한다. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_LINECOUNT.h" @@ -15,6 +17,77 @@ void LG_LINECOUNT(TPSVCINFO *p) char h_bizdate[16]; long h_posted, h_closed, h_vouchers; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_LINECOUNT"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_LINECOUNT 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_LINECOUNT 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_LINECOUNT 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_LINECOUNT 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(RETAINED/ACCRUED_PAY) */ + userlog("LG_LINECOUNT 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_LINECOUNT 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_LINECOUNT 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; @@ -31,6 +104,32 @@ void LG_LINECOUNT(TPSVCINFO *p) setl(b, T_ARG1, h_posted); setl(b, T_ARG2, h_closed); setl(b, T_COUNT, h_vouchers); setl(b, T_ARG3, per100); userlog("LG_LINECOUNT %s posted=%ld closed=%ld 전표=%ld (평균 x100=%ld)", bizdate, h_posted, h_closed, h_vouchers, per100); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_LINECOUNT band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_LINECOUNT 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_LINECOUNT 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_MASKINQ.pgc b/app/src/lg/svc/LG_MASKINQ.pgc index 4af1e89..28c69d2 100644 --- a/app/src/lg/svc/LG_MASKINQ.pgc +++ b/app/src/lg/svc/LG_MASKINQ.pgc @@ -1,8 +1,10 @@ /* - * LG_MASKINQ.pgc - lg 모듈 서비스 LG_MASKINQ (one service = one file). + * LG_MASKINQ.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_MASKINQ. * 원장 마스킹 조회: 라인 ID(T_ID1)의 원장 라인을 가맹점 ID 마스킹(앞 2자리 * 노출 + '****') 상태로 조회 — 개인정보 최소노출 조회 채널용. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_MASKINQ.h" @@ -14,6 +16,77 @@ void LG_MASKINQ(TPSVCINFO *p) long h_lid, h_amt, h_vid; char h_acct[32], h_dc[4], h_masked[72], h_date[16]; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_MASKINQ"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "금액 음수 불가", 0L); + userlog("LG_MASKINQ 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_MASKINQ 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_MASKINQ 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_MASKINQ 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(MDR_EXP/ACCRUED_PAY) */ + userlog("LG_MASKINQ 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_MASKINQ 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_MASKINQ 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); h_lid = lid; EXEC SQL SELECT voucher_id, acct, dc, amount, @@ -31,6 +104,32 @@ void LG_MASKINQ(TPSVCINFO *p) Bchg(b, T_MERCHANT, 0, h_masked, 0L); Bchg(b, T_BIZDATE, 0, h_date, 0L); userlog("LG_MASKINQ line=%ld 마스킹 조회 (%s)", lid, h_masked); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_MASKINQ band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_MASKINQ 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_MASKINQ 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_MEMOAUDIT.pgc b/app/src/lg/svc/LG_MEMOAUDIT.pgc index 7b5ed6b..109ecd6 100644 --- a/app/src/lg/svc/LG_MEMOAUDIT.pgc +++ b/app/src/lg/svc/LG_MEMOAUDIT.pgc @@ -1,8 +1,10 @@ /* - * LG_MEMOAUDIT.pgc - lg 모듈 서비스 LG_MEMOAUDIT (one service = one file). + * LG_MEMOAUDIT.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_MEMOAUDIT. * 전표 메모 감사: 당일 전기(P) 전표 중 메모가 비어 있는 건수와 표본 전표 ID, * 전체 대비 비율(bps)을 반환한다 (기표 규정 준수 감사). - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_MEMOAUDIT.h" @@ -15,6 +17,84 @@ void LG_MEMOAUDIT(TPSVCINFO *p) char h_bizdate[16]; long h_empty, h_all, h_sample; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_MEMOAUDIT"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "분개액 음수 불가", 0L); + userlog("LG_MEMOAUDIT 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_MEMOAUDIT 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_MEMOAUDIT 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_MEMOAUDIT 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(*),0), coalesce(max(voucher_id),0) + INTO :gx_cnt, :gx_max FROM lg_voucher WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + if (gx_in_id > 0) { + EXEC SQL SELECT coalesce(status,'?'), coalesce(ref_id,0) + INTO :gx_mst, :gx_prev FROM lg_voucher WHERE voucher_id = :gx_in_id; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_MEMOAUDIT 전표 미존재 vid=%ld", gx_in_id); + } + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(MERCH_PAY/BADDEBT_EXP) */ + userlog("LG_MEMOAUDIT 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "S") == 0) { + userlog("LG_MEMOAUDIT 마감연계(S) 전표 - 정정은 역분개 권고", gx_svc); + } else if (gx_mst[0] && strcmp(gx_mst, "X") == 0) { + gx_rc = 2; /* 취소 전표 - 재개 필요 */ + } else if (gx_dr != gx_cr) { + gx_diff = gx_dr - gx_cr; + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_MEMOAUDIT 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; @@ -31,6 +111,31 @@ void LG_MEMOAUDIT(TPSVCINFO *p) setl(b, T_ID1, h_sample); setl(b, T_ARG2, bps); Bchg(b, T_STATUS, 0, (h_empty == 0) ? "OK" : "NG", 0L); userlog("LG_MEMOAUDIT %s 전기 %ld건 중 메모누락 %ld건 (%ldbps)", bizdate, h_all, h_empty, bps); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_MEMOAUDIT band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_MEMOAUDIT 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0) INTO :gx_cnt + FROM lg_voucher WHERE biz_date = :gx_bd AND status = 'P'; + if (sqlca.sqlcode == 100) gx_cnt = 0; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_MEMOAUDIT 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_MERCHBAL.pgc b/app/src/lg/svc/LG_MERCHBAL.pgc index 1f1acf8..a0dd9f0 100644 --- a/app/src/lg/svc/LG_MERCHBAL.pgc +++ b/app/src/lg/svc/LG_MERCHBAL.pgc @@ -1,8 +1,9 @@ /* - * LG_MERCHBAL.pgc - lg 모듈 서비스 LG_MERCHBAL (one service = one file). + * LG_MERCHBAL.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_MERCHBAL. * 가맹점 잔액 재집계(dbio). - * 이전 인라인 lg_svr.pgc 에서 분리; 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). - * lg_svr 가 tpsvrinit 에서 tpadvertise 로 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_MERCHBAL.h" @@ -10,10 +11,122 @@ void LG_MERCHBAL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char merch[64], bizdate[16]; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_MERCHBAL"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "대상액 음수 불가", 0L); + userlog("LG_MERCHBAL 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_MERCHBAL 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_MERCHBAL 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_MERCHBAL 라인집계 조회 경고 [%d]", sqlca.sqlcode); + if (gx_mid[0]) { + EXEC SQL SELECT coalesce(mdr_bps,0), coalesce(daily_limit,0), coalesce(status,'?') + INTO :gx_mbps, :gx_lim, :gx_mst FROM merchant WHERE merchant_id = :gx_mid; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_MERCHBAL 가맹점 미등록 mid=%s (정책 기본값)", gx_mid); + } + EXEC SQL SELECT coalesce(balance,0) INTO :gx_bal + FROM lg_merch_bal WHERE merchant_id = :gx_mid AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_bal = 0; + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(BADDEBT_EXP/ACCRUED_PAY) */ + userlog("LG_MERCHBAL 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "CLOSED") == 0) { + userlog("LG_MERCHBAL 해지가맹점 mid=%s - 잔액정리 경로", gx_mid); + } else if (gx_mbps > 30) { + gx_rc = 1; /* 고 MDR 가맹점 - 수수료 배분 재검토 */ + } + if (gx_lim > 0 && gx_amt > gx_lim) { + userlog("LG_MERCHBAL 일한도 초과소지 amt=%ld limit=%ld", gx_amt, gx_lim); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_MERCHBAL 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); if (merch[0] == 0) FAIL(b); if (lgdb_upsert_merch_bal(merch, bizdate) < 0) FAIL(b); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_MERCHBAL band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_MERCHBAL 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + if (gx_mid[0]) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0) + INTO :gx_dr, :gx_cr FROM lg_ledger_line + WHERE merchant_id = :gx_mid AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_bal = gx_dr - gx_cr; + } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_MERCHBAL 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_MERCHCERT.pgc b/app/src/lg/svc/LG_MERCHCERT.pgc index 8f78bb3..fff7a9a 100644 --- a/app/src/lg/svc/LG_MERCHCERT.pgc +++ b/app/src/lg/svc/LG_MERCHCERT.pgc @@ -1,7 +1,9 @@ /* - * LG_MERCHCERT.pgc - lg 모듈 서비스 LG_MERCHCERT (one service = one file). + * LG_MERCHCERT.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_MERCHCERT. * 가맹점 잔액증명: 가맹점잔액 기록치와 라인 재계산치를 대조해 증명(OK/NG). - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_MERCHCERT.h" @@ -14,6 +16,88 @@ void LG_MERCHCERT(TPSVCINFO *p) char h_merch[64], h_bizdate[16]; long h_bal, h_dr, h_cr; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_MERCHCERT"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "분개액 음수 불가", 0L); + userlog("LG_MERCHCERT 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_MERCHCERT 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_MERCHCERT 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_MERCHCERT 라인집계 조회 경고 [%d]", sqlca.sqlcode); + if (gx_mid[0]) { + EXEC SQL SELECT coalesce(mdr_bps,0), coalesce(daily_limit,0), coalesce(status,'?') + INTO :gx_mbps, :gx_lim, :gx_mst FROM merchant WHERE merchant_id = :gx_mid; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_MERCHCERT 가맹점 미등록 mid=%s (정책 기본값)", gx_mid); + } + EXEC SQL SELECT coalesce(balance,0) INTO :gx_bal + FROM lg_merch_bal WHERE merchant_id = :gx_mid AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_bal = 0; + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(RECV_ACCR/BADDEBT_EXP) */ + userlog("LG_MERCHCERT 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "CLOSED") == 0) { + userlog("LG_MERCHCERT 해지가맹점 mid=%s - 잔액정리 경로", gx_mid); + } else if (gx_mbps > 30) { + gx_rc = 1; /* 고 MDR 가맹점 - 수수료 배분 재검토 */ + } + if (gx_lim > 0 && gx_amt > gx_lim) { + userlog("LG_MERCHCERT 일한도 초과소지 amt=%ld limit=%ld", gx_amt, gx_lim); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_MERCHCERT 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_MERCHANT, merch, sizeof(merch)); if (merch[0] == 0) strcpy(merch, "M0001"); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; @@ -33,6 +117,36 @@ void LG_MERCHCERT(TPSVCINFO *p) setl(b, T_DELTA, recorded - recomputed); Bchg(b, T_STATUS, 0, (recorded == recomputed) ? "OK" : "NG", 0L); userlog("LG_MERCHCERT %s %s 기록 %ld 재계산 %ld", merch, bizdate, recorded, recomputed); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_MERCHCERT band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_MERCHCERT 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + if (gx_mid[0]) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0) + INTO :gx_dr, :gx_cr FROM lg_ledger_line + WHERE merchant_id = :gx_mid AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_bal = gx_dr - gx_cr; + } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_MERCHCERT 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_MERCHPROOF.pgc b/app/src/lg/svc/LG_MERCHPROOF.pgc index 28a6bb1..4481fba 100644 --- a/app/src/lg/svc/LG_MERCHPROOF.pgc +++ b/app/src/lg/svc/LG_MERCHPROOF.pgc @@ -1,8 +1,10 @@ /* - * LG_MERCHPROOF.pgc - lg 모듈 서비스 LG_MERCHPROOF (one service = one file). + * LG_MERCHPROOF.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_MERCHPROOF. * 가맹점 잔액 증명: 가맹점 잔액 테이블(lg_merch_bal)의 차변/대변 누계와 * 원장 라인 재집계를 항목별(차변·대변·잔액)로 대조해 3중 검증 결과를 낸다. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_MERCHPROOF.h" @@ -15,6 +17,85 @@ void LG_MERCHPROOF(TPSVCINFO *p) char h_merch[64], h_bizdate[16]; long h_tdr, h_tcr, h_tbal, h_ldr, h_lcr; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_MERCHPROOF"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "대상액 음수 불가", 0L); + userlog("LG_MERCHPROOF 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_MERCHPROOF 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_MERCHPROOF 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_MERCHPROOF 라인집계 조회 경고 [%d]", sqlca.sqlcode); + if (gx_mid[0]) { + EXEC SQL SELECT coalesce(mdr_bps,0), coalesce(daily_limit,0), coalesce(status,'?') + INTO :gx_mbps, :gx_lim, :gx_mst FROM merchant WHERE merchant_id = :gx_mid; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_MERCHPROOF 가맹점 미등록 mid=%s (정책 기본값)", gx_mid); + } + EXEC SQL SELECT coalesce(balance,0) INTO :gx_bal + FROM lg_merch_bal WHERE merchant_id = :gx_mid AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_bal = 0; + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(CASH_RECOV/FEE_INC) */ + userlog("LG_MERCHPROOF 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "CLOSED") == 0) { + userlog("LG_MERCHPROOF 해지가맹점 mid=%s - 잔액정리 경로", gx_mid); + } else if (gx_mbps > 30) { + gx_rc = 1; /* 고 MDR 가맹점 - 수수료 배분 재검토 */ + } + if (gx_lim > 0 && gx_amt > gx_lim) { + userlog("LG_MERCHPROOF 일한도 초과소지 amt=%ld limit=%ld", gx_amt, gx_lim); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_MERCHPROOF 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); @@ -42,6 +123,36 @@ void LG_MERCHPROOF(TPSVCINFO *p) setl(b, T_AMT4, h_tbal); setl(b, T_ARG1, mismatch); Bchg(b, T_STATUS, 0, (mismatch == 0) ? "OK" : "NG", 0L); userlog("LG_MERCHPROOF %s %s 불일치 항목 %d개", merch, bizdate, mismatch); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_MERCHPROOF band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_MERCHPROOF 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + if (gx_mid[0]) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0) + INTO :gx_dr, :gx_cr FROM lg_ledger_line + WHERE merchant_id = :gx_mid AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_bal = gx_dr - gx_cr; + } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_MERCHPROOF 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_MONCLOSEJE.pgc b/app/src/lg/svc/LG_MONCLOSEJE.pgc index 842a34d..641e7ba 100644 --- a/app/src/lg/svc/LG_MONCLOSEJE.pgc +++ b/app/src/lg/svc/LG_MONCLOSEJE.pgc @@ -1,7 +1,9 @@ /* - * LG_MONCLOSEJE.pgc - lg 모듈 서비스 LG_MONCLOSEJE (one service = one file). + * LG_MONCLOSEJE.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_MONCLOSEJE. * 월차 결산 분개: 당월 RECV 순증가를 미수수익(RECV_ACCRUED)으로 계상. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_MONCLOSEJE.h" @@ -14,6 +16,74 @@ void LG_MONCLOSEJE(TPSVCINFO *p) char h_from[16], h_end[16]; long h_net; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_MONCLOSEJE"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "금액 음수 불가", 0L); + userlog("LG_MONCLOSEJE 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_MONCLOSEJE 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_MONCLOSEJE 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_MONCLOSEJE 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(MERCH_PAY/ADJ) */ + userlog("LG_MONCLOSEJE 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_MONCLOSEJE 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_MONCLOSEJE 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, endd, sizeof(endd)); gets_(b, T_STR1, fromd, sizeof(fromd)); if (fromd[0] == 0) strcpy(fromd, endd); strncpy(h_from, fromd, sizeof(h_from)-1); h_from[sizeof(h_from)-1] = 0; @@ -31,6 +101,32 @@ void LG_MONCLOSEJE(TPSVCINFO *p) lgdb_insert_line(vid, "FEE_INC", "C", net, "", endd, "posted") < 0) FAIL(b); setl(b, T_ID1, vid); setl(b, T_NET, net); userlog("LG_MONCLOSEJE %s~%s 미수계상 %ld vid=%ld", fromd, endd, net, vid); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_MONCLOSEJE band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_MONCLOSEJE 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_MONCLOSEJE 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_MONTHCLOSE.pgc b/app/src/lg/svc/LG_MONTHCLOSE.pgc index ab87b19..ad0715a 100644 --- a/app/src/lg/svc/LG_MONTHCLOSE.pgc +++ b/app/src/lg/svc/LG_MONTHCLOSE.pgc @@ -1,8 +1,10 @@ /* - * LG_MONTHCLOSE.pgc - lg 모듈 서비스 LG_MONTHCLOSE (one service = one file). + * LG_MONTHCLOSE.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_MONTHCLOSE. * 월차 결산 분개: [T_BIZDATE=월초 ~ T_STR1=월말] 구간 FEE_INC 순액(대변-차변)을 * 이익잉여금(RETAINED)으로 대체하는 균형 전표(D:FEE_INC / C:RETAINED)를 전기. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_MONTHCLOSE.h" @@ -15,6 +17,77 @@ void LG_MONTHCLOSE(TPSVCINFO *p) char h_from[16], h_to[16]; long h_net; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_MONTHCLOSE"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "대상액 음수 불가", 0L); + userlog("LG_MONTHCLOSE 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_MONTHCLOSE 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_MONTHCLOSE 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_MONTHCLOSE 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(PREPAID/RETAINED) */ + userlog("LG_MONTHCLOSE 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_MONTHCLOSE 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_MONTHCLOSE 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, dfrom, sizeof(dfrom)); gets_(b, T_STR1, dto, sizeof(dto)); @@ -41,6 +114,32 @@ void LG_MONTHCLOSE(TPSVCINFO *p) setl(b, T_ID1, vid); setl(b, T_AMT2, net); Bchg(b, T_STATUS, 0, "P", 0L); userlog("LG_MONTHCLOSE vid=%ld [%s~%s] 손익대체 %ld (차=대 균형)", vid, dfrom, dto, net); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_MONTHCLOSE band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_MONTHCLOSE 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_MONTHCLOSE 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_MONTHLYSUM.pgc b/app/src/lg/svc/LG_MONTHLYSUM.pgc index d654f66..23c7d39 100644 --- a/app/src/lg/svc/LG_MONTHLYSUM.pgc +++ b/app/src/lg/svc/LG_MONTHLYSUM.pgc @@ -1,8 +1,9 @@ /* - * LG_MONTHLYSUM.pgc - lg 모듈 서비스 LG_MONTHLYSUM (one service = one file). + * LG_MONTHLYSUM.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_MONTHLYSUM. * 월 계정과목별 차/대 집계 건수 조회(YYYY-MM). - * 이전 인라인 lg_svr.pgc 에서 분리; 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). - * lg_svr 가 tpsvrinit 에서 tpadvertise 로 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_MONTHLYSUM.h" @@ -14,6 +15,74 @@ void LG_MONTHLYSUM(TPSVCINFO *p) long h_cnt, h_dr; char h_month[16]; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_MONTHLYSUM"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "대상액 음수 불가", 0L); + userlog("LG_MONTHLYSUM 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_MONTHLYSUM 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_MONTHLYSUM 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_MONTHLYSUM 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(ACCRUED_PAY/RECV_ACCR) */ + userlog("LG_MONTHLYSUM 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_MONTHLYSUM 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_MONTHLYSUM 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_STR1, month, sizeof(month)); if (month[0] == 0) { gets_(b, T_BIZDATE, month, sizeof(month)); month[7] = 0; } strncpy(h_month, month, sizeof(h_month)-1); h_month[sizeof(h_month)-1] = 0; @@ -24,6 +93,32 @@ void LG_MONTHLYSUM(TPSVCINFO *p) if (sqlca.sqlcode < 0) FAIL(b); setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_dr); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_MONTHLYSUM band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_MONTHLYSUM 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_MONTHLYSUM 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_NETPOS.pgc b/app/src/lg/svc/LG_NETPOS.pgc index e1fc618..2e191dd 100644 --- a/app/src/lg/svc/LG_NETPOS.pgc +++ b/app/src/lg/svc/LG_NETPOS.pgc @@ -1,8 +1,10 @@ /* - * LG_NETPOS.pgc - lg 모듈 서비스 LG_NETPOS (one service = one file). + * LG_NETPOS.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_NETPOS. * 순포지션 조회: 당일 채권(RECV) 잔액과 가맹점미지급(MERCH_PAY) 잔액을 * 계정잔액 테이블에서 dbio 로 읽어 순포지션(RECV - MERCH_PAY)을 산출한다. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_NETPOS.h" @@ -11,6 +13,85 @@ void LG_NETPOS(TPSVCINFO *p) UBFH *b = (UBFH *)p->data; char bizdate[16]; long recv = 0, pay = 0, net; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_NETPOS"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "대상액 음수 불가", 0L); + userlog("LG_NETPOS 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_NETPOS 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_NETPOS 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_NETPOS 라인집계 조회 경고 [%d]", sqlca.sqlcode); + if (gx_mid[0]) { + EXEC SQL SELECT coalesce(mdr_bps,0), coalesce(daily_limit,0), coalesce(status,'?') + INTO :gx_mbps, :gx_lim, :gx_mst FROM merchant WHERE merchant_id = :gx_mid; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_NETPOS 가맹점 미등록 mid=%s (정책 기본값)", gx_mid); + } + EXEC SQL SELECT coalesce(balance,0) INTO :gx_bal + FROM lg_merch_bal WHERE merchant_id = :gx_mid AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_bal = 0; + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(CASH_RECOV/ACCRUED_PAY) */ + userlog("LG_NETPOS 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "CLOSED") == 0) { + userlog("LG_NETPOS 해지가맹점 mid=%s - 잔액정리 경로", gx_mid); + } else if (gx_mbps > 30) { + gx_rc = 1; /* 고 MDR 가맹점 - 수수료 배분 재검토 */ + } + if (gx_lim > 0 && gx_amt > gx_lim) { + userlog("LG_NETPOS 일한도 초과소지 amt=%ld limit=%ld", gx_amt, gx_lim); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_NETPOS 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); if (lgdb_get_acct_bal("RECV", bizdate, &recv) < 0) FAIL(b); @@ -20,6 +101,36 @@ void LG_NETPOS(TPSVCINFO *p) setl(b, T_AMT2, recv); setl(b, T_AMT3, pay); setl(b, T_NET, net); Bchg(b, T_STATUS, 0, (net >= 0) ? "L" : "S", 0L); /* L=롱, S=숏 포지션 */ userlog("LG_NETPOS %s RECV %ld - MERCH_PAY %ld = %ld", bizdate, recv, pay, net); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_NETPOS band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_NETPOS 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + if (gx_mid[0]) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0) + INTO :gx_dr, :gx_cr FROM lg_ledger_line + WHERE merchant_id = :gx_mid AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_bal = gx_dr - gx_cr; + } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_NETPOS 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_PARTREVERSE.pgc b/app/src/lg/svc/LG_PARTREVERSE.pgc index 2bd4b69..5d87801 100644 --- a/app/src/lg/svc/LG_PARTREVERSE.pgc +++ b/app/src/lg/svc/LG_PARTREVERSE.pgc @@ -1,7 +1,9 @@ /* - * LG_PARTREVERSE.pgc - lg 모듈 서비스 LG_PARTREVERSE (one service = one file). + * LG_PARTREVERSE.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_PARTREVERSE. * 부분 역분개: 원전표에서 특정 계정 라인만 차대를 뒤집어 신규 전표로 복사. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_PARTREVERSE.h" @@ -14,6 +16,84 @@ void LG_PARTREVERSE(TPSVCINFO *p) char h_acct[32], h_bizdate[16]; long h_src, h_new; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_PARTREVERSE"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_PARTREVERSE 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_PARTREVERSE 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_PARTREVERSE 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_PARTREVERSE 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(*),0), coalesce(max(voucher_id),0) + INTO :gx_cnt, :gx_max FROM lg_voucher WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + if (gx_in_id > 0) { + EXEC SQL SELECT coalesce(status,'?'), coalesce(ref_id,0) + INTO :gx_mst, :gx_prev FROM lg_voucher WHERE voucher_id = :gx_in_id; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_PARTREVERSE 전표 미존재 vid=%ld", gx_in_id); + } + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(BADDEBT_RCV/PREPAID) */ + userlog("LG_PARTREVERSE 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "S") == 0) { + userlog("LG_PARTREVERSE 마감연계(S) 전표 - 정정은 역분개 권고", gx_svc); + } else if (gx_mst[0] && strcmp(gx_mst, "X") == 0) { + gx_rc = 2; /* 취소 전표 - 재개 필요 */ + } else if (gx_dr != gx_cr) { + gx_diff = gx_dr - gx_cr; + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_PARTREVERSE 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); src = getl(b, T_ID1); gets_(b, T_KEY1, acct, sizeof(acct)); if (acct[0] == 0) strcpy(acct, "RECV"); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); @@ -32,6 +112,31 @@ void LG_PARTREVERSE(TPSVCINFO *p) if (sqlca.sqlcode < 0) FAIL(b); setl(b, T_ID2, new); setl(b, T_COUNT, sqlca.sqlerrd[2]); userlog("LG_PARTREVERSE 원전표 %ld 계정 %s -> vid %ld %ld줄", src, acct, new, (long)sqlca.sqlerrd[2]); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_PARTREVERSE band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_PARTREVERSE 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0) INTO :gx_cnt + FROM lg_voucher WHERE biz_date = :gx_bd AND status = 'P'; + if (sqlca.sqlcode == 100) gx_cnt = 0; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_PARTREVERSE 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_PAYACCRUE.pgc b/app/src/lg/svc/LG_PAYACCRUE.pgc index 36042f8..0055d54 100644 --- a/app/src/lg/svc/LG_PAYACCRUE.pgc +++ b/app/src/lg/svc/LG_PAYACCRUE.pgc @@ -1,8 +1,10 @@ /* - * LG_PAYACCRUE.pgc - lg 모듈 서비스 LG_PAYACCRUE (one service = one file). + * LG_PAYACCRUE.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_PAYACCRUE. * 미지급 계상 분개: 당일 정산 순액 합계와 원장 MERCH_PAY 대변 합계의 갭을 * 계산해, 갭이 양수이면 D:SUSP / C:MERCH_PAY 균형 전표로 미지급을 계상한다. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_PAYACCRUE.h" @@ -15,6 +17,88 @@ void LG_PAYACCRUE(TPSVCINFO *p) char h_bizdate[16]; long h_settle, h_posted; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_PAYACCRUE"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_PAYACCRUE 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_PAYACCRUE 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_PAYACCRUE 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_PAYACCRUE 라인집계 조회 경고 [%d]", sqlca.sqlcode); + if (gx_mid[0]) { + EXEC SQL SELECT coalesce(mdr_bps,0), coalesce(daily_limit,0), coalesce(status,'?') + INTO :gx_mbps, :gx_lim, :gx_mst FROM merchant WHERE merchant_id = :gx_mid; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_PAYACCRUE 가맹점 미등록 mid=%s (정책 기본값)", gx_mid); + } + EXEC SQL SELECT coalesce(balance,0) INTO :gx_bal + FROM lg_merch_bal WHERE merchant_id = :gx_mid AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_bal = 0; + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(CASH_RECOV/ADJ) */ + userlog("LG_PAYACCRUE 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "CLOSED") == 0) { + userlog("LG_PAYACCRUE 해지가맹점 mid=%s - 잔액정리 경로", gx_mid); + } else if (gx_mbps > 30) { + gx_rc = 1; /* 고 MDR 가맹점 - 수수료 배분 재검토 */ + } + if (gx_lim > 0 && gx_amt > gx_lim) { + userlog("LG_PAYACCRUE 일한도 초과소지 amt=%ld limit=%ld", gx_amt, gx_lim); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_PAYACCRUE 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; @@ -41,6 +125,36 @@ void LG_PAYACCRUE(TPSVCINFO *p) setl(b, T_ID1, vid); Bchg(b, T_STATUS, 0, "P", 0L); userlog("LG_PAYACCRUE vid=%ld 미지급 갭 %ld 계상 (정산 %ld vs 원장 %ld)", vid, gap, h_settle, h_posted); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_PAYACCRUE band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_PAYACCRUE 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + if (gx_mid[0]) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0) + INTO :gx_dr, :gx_cr FROM lg_ledger_line + WHERE merchant_id = :gx_mid AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_bal = gx_dr - gx_cr; + } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_PAYACCRUE 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_PAYMATCH.pgc b/app/src/lg/svc/LG_PAYMATCH.pgc index 0035a30..2f378be 100644 --- a/app/src/lg/svc/LG_PAYMATCH.pgc +++ b/app/src/lg/svc/LG_PAYMATCH.pgc @@ -1,7 +1,9 @@ /* - * LG_PAYMATCH.pgc - lg 모듈 서비스 LG_PAYMATCH (one service = one file). + * LG_PAYMATCH.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_PAYMATCH. * 지급 매칭: 원장 지급(MERCH_PAY 대변) 건수와 정산 건수를 대조해 매칭율 판정. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_PAYMATCH.h" @@ -13,6 +15,85 @@ void LG_PAYMATCH(TPSVCINFO *p) char h_bizdate[16]; long h_ledg, h_settle; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_PAYMATCH"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "금액 음수 불가", 0L); + userlog("LG_PAYMATCH 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_PAYMATCH 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_PAYMATCH 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_PAYMATCH 라인집계 조회 경고 [%d]", sqlca.sqlcode); + if (gx_mid[0]) { + EXEC SQL SELECT coalesce(mdr_bps,0), coalesce(daily_limit,0), coalesce(status,'?') + INTO :gx_mbps, :gx_lim, :gx_mst FROM merchant WHERE merchant_id = :gx_mid; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_PAYMATCH 가맹점 미등록 mid=%s (정책 기본값)", gx_mid); + } + EXEC SQL SELECT coalesce(balance,0) INTO :gx_bal + FROM lg_merch_bal WHERE merchant_id = :gx_mid AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_bal = 0; + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(FEE_INC/BADDEBT_EXP) */ + userlog("LG_PAYMATCH 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "CLOSED") == 0) { + userlog("LG_PAYMATCH 해지가맹점 mid=%s - 잔액정리 경로", gx_mid); + } else if (gx_mbps > 30) { + gx_rc = 1; /* 고 MDR 가맹점 - 수수료 배분 재검토 */ + } + if (gx_lim > 0 && gx_amt > gx_lim) { + userlog("LG_PAYMATCH 일한도 초과소지 amt=%ld limit=%ld", gx_amt, gx_lim); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_PAYMATCH 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); 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_ledg FROM lg_ledger_line @@ -24,6 +105,36 @@ void LG_PAYMATCH(TPSVCINFO *p) setl(b, T_DELTA, h_ledg - h_settle); Bchg(b, T_STATUS, 0, (h_ledg == h_settle) ? "MATCH" : "GAP", 0L); userlog("LG_PAYMATCH %s 원장지급 %ld건 정산 %ld건", bizdate, h_ledg, h_settle); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_PAYMATCH band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_PAYMATCH 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + if (gx_mid[0]) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0) + INTO :gx_dr, :gx_cr FROM lg_ledger_line + WHERE merchant_id = :gx_mid AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_bal = gx_dr - gx_cr; + } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_PAYMATCH 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_PAYRECON.pgc b/app/src/lg/svc/LG_PAYRECON.pgc index 876b875..bb8f516 100644 --- a/app/src/lg/svc/LG_PAYRECON.pgc +++ b/app/src/lg/svc/LG_PAYRECON.pgc @@ -1,8 +1,10 @@ /* - * LG_PAYRECON.pgc - lg 모듈 서비스 LG_PAYRECON (one service = one file). + * LG_PAYRECON.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_PAYRECON. * 원장-지급 대사: 가맹점(T_MERCHANT)의 원장 라인 순액(대변-차변)과 정산 * 지급 순액을 비교해 갭을 반환한다 — 지급 대사 담당자 화면용. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_PAYRECON.h" @@ -15,6 +17,88 @@ void LG_PAYRECON(TPSVCINFO *p) char h_merch[64], h_bizdate[16]; long h_ledger, h_paid, h_paycnt; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_PAYRECON"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_PAYRECON 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_PAYRECON 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_PAYRECON 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_PAYRECON 라인집계 조회 경고 [%d]", sqlca.sqlcode); + if (gx_mid[0]) { + EXEC SQL SELECT coalesce(mdr_bps,0), coalesce(daily_limit,0), coalesce(status,'?') + INTO :gx_mbps, :gx_lim, :gx_mst FROM merchant WHERE merchant_id = :gx_mid; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_PAYRECON 가맹점 미등록 mid=%s (정책 기본값)", gx_mid); + } + EXEC SQL SELECT coalesce(balance,0) INTO :gx_bal + FROM lg_merch_bal WHERE merchant_id = :gx_mid AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_bal = 0; + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(RETAINED/BADDEBT_RCV) */ + userlog("LG_PAYRECON 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "CLOSED") == 0) { + userlog("LG_PAYRECON 해지가맹점 mid=%s - 잔액정리 경로", gx_mid); + } else if (gx_mbps > 30) { + gx_rc = 1; /* 고 MDR 가맹점 - 수수료 배분 재검토 */ + } + if (gx_lim > 0 && gx_amt > gx_lim) { + userlog("LG_PAYRECON 일한도 초과소지 amt=%ld limit=%ld", gx_amt, gx_lim); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_PAYRECON 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); @@ -36,6 +120,36 @@ void LG_PAYRECON(TPSVCINFO *p) setl(b, T_DELTA, gap); setl(b, T_COUNT, h_paycnt); Bchg(b, T_STATUS, 0, (gap == 0) ? "M" : "U", 0L); userlog("LG_PAYRECON %s %s 원장 %ld vs 지급 %ld 갭 %ld", merch, bizdate, h_ledger, h_paid, gap); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_PAYRECON band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_PAYRECON 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + if (gx_mid[0]) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0) + INTO :gx_dr, :gx_cr FROM lg_ledger_line + WHERE merchant_id = :gx_mid AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_bal = gx_dr - gx_cr; + } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_PAYRECON 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_PENDAGE.pgc b/app/src/lg/svc/LG_PENDAGE.pgc index 03604ee..4421c4b 100644 --- a/app/src/lg/svc/LG_PENDAGE.pgc +++ b/app/src/lg/svc/LG_PENDAGE.pgc @@ -1,8 +1,10 @@ /* - * LG_PENDAGE.pgc - lg 모듈 서비스 LG_PENDAGE (one service = one file). + * LG_PENDAGE.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_PENDAGE. * 미결전표 경과일 집계: 작성(D) 전표를 경과일 구간(0-1일 / 2-7일 / 8일+)별로 * 건수 집계하고 최장 경과일을 반환한다 (에이징 리포트). - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_PENDAGE.h" @@ -12,6 +14,84 @@ void LG_PENDAGE(TPSVCINFO *p) EXEC SQL BEGIN DECLARE SECTION; long h_b0, h_b1, h_b2, h_maxage; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_PENDAGE"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "금액 음수 불가", 0L); + userlog("LG_PENDAGE 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_PENDAGE 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_PENDAGE 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_PENDAGE 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(*),0), coalesce(max(voucher_id),0) + INTO :gx_cnt, :gx_max FROM lg_voucher WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + if (gx_in_id > 0) { + EXEC SQL SELECT coalesce(status,'?'), coalesce(ref_id,0) + INTO :gx_mst, :gx_prev FROM lg_voucher WHERE voucher_id = :gx_in_id; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_PENDAGE 전표 미존재 vid=%ld", gx_in_id); + } + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(FEE_INC/RECV) */ + userlog("LG_PENDAGE 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "S") == 0) { + userlog("LG_PENDAGE 마감연계(S) 전표 - 정정은 역분개 권고", gx_svc); + } else if (gx_mst[0] && strcmp(gx_mst, "X") == 0) { + gx_rc = 2; /* 취소 전표 - 재개 필요 */ + } else if (gx_dr != gx_cr) { + gx_diff = gx_dr - gx_cr; + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_PENDAGE 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); EXEC SQL SELECT coalesce(sum(CASE WHEN CURRENT_DATE - biz_date <= 1 THEN 1 ELSE 0 END), 0), @@ -26,6 +106,31 @@ void LG_PENDAGE(TPSVCINFO *p) setl(b, T_ARG4, h_maxage); setl(b, T_COUNT, h_b0 + h_b1 + h_b2); userlog("LG_PENDAGE 에이징 0-1일=%ld 2-7일=%ld 8일+=%ld 최장=%ld일", h_b0, h_b1, h_b2, h_maxage); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_PENDAGE band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_PENDAGE 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0) INTO :gx_cnt + FROM lg_voucher WHERE biz_date = :gx_bd AND status = 'P'; + if (sqlca.sqlcode == 100) gx_cnt = 0; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_PENDAGE 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_PENDCLEAR.pgc b/app/src/lg/svc/LG_PENDCLEAR.pgc index 7035bd6..4ff4e28 100644 --- a/app/src/lg/svc/LG_PENDCLEAR.pgc +++ b/app/src/lg/svc/LG_PENDCLEAR.pgc @@ -1,7 +1,9 @@ /* - * LG_PENDCLEAR.pgc - lg 모듈 서비스 LG_PENDCLEAR (one service = one file). + * LG_PENDCLEAR.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_PENDCLEAR. * 미결전표 정리: 기준일 이전 작성(D) 전표를 취소(X)하고 라인을 마감 처리. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_PENDCLEAR.h" @@ -13,6 +15,84 @@ void LG_PENDCLEAR(TPSVCINFO *p) EXEC SQL BEGIN DECLARE SECTION; char h_before[16]; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_PENDCLEAR"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "대상액 음수 불가", 0L); + userlog("LG_PENDCLEAR 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_PENDCLEAR 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_PENDCLEAR 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_PENDCLEAR 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(*),0), coalesce(max(voucher_id),0) + INTO :gx_cnt, :gx_max FROM lg_voucher WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + if (gx_in_id > 0) { + EXEC SQL SELECT coalesce(status,'?'), coalesce(ref_id,0) + INTO :gx_mst, :gx_prev FROM lg_voucher WHERE voucher_id = :gx_in_id; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_PENDCLEAR 전표 미존재 vid=%ld", gx_in_id); + } + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(BADDEBT_RCV/PREPAID) */ + userlog("LG_PENDCLEAR 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "S") == 0) { + userlog("LG_PENDCLEAR 마감연계(S) 전표 - 정정은 역분개 권고", gx_svc); + } else if (gx_mst[0] && strcmp(gx_mst, "X") == 0) { + gx_rc = 2; /* 취소 전표 - 재개 필요 */ + } else if (gx_dr != gx_cr) { + gx_diff = gx_dr - gx_cr; + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_PENDCLEAR 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, before, sizeof(before)); strncpy(h_before, before, sizeof(h_before)-1); h_before[sizeof(h_before)-1] = 0; EXEC SQL UPDATE lg_ledger_line SET status = 'closed' @@ -25,6 +105,31 @@ void LG_PENDCLEAR(TPSVCINFO *p) vcnt = sqlca.sqlerrd[2]; setl(b, T_COUNT, vcnt); userlog("LG_PENDCLEAR %s 이전 미결 %ld건 정리", before, vcnt); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_PENDCLEAR band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_PENDCLEAR 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0) INTO :gx_cnt + FROM lg_voucher WHERE biz_date = :gx_bd AND status = 'P'; + if (sqlca.sqlcode == 100) gx_cnt = 0; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_PENDCLEAR 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_PENDESCAL.pgc b/app/src/lg/svc/LG_PENDESCAL.pgc index 0788b0b..aaae447 100644 --- a/app/src/lg/svc/LG_PENDESCAL.pgc +++ b/app/src/lg/svc/LG_PENDESCAL.pgc @@ -1,7 +1,9 @@ /* - * LG_PENDESCAL.pgc - lg 모듈 서비스 LG_PENDESCAL (one service = one file). + * LG_PENDESCAL.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_PENDESCAL. * 미결 에스컬레이션: 기준일보다 오래 방치된 작성(D) 전표 건수를 집계. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_PENDESCAL.h" @@ -13,6 +15,87 @@ void LG_PENDESCAL(TPSVCINFO *p) char h_bizdate[16]; long h_cnt, h_amt; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_PENDESCAL"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "금액 음수 불가", 0L); + userlog("LG_PENDESCAL 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_PENDESCAL 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_PENDESCAL 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_PENDESCAL 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(*),0), coalesce(max(voucher_id),0) + INTO :gx_cnt, :gx_max FROM lg_voucher WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + if (gx_in_id > 0) { + EXEC SQL SELECT coalesce(status,'?'), coalesce(ref_id,0) + INTO :gx_mst, :gx_prev FROM lg_voucher WHERE voucher_id = :gx_in_id; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_PENDESCAL 전표 미존재 vid=%ld", gx_in_id); + } + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(ADJ/BADDEBT_EXP) */ + userlog("LG_PENDESCAL 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "S") == 0) { + userlog("LG_PENDESCAL 마감연계(S) 전표 - 정정은 역분개 권고", gx_svc); + } else if (gx_mst[0] && strcmp(gx_mst, "X") == 0) { + gx_rc = 2; /* 취소 전표 - 재개 필요 */ + } else if (gx_dr != gx_cr) { + gx_diff = gx_dr - gx_cr; + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_PENDESCAL 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); 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(*), @@ -24,6 +107,31 @@ void LG_PENDESCAL(TPSVCINFO *p) setl(b, T_COUNT, h_cnt); setl(b, T_AMT2, h_amt); Bchg(b, T_STATUS, 0, (h_cnt > 0) ? "ESCALATE" : "CLEAN", 0L); userlog("LG_PENDESCAL %s 지연 미결 %ld건", bizdate, h_cnt); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_PENDESCAL band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_PENDESCAL 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0) INTO :gx_cnt + FROM lg_voucher WHERE biz_date = :gx_bd AND status = 'P'; + if (sqlca.sqlcode == 100) gx_cnt = 0; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_PENDESCAL 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_PENDLIST.pgc b/app/src/lg/svc/LG_PENDLIST.pgc index 95a5a5e..ddefce2 100644 --- a/app/src/lg/svc/LG_PENDLIST.pgc +++ b/app/src/lg/svc/LG_PENDLIST.pgc @@ -1,8 +1,10 @@ /* - * LG_PENDLIST.pgc - lg 모듈 서비스 LG_PENDLIST (one service = one file). + * LG_PENDLIST.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_PENDLIST. * 미결전표 관리(목록 집계): 작성(D) 상태 전표의 건수, 연계 라인 총차변액, * 최고령 전표 ID/영업일을 반환한다. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_PENDLIST.h" @@ -13,6 +15,84 @@ void LG_PENDLIST(TPSVCINFO *p) long h_cnt, h_oldest, h_amt; char h_odate[16]; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_PENDLIST"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_PENDLIST 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_PENDLIST 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_PENDLIST 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_PENDLIST 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(*),0), coalesce(max(voucher_id),0) + INTO :gx_cnt, :gx_max FROM lg_voucher WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + if (gx_in_id > 0) { + EXEC SQL SELECT coalesce(status,'?'), coalesce(ref_id,0) + INTO :gx_mst, :gx_prev FROM lg_voucher WHERE voucher_id = :gx_in_id; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_PENDLIST 전표 미존재 vid=%ld", gx_in_id); + } + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(UNEARNED_REV/MERCH_PAY) */ + userlog("LG_PENDLIST 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "S") == 0) { + userlog("LG_PENDLIST 마감연계(S) 전표 - 정정은 역분개 권고", gx_svc); + } else if (gx_mst[0] && strcmp(gx_mst, "X") == 0) { + gx_rc = 2; /* 취소 전표 - 재개 필요 */ + } else if (gx_dr != gx_cr) { + gx_diff = gx_dr - gx_cr; + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_PENDLIST 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); EXEC SQL SELECT count(*), coalesce(min(voucher_id), 0) INTO :h_cnt, :h_oldest FROM lg_voucher WHERE status = 'D'; @@ -34,6 +114,31 @@ void LG_PENDLIST(TPSVCINFO *p) setl(b, T_COUNT, h_cnt); setl(b, T_ID1, h_oldest); setl(b, T_AMT2, h_amt); Bchg(b, T_STR2, 0, h_odate, 0L); userlog("LG_PENDLIST 미결 %ld건 차변누계 %ld 최고령 %ld", h_cnt, h_amt, h_oldest); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_PENDLIST band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_PENDLIST 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0) INTO :gx_cnt + FROM lg_voucher WHERE biz_date = :gx_bd AND status = 'P'; + if (sqlca.sqlcode == 100) gx_cnt = 0; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_PENDLIST 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_PENDROUTE.pgc b/app/src/lg/svc/LG_PENDROUTE.pgc index 53a04e1..2a17e40 100644 --- a/app/src/lg/svc/LG_PENDROUTE.pgc +++ b/app/src/lg/svc/LG_PENDROUTE.pgc @@ -1,7 +1,9 @@ /* - * LG_PENDROUTE.pgc - lg 모듈 서비스 LG_PENDROUTE (one service = one file). + * LG_PENDROUTE.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_PENDROUTE. * 미결전표 배정: 특정 작성(D) 전표의 메모에 담당자를 기록해 배정. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_PENDROUTE.h" @@ -14,6 +16,87 @@ void LG_PENDROUTE(TPSVCINFO *p) long h_vid; char h_who[64]; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_PENDROUTE"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_PENDROUTE 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_PENDROUTE 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_PENDROUTE 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_PENDROUTE 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(*),0), coalesce(max(voucher_id),0) + INTO :gx_cnt, :gx_max FROM lg_voucher WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + if (gx_in_id > 0) { + EXEC SQL SELECT coalesce(status,'?'), coalesce(ref_id,0) + INTO :gx_mst, :gx_prev FROM lg_voucher WHERE voucher_id = :gx_in_id; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_PENDROUTE 전표 미존재 vid=%ld", gx_in_id); + } + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(CASH_RECOV/ADJ) */ + userlog("LG_PENDROUTE 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "S") == 0) { + userlog("LG_PENDROUTE 마감연계(S) 전표 - 정정은 역분개 권고", gx_svc); + } else if (gx_mst[0] && strcmp(gx_mst, "X") == 0) { + gx_rc = 2; /* 취소 전표 - 재개 필요 */ + } else if (gx_dr != gx_cr) { + gx_diff = gx_dr - gx_cr; + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_PENDROUTE 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); vid = getl(b, T_ID1); gets_(b, T_STR1, who, sizeof(who)); if (who[0] == 0) strcpy(who, "미배정"); h_vid = vid; @@ -24,6 +107,31 @@ void LG_PENDROUTE(TPSVCINFO *p) if (sqlca.sqlerrd[2] == 0) { Bchg(b, T_MSG, 0, "작성 전표 아님", 0L); FAIL(b); } setl(b, T_COUNT, sqlca.sqlerrd[2]); userlog("LG_PENDROUTE vid=%ld 담당 %s 배정", vid, who); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_PENDROUTE band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_PENDROUTE 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0) INTO :gx_cnt + FROM lg_voucher WHERE biz_date = :gx_bd AND status = 'P'; + if (sqlca.sqlcode == 100) gx_cnt = 0; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_PENDROUTE 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_PENDSTAT.pgc b/app/src/lg/svc/LG_PENDSTAT.pgc index 66c8d42..b0b750d 100644 --- a/app/src/lg/svc/LG_PENDSTAT.pgc +++ b/app/src/lg/svc/LG_PENDSTAT.pgc @@ -1,7 +1,9 @@ /* - * LG_PENDSTAT.pgc - lg 모듈 서비스 LG_PENDSTAT (one service = one file). + * LG_PENDSTAT.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_PENDSTAT. * 미결 상태별 집계: 당일 전표를 상태(D/P/M/S/X)별로 순회 집계. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_PENDSTAT.h" @@ -14,6 +16,84 @@ void LG_PENDSTAT(TPSVCINFO *p) char h_bizdate[16], h_st[4]; long h_cnt; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_PENDSTAT"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "대상액 음수 불가", 0L); + userlog("LG_PENDSTAT 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_PENDSTAT 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_PENDSTAT 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_PENDSTAT 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(*),0), coalesce(max(voucher_id),0) + INTO :gx_cnt, :gx_max FROM lg_voucher WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + if (gx_in_id > 0) { + EXEC SQL SELECT coalesce(status,'?'), coalesce(ref_id,0) + INTO :gx_mst, :gx_prev FROM lg_voucher WHERE voucher_id = :gx_in_id; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_PENDSTAT 전표 미존재 vid=%ld", gx_in_id); + } + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(RETAINED/CASH_RECOV) */ + userlog("LG_PENDSTAT 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "S") == 0) { + userlog("LG_PENDSTAT 마감연계(S) 전표 - 정정은 역분개 권고", gx_svc); + } else if (gx_mst[0] && strcmp(gx_mst, "X") == 0) { + gx_rc = 2; /* 취소 전표 - 재개 필요 */ + } else if (gx_dr != gx_cr) { + gx_diff = gx_dr - gx_cr; + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_PENDSTAT 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; EXEC SQL DECLARE ps_cur CURSOR FOR @@ -31,6 +111,31 @@ void LG_PENDSTAT(TPSVCINFO *p) EXEC SQL CLOSE ps_cur; setl(b, T_COUNT, total); setl(b, T_ARG1, drafts); userlog("LG_PENDSTAT %s 전표 %ld건 중 미결 %ld건", bizdate, total, drafts); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_PENDSTAT band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_PENDSTAT 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0) INTO :gx_cnt + FROM lg_voucher WHERE biz_date = :gx_bd AND status = 'P'; + if (sqlca.sqlcode == 100) gx_cnt = 0; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_PENDSTAT 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_PERIODTB.pgc b/app/src/lg/svc/LG_PERIODTB.pgc index bfcf3c8..767cdbd 100644 --- a/app/src/lg/svc/LG_PERIODTB.pgc +++ b/app/src/lg/svc/LG_PERIODTB.pgc @@ -1,8 +1,10 @@ /* - * LG_PERIODTB.pgc - lg 모듈 서비스 LG_PERIODTB (one service = one file). + * LG_PERIODTB.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_PERIODTB. * 기간별 시산표: [T_BIZDATE ~ T_STR1] 구간을 계정별 GROUP BY 커서로 집계해 * 기말일 키로 lg_trial_bal 에 적재 — 계정 수와 총대차차액을 반환한다. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_PERIODTB.h" @@ -15,6 +17,79 @@ void LG_PERIODTB(TPSVCINFO *p) char h_from[16], h_to[16], h_acct[32]; long h_dr, h_cr; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_PERIODTB"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "분개액 음수 불가", 0L); + userlog("LG_PERIODTB 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_PERIODTB 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_PERIODTB 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_PERIODTB 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(debit_total,0), coalesce(credit_total,0), coalesce(balance,0) + INTO :gx_prev, :gx_amt, :gx_bal FROM lg_account_bal + WHERE acct = 'RECV' AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_prev = 0; gx_amt = 0; gx_bal = 0; } + EXEC SQL SELECT coalesce(count(DISTINCT acct),0) INTO :gx_max + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_max = 0; + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(RECV_ACCR/SUSP) */ + userlog("LG_PERIODTB 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + gx_diff = gx_dr - gx_cr; + if (gx_diff != 0) { + userlog("LG_PERIODTB 당일 대차 불균형 diff=%ld", gx_diff); + } else if (gx_max == 0) { + gx_rc = 3; /* 무거래일 */ + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_PERIODTB 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, dfrom, sizeof(dfrom)); gets_(b, T_STR1, dto, sizeof(dto)); @@ -49,6 +124,32 @@ void LG_PERIODTB(TPSVCINFO *p) setl(b, T_COUNT, naccts); setl(b, T_DELTA, netdiff); Bchg(b, T_STATUS, 0, (netdiff == 0) ? "OK" : "NG", 0L); userlog("LG_PERIODTB [%s~%s] 계정 %ld개 총대차차액 %ld", dfrom, dto, naccts, netdiff); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_PERIODTB band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_PERIODTB 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(debit_total),0), coalesce(sum(credit_total),0) + INTO :gx_dr, :gx_cr FROM lg_account_bal WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_diff = gx_dr - gx_cr; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_PERIODTB 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_PLCLOSE.pgc b/app/src/lg/svc/LG_PLCLOSE.pgc index d099215..92f1265 100644 --- a/app/src/lg/svc/LG_PLCLOSE.pgc +++ b/app/src/lg/svc/LG_PLCLOSE.pgc @@ -1,9 +1,10 @@ /* - * LG_PLCLOSE.pgc - lg 모듈 서비스 LG_PLCLOSE (one service = one file). + * LG_PLCLOSE.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_PLCLOSE. * 손익 대체 분개(일 단위): 당일 손익계정(FEE_INC, BADDEBT_EXP)의 순액을 각각 * 커서로 산출해 이익잉여금(RETAINED)으로 대체 — 수익은 D/C, 비용은 C/D 로 - * 방향을 바꿔 모두 균형 전기한다. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_PLCLOSE.h" @@ -16,6 +17,77 @@ void LG_PLCLOSE(TPSVCINFO *p) char h_bizdate[16], h_acct[32]; long h_net; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_PLCLOSE"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "금액 음수 불가", 0L); + userlog("LG_PLCLOSE 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_PLCLOSE 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_PLCLOSE 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_PLCLOSE 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(CASH/FEE_INC) */ + userlog("LG_PLCLOSE 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_PLCLOSE 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_PLCLOSE 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; @@ -57,6 +129,32 @@ void LG_PLCLOSE(TPSVCINFO *p) setl(b, T_COUNT, closed); setl(b, T_NET, moved); userlog("LG_PLCLOSE %s 손익계정 %ld개 대체 순이익 %ld", bizdate, closed, moved); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_PLCLOSE band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_PLCLOSE 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_PLCLOSE 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_PLSUMMARY.pgc b/app/src/lg/svc/LG_PLSUMMARY.pgc index 0bb070a..5469ace 100644 --- a/app/src/lg/svc/LG_PLSUMMARY.pgc +++ b/app/src/lg/svc/LG_PLSUMMARY.pgc @@ -1,7 +1,9 @@ /* - * LG_PLSUMMARY.pgc - lg 모듈 서비스 LG_PLSUMMARY (one service = one file). + * LG_PLSUMMARY.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_PLSUMMARY. * 손익 요약: 당일 수익(_INC)과 비용(_EXP)의 순액을 계산해 순손익을 산출. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_PLSUMMARY.h" @@ -13,6 +15,77 @@ void LG_PLSUMMARY(TPSVCINFO *p) char h_bizdate[16]; long h_inc, h_exp; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_PLSUMMARY"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "분개액 음수 불가", 0L); + userlog("LG_PLSUMMARY 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_PLSUMMARY 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_PLSUMMARY 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_PLSUMMARY 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(RECV/BADDEBT_EXP) */ + userlog("LG_PLSUMMARY 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_PLSUMMARY 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_PLSUMMARY 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); 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 @@ -24,6 +97,32 @@ void LG_PLSUMMARY(TPSVCINFO *p) setl(b, T_NET, h_inc - h_exp); Bchg(b, T_STATUS, 0, (h_inc - h_exp >= 0) ? "PROFIT" : "LOSS", 0L); userlog("LG_PLSUMMARY %s 수익 %ld 비용 %ld 순익 %ld", bizdate, h_inc, h_exp, h_inc - h_exp); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_PLSUMMARY band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_PLSUMMARY 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_PLSUMMARY 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_POST.pgc b/app/src/lg/svc/LG_POST.pgc index 6721b65..0e7c242 100644 --- a/app/src/lg/svc/LG_POST.pgc +++ b/app/src/lg/svc/LG_POST.pgc @@ -1,8 +1,9 @@ /* - * LG_POST.pgc - lg 모듈 서비스 LG_POST (one service = one file). + * LG_POST.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_POST. * 전표 작성(draft) + 차변/대변 균형 분개 2줄. - * 이전 인라인 lg_svr.pgc 에서 분리; 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). - * lg_svr 가 tpsvrinit 에서 tpadvertise 로 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_POST.h" @@ -11,6 +12,84 @@ void LG_POST(TPSVCINFO *p) UBFH *b = (UBFH *)p->data; char dacct[32], cacct[32], bizdate[16], merch[64]; long vid, ref, amount; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_POST"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "대상액 음수 불가", 0L); + userlog("LG_POST 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_POST 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_POST 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_POST 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(*),0), coalesce(max(voucher_id),0) + INTO :gx_cnt, :gx_max FROM lg_voucher WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + if (gx_in_id > 0) { + EXEC SQL SELECT coalesce(status,'?'), coalesce(ref_id,0) + INTO :gx_mst, :gx_prev FROM lg_voucher WHERE voucher_id = :gx_in_id; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_POST 전표 미존재 vid=%ld", gx_in_id); + } + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(CASH_RECOV/FEE_INC) */ + userlog("LG_POST 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "S") == 0) { + userlog("LG_POST 마감연계(S) 전표 - 정정은 역분개 권고", gx_svc); + } else if (gx_mst[0] && strcmp(gx_mst, "X") == 0) { + gx_rc = 2; /* 취소 전표 - 재개 필요 */ + } else if (gx_dr != gx_cr) { + gx_diff = gx_dr - gx_cr; + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_POST 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_KEY1, dacct, sizeof(dacct)); if (dacct[0] == 0) strcpy(dacct, "RECV"); gets_(b, T_KEY2, cacct, sizeof(cacct)); if (cacct[0] == 0) strcpy(cacct, "MERCH_PAY"); @@ -28,6 +107,31 @@ void LG_POST(TPSVCINFO *p) setl(b, T_ID1, vid); Bchg(b, T_STATUS, 0, "D", 0L); userlog("LG_POST vid=%ld 차변=%s 대변=%s 금액=%ld", vid, dacct, cacct, amount); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_POST band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_POST 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0) INTO :gx_cnt + FROM lg_voucher WHERE biz_date = :gx_bd AND status = 'P'; + if (sqlca.sqlcode == 100) gx_cnt = 0; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_POST 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_PREPAIDAMORT.pgc b/app/src/lg/svc/LG_PREPAIDAMORT.pgc index beca4d4..7dc6e8b 100644 --- a/app/src/lg/svc/LG_PREPAIDAMORT.pgc +++ b/app/src/lg/svc/LG_PREPAIDAMORT.pgc @@ -1,7 +1,9 @@ /* - * LG_PREPAIDAMORT.pgc - lg 모듈 서비스 LG_PREPAIDAMORT (one service = one file). + * LG_PREPAIDAMORT.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_PREPAIDAMORT. * 선급 상각: 선급비용 총액을 개월(N)로 나눠 1개월분을 비용(D)/선급(C) 상각. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_PREPAIDAMORT.h" @@ -10,6 +12,74 @@ void LG_PREPAIDAMORT(TPSVCINFO *p) UBFH *b = (UBFH *)p->data; char bizdate[16]; long vid, total, months, per; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_PREPAIDAMORT"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_PREPAIDAMORT 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_PREPAIDAMORT 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_PREPAIDAMORT 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_PREPAIDAMORT 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(ACCRUED_PAY/BADDEBT_EXP) */ + userlog("LG_PREPAIDAMORT 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_PREPAIDAMORT 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_PREPAIDAMORT 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); total = getl(b, T_AMOUNT); months = getl(b, T_COUNT); if (months <= 0) months = 12; @@ -22,6 +92,32 @@ void LG_PREPAIDAMORT(TPSVCINFO *p) lgdb_insert_line(vid, "PREPAID", "C", per, "", bizdate, "posted") < 0) FAIL(b); setl(b, T_ID1, vid); setl(b, T_NET, per); userlog("LG_PREPAIDAMORT vid=%ld 총 %ld / %ld개월 = %ld", vid, total, months, per); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_PREPAIDAMORT band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_PREPAIDAMORT 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_PREPAIDAMORT 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_PREPAIDPOST.pgc b/app/src/lg/svc/LG_PREPAIDPOST.pgc index a331023..78b4a37 100644 --- a/app/src/lg/svc/LG_PREPAIDPOST.pgc +++ b/app/src/lg/svc/LG_PREPAIDPOST.pgc @@ -1,7 +1,9 @@ /* - * LG_PREPAIDPOST.pgc - lg 모듈 서비스 LG_PREPAIDPOST (one service = one file). + * LG_PREPAIDPOST.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_PREPAIDPOST. * 선급비용 계상: 선급비용(D)/현금(C)으로 지출 시점 자산 계상. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_PREPAIDPOST.h" @@ -10,6 +12,84 @@ void LG_PREPAIDPOST(TPSVCINFO *p) UBFH *b = (UBFH *)p->data; char bizdate[16]; long vid, amount; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_PREPAIDPOST"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "대상액 음수 불가", 0L); + userlog("LG_PREPAIDPOST 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_PREPAIDPOST 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_PREPAIDPOST 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_PREPAIDPOST 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(*),0), coalesce(max(voucher_id),0) + INTO :gx_cnt, :gx_max FROM lg_voucher WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + if (gx_in_id > 0) { + EXEC SQL SELECT coalesce(status,'?'), coalesce(ref_id,0) + INTO :gx_mst, :gx_prev FROM lg_voucher WHERE voucher_id = :gx_in_id; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_PREPAIDPOST 전표 미존재 vid=%ld", gx_in_id); + } + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(PREPAID/SUSP) */ + userlog("LG_PREPAIDPOST 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "S") == 0) { + userlog("LG_PREPAIDPOST 마감연계(S) 전표 - 정정은 역분개 권고", gx_svc); + } else if (gx_mst[0] && strcmp(gx_mst, "X") == 0) { + gx_rc = 2; /* 취소 전표 - 재개 필요 */ + } else if (gx_dr != gx_cr) { + gx_diff = gx_dr - gx_cr; + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_PREPAIDPOST 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); amount = getl(b, T_AMOUNT); if (amount <= 0) { Bchg(b, T_MSG, 0, "선급액 없음", 0L); FAIL(b); } @@ -20,6 +100,31 @@ void LG_PREPAIDPOST(TPSVCINFO *p) lgdb_insert_line(vid, "CASH", "C", amount, "", bizdate, "posted") < 0) FAIL(b); setl(b, T_ID1, vid); userlog("LG_PREPAIDPOST vid=%ld 선급 %ld", vid, amount); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_PREPAIDPOST band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_PREPAIDPOST 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0) INTO :gx_cnt + FROM lg_voucher WHERE biz_date = :gx_bd AND status = 'P'; + if (sqlca.sqlcode == 100) gx_cnt = 0; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_PREPAIDPOST 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_QTRCLOSE.pgc b/app/src/lg/svc/LG_QTRCLOSE.pgc index 28f4bf4..d8d1ce9 100644 --- a/app/src/lg/svc/LG_QTRCLOSE.pgc +++ b/app/src/lg/svc/LG_QTRCLOSE.pgc @@ -1,8 +1,10 @@ /* - * LG_QTRCLOSE.pgc - lg 모듈 서비스 LG_QTRCLOSE (one service = one file). + * LG_QTRCLOSE.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_QTRCLOSE. * 분기 결산 분개: 연도(T_ARG1)/분기(T_ARG2)로 분기 구간을 산출해 그 구간의 * FEE_INC 순액을 RETAINED 로 대체 전기한다 (차변/대변 균형 분개). - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_QTRCLOSE.h" @@ -11,6 +13,74 @@ void LG_QTRCLOSE(TPSVCINFO *p) UBFH *b = (UBFH *)p->data; long year = getl(b, T_ARG1), qtr = getl(b, T_ARG2); long vid, net; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_QTRCLOSE"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "금액 음수 불가", 0L); + userlog("LG_QTRCLOSE 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_QTRCLOSE 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_QTRCLOSE 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_QTRCLOSE 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(FEE_INC/CASH) */ + userlog("LG_QTRCLOSE 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_QTRCLOSE 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_QTRCLOSE 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); static const char *QEND[4] = { "03-31", "06-30", "09-30", "12-31" }; EXEC SQL BEGIN DECLARE SECTION; char h_from[16], h_to[16]; @@ -41,6 +111,32 @@ void LG_QTRCLOSE(TPSVCINFO *p) setl(b, T_AMT2, net); Bchg(b, T_STR2, 0, h_to, 0L); userlog("LG_QTRCLOSE %ldQ%ld 대체 %ld", year, qtr, net); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_QTRCLOSE band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_QTRCLOSE 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_QTRCLOSE 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_QTRCLOSEJE.pgc b/app/src/lg/svc/LG_QTRCLOSEJE.pgc index 2f53cce..779259d 100644 --- a/app/src/lg/svc/LG_QTRCLOSEJE.pgc +++ b/app/src/lg/svc/LG_QTRCLOSEJE.pgc @@ -1,7 +1,9 @@ /* - * LG_QTRCLOSEJE.pgc - lg 모듈 서비스 LG_QTRCLOSEJE (one service = one file). + * LG_QTRCLOSEJE.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_QTRCLOSEJE. * 분기 결산 분개: 당분기 대손비용 발생분을 미지급(ACCRUED_PAY)으로 계상. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_QTRCLOSEJE.h" @@ -14,6 +16,74 @@ void LG_QTRCLOSEJE(TPSVCINFO *p) char h_from[16], h_end[16]; long h_net; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_QTRCLOSEJE"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "대상액 음수 불가", 0L); + userlog("LG_QTRCLOSEJE 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_QTRCLOSEJE 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_QTRCLOSEJE 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_QTRCLOSEJE 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(BADDEBT_RCV/FEE_INC) */ + userlog("LG_QTRCLOSEJE 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_QTRCLOSEJE 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_QTRCLOSEJE 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, endd, sizeof(endd)); gets_(b, T_STR1, fromd, sizeof(fromd)); if (fromd[0] == 0) strcpy(fromd, endd); strncpy(h_from, fromd, sizeof(h_from)-1); h_from[sizeof(h_from)-1] = 0; @@ -31,6 +101,32 @@ void LG_QTRCLOSEJE(TPSVCINFO *p) lgdb_insert_line(vid, "ACCRUED_PAY", "C", net, "", endd, "posted") < 0) FAIL(b); setl(b, T_ID1, vid); setl(b, T_NET, net); userlog("LG_QTRCLOSEJE %s~%s 미지급계상 %ld vid=%ld", fromd, endd, net, vid); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_QTRCLOSEJE band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_QTRCLOSEJE 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_QTRCLOSEJE 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_RECALC.pgc b/app/src/lg/svc/LG_RECALC.pgc index 7bc0102..4f6ca36 100644 --- a/app/src/lg/svc/LG_RECALC.pgc +++ b/app/src/lg/svc/LG_RECALC.pgc @@ -1,8 +1,9 @@ /* - * LG_RECALC.pgc - lg 모듈 서비스 LG_RECALC (one service = one file). + * LG_RECALC.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_RECALC. * 계정 잔액 원장 라인 기준 재계산(dbio). - * 이전 인라인 lg_svr.pgc 에서 분리; 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). - * lg_svr 가 tpsvrinit 에서 tpadvertise 로 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_RECALC.h" @@ -10,9 +11,106 @@ void LG_RECALC(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; char acct[32], bizdate[16]; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_RECALC"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "금액 음수 불가", 0L); + userlog("LG_RECALC 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_RECALC 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_RECALC 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_RECALC 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(ADJ/UNEARNED_REV) */ + userlog("LG_RECALC 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_RECALC 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_RECALC 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_KEY1, acct, sizeof(acct)); if (acct[0] == 0) strcpy(acct, "RECV"); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); if (lgdb_recalc_acct_bal(acct, bizdate) < 0) FAIL(b); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_RECALC band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_RECALC 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_RECALC 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_RECOVERY.pgc b/app/src/lg/svc/LG_RECOVERY.pgc index 480c49b..0ddaa03 100644 --- a/app/src/lg/svc/LG_RECOVERY.pgc +++ b/app/src/lg/svc/LG_RECOVERY.pgc @@ -1,7 +1,9 @@ /* - * LG_RECOVERY.pgc - lg 모듈 서비스 LG_RECOVERY (one service = one file). + * LG_RECOVERY.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_RECOVERY. * 상각채권 회수: 제각 채권 회수를 현금(D)/대손충당금환입(C)으로 인식. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_RECOVERY.h" @@ -10,6 +12,74 @@ void LG_RECOVERY(TPSVCINFO *p) UBFH *b = (UBFH *)p->data; char bizdate[16], merch[64]; long vid, amount; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_RECOVERY"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "분개액 음수 불가", 0L); + userlog("LG_RECOVERY 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_RECOVERY 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_RECOVERY 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_RECOVERY 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(RECV_ACCR/CASH_RECOV) */ + userlog("LG_RECOVERY 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_RECOVERY 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_RECOVERY 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); gets_(b, T_MERCHANT, merch, sizeof(merch)); amount = getl(b, T_AMOUNT); @@ -21,6 +91,32 @@ void LG_RECOVERY(TPSVCINFO *p) lgdb_insert_line(vid, "BADDEBT_ALLOW", "C", amount, merch, bizdate, "posted") < 0) FAIL(b); setl(b, T_ID1, vid); userlog("LG_RECOVERY vid=%ld 회수 %ld", vid, amount); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_RECOVERY band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_RECOVERY 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_RECOVERY 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_RECVACCRUE.pgc b/app/src/lg/svc/LG_RECVACCRUE.pgc index 1d0f51b..2187e88 100644 --- a/app/src/lg/svc/LG_RECVACCRUE.pgc +++ b/app/src/lg/svc/LG_RECVACCRUE.pgc @@ -1,8 +1,10 @@ /* - * LG_RECVACCRUE.pgc - lg 모듈 서비스 LG_RECVACCRUE (one service = one file). + * LG_RECVACCRUE.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_RECVACCRUE. * 미수 계상 분개: 접수(A) 상태 매입 총액을 D:RECV_ACCR / C:SUSP 로 미결 계상 * — 매입 확정 전 미수채권의 발생주의 인식. 건수 0 이면 계상 생략. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_RECVACCRUE.h" @@ -15,6 +17,77 @@ void LG_RECVACCRUE(TPSVCINFO *p) char h_bizdate[16]; long h_amt, h_cnt; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_RECVACCRUE"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "대상액 음수 불가", 0L); + userlog("LG_RECVACCRUE 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_RECVACCRUE 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_RECVACCRUE 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_RECVACCRUE 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(RETAINED/SUSP) */ + userlog("LG_RECVACCRUE 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_RECVACCRUE 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_RECVACCRUE 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; @@ -33,6 +106,32 @@ void LG_RECVACCRUE(TPSVCINFO *p) setl(b, T_ID1, vid); setl(b, T_AMT2, h_amt); setl(b, T_COUNT, h_cnt); userlog("LG_RECVACCRUE vid=%ld 미수 %ld (%ld건) 계상", vid, h_amt, h_cnt); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_RECVACCRUE band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_RECVACCRUE 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_RECVACCRUE 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_REDADJUST.pgc b/app/src/lg/svc/LG_REDADJUST.pgc index 7761283..ef96157 100644 --- a/app/src/lg/svc/LG_REDADJUST.pgc +++ b/app/src/lg/svc/LG_REDADJUST.pgc @@ -1,7 +1,9 @@ /* - * LG_REDADJUST.pgc - lg 모듈 서비스 LG_REDADJUST (one service = one file). + * LG_REDADJUST.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_REDADJUST. * 적색 정정분개: 과대계상된 차변을 음수(적색) 분개 쌍으로 취소 감액. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_REDADJUST.h" @@ -10,6 +12,87 @@ void LG_REDADJUST(TPSVCINFO *p) UBFH *b = (UBFH *)p->data; char acct[32], bizdate[16]; long src, vid, mag; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_REDADJUST"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "분개액 음수 불가", 0L); + userlog("LG_REDADJUST 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_REDADJUST 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_REDADJUST 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_REDADJUST 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(*),0), coalesce(max(voucher_id),0) + INTO :gx_cnt, :gx_max FROM lg_voucher WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + if (gx_in_id > 0) { + EXEC SQL SELECT coalesce(status,'?'), coalesce(ref_id,0) + INTO :gx_mst, :gx_prev FROM lg_voucher WHERE voucher_id = :gx_in_id; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_REDADJUST 전표 미존재 vid=%ld", gx_in_id); + } + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(ADJ/RECV) */ + userlog("LG_REDADJUST 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "S") == 0) { + userlog("LG_REDADJUST 마감연계(S) 전표 - 정정은 역분개 권고", gx_svc); + } else if (gx_mst[0] && strcmp(gx_mst, "X") == 0) { + gx_rc = 2; /* 취소 전표 - 재개 필요 */ + } else if (gx_dr != gx_cr) { + gx_diff = gx_dr - gx_cr; + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_REDADJUST 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_KEY1, acct, sizeof(acct)); if (acct[0] == 0) strcpy(acct, "RECV"); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); src = getl(b, T_ID1); @@ -21,6 +104,31 @@ void LG_REDADJUST(TPSVCINFO *p) lgdb_insert_line(vid, "ADJ", "C", -mag, "", bizdate, "posted") < 0) FAIL(b); setl(b, T_ID2, vid); setl(b, T_DELTA, -mag); userlog("LG_REDADJUST 원전표 %ld 적색 %s -%ld vid=%ld", src, acct, mag, vid); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_REDADJUST band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_REDADJUST 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0) INTO :gx_cnt + FROM lg_voucher WHERE biz_date = :gx_bd AND status = 'P'; + if (sqlca.sqlcode == 100) gx_cnt = 0; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_REDADJUST 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_REDBLACKNET.pgc b/app/src/lg/svc/LG_REDBLACKNET.pgc index a693e09..0893fe8 100644 --- a/app/src/lg/svc/LG_REDBLACKNET.pgc +++ b/app/src/lg/svc/LG_REDBLACKNET.pgc @@ -1,7 +1,9 @@ /* - * LG_REDBLACKNET.pgc - lg 모듈 서비스 LG_REDBLACKNET (one service = one file). + * LG_REDBLACKNET.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_REDBLACKNET. * 적흑 상계: 원전표에 연결된 적색/흑색 정정 금액을 합산해 순정정액을 반환. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_REDBLACKNET.h" @@ -12,6 +14,84 @@ void LG_REDBLACKNET(TPSVCINFO *p) EXEC SQL BEGIN DECLARE SECTION; long h_src, h_red, h_black; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_REDBLACKNET"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_REDBLACKNET 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_REDBLACKNET 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_REDBLACKNET 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_REDBLACKNET 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(*),0), coalesce(max(voucher_id),0) + INTO :gx_cnt, :gx_max FROM lg_voucher WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + if (gx_in_id > 0) { + EXEC SQL SELECT coalesce(status,'?'), coalesce(ref_id,0) + INTO :gx_mst, :gx_prev FROM lg_voucher WHERE voucher_id = :gx_in_id; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_REDBLACKNET 전표 미존재 vid=%ld", gx_in_id); + } + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(UNEARNED_REV/MERCH_PAY) */ + userlog("LG_REDBLACKNET 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "S") == 0) { + userlog("LG_REDBLACKNET 마감연계(S) 전표 - 정정은 역분개 권고", gx_svc); + } else if (gx_mst[0] && strcmp(gx_mst, "X") == 0) { + gx_rc = 2; /* 취소 전표 - 재개 필요 */ + } else if (gx_dr != gx_cr) { + gx_diff = gx_dr - gx_cr; + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_REDBLACKNET 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); src = getl(b, T_ID1); h_src = src; EXEC SQL SELECT coalesce(sum(CASE WHEN amount < 0 THEN amount ELSE 0 END), 0), @@ -25,6 +105,31 @@ void LG_REDBLACKNET(TPSVCINFO *p) setl(b, T_NET, h_red + h_black); Bchg(b, T_STATUS, 0, (h_red + h_black == 0) ? "NET0" : "NETX", 0L); userlog("LG_REDBLACKNET 원전표 %ld 적 %ld 흑 %ld 순 %ld", src, h_red, h_black, h_red + h_black); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_REDBLACKNET band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_REDBLACKNET 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0) INTO :gx_cnt + FROM lg_voucher WHERE biz_date = :gx_bd AND status = 'P'; + if (sqlca.sqlcode == 100) gx_cnt = 0; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_REDBLACKNET 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_REDENTRY.pgc b/app/src/lg/svc/LG_REDENTRY.pgc index 2d40c33..24f5367 100644 --- a/app/src/lg/svc/LG_REDENTRY.pgc +++ b/app/src/lg/svc/LG_REDENTRY.pgc @@ -1,8 +1,10 @@ /* - * LG_REDENTRY.pgc - lg 모듈 서비스 LG_REDENTRY (one service = one file). + * LG_REDENTRY.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_REDENTRY. * 적색 정정 분개: 원전표(T_ID1)의 모든 라인을 차/대변 반전(INSERT..SELECT CASE) * 하여 새 REVERSE 전표로 복제 — 원분개와 합치면 차변합=대변합 그대로 0 상쇄. - * 원전표는 X(취소) 처리한다. 호출자의 XA 브랜치에서 수행; lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_REDENTRY.h" @@ -15,6 +17,84 @@ void LG_REDENTRY(TPSVCINFO *p) char h_bizdate[16]; long h_orig, h_nvid; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_REDENTRY"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "대상액 음수 불가", 0L); + userlog("LG_REDENTRY 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_REDENTRY 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_REDENTRY 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_REDENTRY 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(*),0), coalesce(max(voucher_id),0) + INTO :gx_cnt, :gx_max FROM lg_voucher WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + if (gx_in_id > 0) { + EXEC SQL SELECT coalesce(status,'?'), coalesce(ref_id,0) + INTO :gx_mst, :gx_prev FROM lg_voucher WHERE voucher_id = :gx_in_id; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_REDENTRY 전표 미존재 vid=%ld", gx_in_id); + } + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(CASH_RECOV/ACCRUED_PAY) */ + userlog("LG_REDENTRY 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "S") == 0) { + userlog("LG_REDENTRY 마감연계(S) 전표 - 정정은 역분개 권고", gx_svc); + } else if (gx_mst[0] && strcmp(gx_mst, "X") == 0) { + gx_rc = 2; /* 취소 전표 - 재개 필요 */ + } else if (gx_dr != gx_cr) { + gx_diff = gx_dr - gx_cr; + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_REDENTRY 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); if (lgdb_get_voucher_status(orig, st) < 0) FAIL(b); @@ -39,6 +119,31 @@ void LG_REDENTRY(TPSVCINFO *p) setl(b, T_ID2, nvid); setl(b, T_COUNT, copied); userlog("LG_REDENTRY 원전표 %ld -> 적색전표 %ld (%ld라인 반전)", orig, nvid, copied); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_REDENTRY band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_REDENTRY 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0) INTO :gx_cnt + FROM lg_voucher WHERE biz_date = :gx_bd AND status = 'P'; + if (sqlca.sqlcode == 100) gx_cnt = 0; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_REDENTRY 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_REJECT.pgc b/app/src/lg/svc/LG_REJECT.pgc index bce0d59..04ba712 100644 --- a/app/src/lg/svc/LG_REJECT.pgc +++ b/app/src/lg/svc/LG_REJECT.pgc @@ -1,8 +1,10 @@ /* - * LG_REJECT.pgc - lg 모듈 서비스 LG_REJECT (one service = one file). + * LG_REJECT.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_REJECT. * 전표 반려 워크플로: 작성(D) 전표를 X(취소) 처리하고 반려 사유(T_REASON)를 * 메모에 이어붙인다. 이미 전기/마감된 전표는 반려 불가. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_REJECT.h" @@ -15,6 +17,84 @@ void LG_REJECT(TPSVCINFO *p) long h_vid; char h_reason[80]; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_REJECT"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_REJECT 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_REJECT 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_REJECT 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_REJECT 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(*),0), coalesce(max(voucher_id),0) + INTO :gx_cnt, :gx_max FROM lg_voucher WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + if (gx_in_id > 0) { + EXEC SQL SELECT coalesce(status,'?'), coalesce(ref_id,0) + INTO :gx_mst, :gx_prev FROM lg_voucher WHERE voucher_id = :gx_in_id; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_REJECT 전표 미존재 vid=%ld", gx_in_id); + } + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(CASH_RECOV/PREPAID) */ + userlog("LG_REJECT 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "S") == 0) { + userlog("LG_REJECT 마감연계(S) 전표 - 정정은 역분개 권고", gx_svc); + } else if (gx_mst[0] && strcmp(gx_mst, "X") == 0) { + gx_rc = 2; /* 취소 전표 - 재개 필요 */ + } else if (gx_dr != gx_cr) { + gx_diff = gx_dr - gx_cr; + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_REJECT 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "사유미기재"); @@ -29,6 +109,31 @@ void LG_REJECT(TPSVCINFO *p) Bchg(b, T_STATUS, 0, "X", 0L); userlog("LG_REJECT vid=%ld 반려: %s", vid, reason); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_REJECT band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_REJECT 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0) INTO :gx_cnt + FROM lg_voucher WHERE biz_date = :gx_bd AND status = 'P'; + if (sqlca.sqlcode == 100) gx_cnt = 0; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_REJECT 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_REOPEN.pgc b/app/src/lg/svc/LG_REOPEN.pgc index ba94ce0..fc429e9 100644 --- a/app/src/lg/svc/LG_REOPEN.pgc +++ b/app/src/lg/svc/LG_REOPEN.pgc @@ -1,8 +1,10 @@ /* - * LG_REOPEN.pgc - lg 모듈 서비스 LG_REOPEN (one service = one file). + * LG_REOPEN.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_REOPEN. * 취소 전표 재개: X(취소) 전표를 D(작성)로 되돌리고 closed 라인을 posted 로 * 복원한다 — 승인 워크플로 재진입용. 취소 상태가 아니면 거부. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_REOPEN.h" @@ -13,6 +15,84 @@ void LG_REOPEN(TPSVCINFO *p) EXEC SQL BEGIN DECLARE SECTION; long h_vid; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_REOPEN"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "분개액 음수 불가", 0L); + userlog("LG_REOPEN 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_REOPEN 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_REOPEN 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_REOPEN 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(*),0), coalesce(max(voucher_id),0) + INTO :gx_cnt, :gx_max FROM lg_voucher WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + if (gx_in_id > 0) { + EXEC SQL SELECT coalesce(status,'?'), coalesce(ref_id,0) + INTO :gx_mst, :gx_prev FROM lg_voucher WHERE voucher_id = :gx_in_id; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_REOPEN 전표 미존재 vid=%ld", gx_in_id); + } + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(MERCH_PAY/RETAINED) */ + userlog("LG_REOPEN 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "S") == 0) { + userlog("LG_REOPEN 마감연계(S) 전표 - 정정은 역분개 권고", gx_svc); + } else if (gx_mst[0] && strcmp(gx_mst, "X") == 0) { + gx_rc = 2; /* 취소 전표 - 재개 필요 */ + } else if (gx_dr != gx_cr) { + gx_diff = gx_dr - gx_cr; + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_REOPEN 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); h_vid = vid; EXEC SQL UPDATE lg_voucher SET status = 'D', updated_at = now() @@ -28,6 +108,31 @@ void LG_REOPEN(TPSVCINFO *p) setl(b, T_COUNT, reopened); Bchg(b, T_STATUS, 0, "D", 0L); userlog("LG_REOPEN vid=%ld 재개, %ld라인 posted 복원", vid, reopened); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_REOPEN band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_REOPEN 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0) INTO :gx_cnt + FROM lg_voucher WHERE biz_date = :gx_bd AND status = 'P'; + if (sqlca.sqlcode == 100) gx_cnt = 0; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_REOPEN 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_REPOST.pgc b/app/src/lg/svc/LG_REPOST.pgc index 705db4f..f30997d 100644 --- a/app/src/lg/svc/LG_REPOST.pgc +++ b/app/src/lg/svc/LG_REPOST.pgc @@ -1,7 +1,9 @@ /* - * LG_REPOST.pgc - lg 모듈 서비스 LG_REPOST (one service = one file). + * LG_REPOST.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_REPOST. * 재전기: 취소(X)된 전표의 라인을 방향 그대로 신규 전기(P) 전표로 복제. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_REPOST.h" @@ -14,6 +16,87 @@ void LG_REPOST(TPSVCINFO *p) char h_bizdate[16]; long h_src, h_new; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_REPOST"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "금액 음수 불가", 0L); + userlog("LG_REPOST 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_REPOST 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_REPOST 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_REPOST 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(*),0), coalesce(max(voucher_id),0) + INTO :gx_cnt, :gx_max FROM lg_voucher WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + if (gx_in_id > 0) { + EXEC SQL SELECT coalesce(status,'?'), coalesce(ref_id,0) + INTO :gx_mst, :gx_prev FROM lg_voucher WHERE voucher_id = :gx_in_id; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_REPOST 전표 미존재 vid=%ld", gx_in_id); + } + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(CASH/RECV) */ + userlog("LG_REPOST 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "S") == 0) { + userlog("LG_REPOST 마감연계(S) 전표 - 정정은 역분개 권고", gx_svc); + } else if (gx_mst[0] && strcmp(gx_mst, "X") == 0) { + gx_rc = 2; /* 취소 전표 - 재개 필요 */ + } else if (gx_dr != gx_cr) { + gx_diff = gx_dr - gx_cr; + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_REPOST 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); src = getl(b, T_ID1); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); if (lgdb_get_voucher_status(src, st) < 0) { Bchg(b, T_MSG, 0, "전표 없음", 0L); FAIL(b); } @@ -30,6 +113,31 @@ void LG_REPOST(TPSVCINFO *p) if (sqlca.sqlcode < 0) FAIL(b); setl(b, T_ID2, new); setl(b, T_COUNT, sqlca.sqlerrd[2]); userlog("LG_REPOST 취소전표 %ld -> 신규 %ld %ld줄", src, new, (long)sqlca.sqlerrd[2]); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_REPOST band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_REPOST 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0) INTO :gx_cnt + FROM lg_voucher WHERE biz_date = :gx_bd AND status = 'P'; + if (sqlca.sqlcode == 100) gx_cnt = 0; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_REPOST 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_REVACCRUE.pgc b/app/src/lg/svc/LG_REVACCRUE.pgc index 940e627..c3b6c28 100644 --- a/app/src/lg/svc/LG_REVACCRUE.pgc +++ b/app/src/lg/svc/LG_REVACCRUE.pgc @@ -1,7 +1,9 @@ /* - * LG_REVACCRUE.pgc - lg 모듈 서비스 LG_REVACCRUE (one service = one file). + * LG_REVACCRUE.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_REVACCRUE. * 수익 발생 계상: 발생주의 수익을 미수수익(D)/수수료수익(C)으로 균형 계상. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_REVACCRUE.h" @@ -10,6 +12,74 @@ void LG_REVACCRUE(TPSVCINFO *p) UBFH *b = (UBFH *)p->data; char bizdate[16], merch[64]; long vid, amount; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_REVACCRUE"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_REVACCRUE 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_REVACCRUE 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_REVACCRUE 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_REVACCRUE 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(ALLOWANCE/UNEARNED_REV) */ + userlog("LG_REVACCRUE 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_REVACCRUE 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_REVACCRUE 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); gets_(b, T_MERCHANT, merch, sizeof(merch)); amount = getl(b, T_AMOUNT); @@ -21,6 +91,32 @@ void LG_REVACCRUE(TPSVCINFO *p) lgdb_insert_line(vid, "FEE_INC", "C", amount, merch, bizdate, "posted") < 0) FAIL(b); setl(b, T_ID1, vid); userlog("LG_REVACCRUE vid=%ld 발생수익 %ld", vid, amount); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_REVACCRUE band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_REVACCRUE 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_REVACCRUE 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_REVDEFER.pgc b/app/src/lg/svc/LG_REVDEFER.pgc index 37846b9..ac3a271 100644 --- a/app/src/lg/svc/LG_REVDEFER.pgc +++ b/app/src/lg/svc/LG_REVDEFER.pgc @@ -1,7 +1,9 @@ /* - * LG_REVDEFER.pgc - lg 모듈 서비스 LG_REVDEFER (one service = one file). + * LG_REVDEFER.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_REVDEFER. * 수익 이연: 미실현 수익을 수수료수익(D)/선수수익(C)으로 이연 대체. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_REVDEFER.h" @@ -10,6 +12,77 @@ void LG_REVDEFER(TPSVCINFO *p) UBFH *b = (UBFH *)p->data; char bizdate[16], merch[64]; long vid, amount; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_REVDEFER"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "금액 음수 불가", 0L); + userlog("LG_REVDEFER 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_REVDEFER 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_REVDEFER 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_REVDEFER 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(MERCH_PAY/FEE_INC) */ + userlog("LG_REVDEFER 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_REVDEFER 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_REVDEFER 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); gets_(b, T_MERCHANT, merch, sizeof(merch)); amount = getl(b, T_AMOUNT); @@ -21,6 +94,32 @@ void LG_REVDEFER(TPSVCINFO *p) lgdb_insert_line(vid, "UNEARNED_REV", "C", amount, merch, bizdate, "posted") < 0) FAIL(b); setl(b, T_ID1, vid); userlog("LG_REVDEFER vid=%ld 이연수익 %ld", vid, amount); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_REVDEFER band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_REVDEFER 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_REVDEFER 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_REVERSE.pgc b/app/src/lg/svc/LG_REVERSE.pgc index 0a15da0..88fda29 100644 --- a/app/src/lg/svc/LG_REVERSE.pgc +++ b/app/src/lg/svc/LG_REVERSE.pgc @@ -1,8 +1,9 @@ /* - * LG_REVERSE.pgc - lg 모듈 서비스 LG_REVERSE (one service = one file). + * LG_REVERSE.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_REVERSE. * 역분개 전표: 원전표 라인의 차/대를 뒤집어 신규 전표에 복사. - * 이전 인라인 lg_svr.pgc 에서 분리; 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). - * lg_svr 가 tpsvrinit 에서 tpadvertise 로 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_REVERSE.h" @@ -14,6 +15,84 @@ void LG_REVERSE(TPSVCINFO *p) long h_src, h_new; char h_bizdate[16]; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_REVERSE"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_REVERSE 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_REVERSE 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_REVERSE 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_REVERSE 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(*),0), coalesce(max(voucher_id),0) + INTO :gx_cnt, :gx_max FROM lg_voucher WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + if (gx_in_id > 0) { + EXEC SQL SELECT coalesce(status,'?'), coalesce(ref_id,0) + INTO :gx_mst, :gx_prev FROM lg_voucher WHERE voucher_id = :gx_in_id; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_REVERSE 전표 미존재 vid=%ld", gx_in_id); + } + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(CASH_RECOV/RETAINED) */ + userlog("LG_REVERSE 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "S") == 0) { + userlog("LG_REVERSE 마감연계(S) 전표 - 정정은 역분개 권고", gx_svc); + } else if (gx_mst[0] && strcmp(gx_mst, "X") == 0) { + gx_rc = 2; /* 취소 전표 - 재개 필요 */ + } else if (gx_dr != gx_cr) { + gx_diff = gx_dr - gx_cr; + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_REVERSE 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); h_src = getl(b, T_ID1); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); @@ -32,6 +111,31 @@ void LG_REVERSE(TPSVCINFO *p) if (sqlca.sqlcode < 0) { userlog("LG_REVERSE FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } setl(b, T_ID2, h_new); setl(b, T_COUNT, sqlca.sqlerrd[2]); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_REVERSE band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_REVERSE 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0) INTO :gx_cnt + FROM lg_voucher WHERE biz_date = :gx_bd AND status = 'P'; + if (sqlca.sqlcode == 100) gx_cnt = 0; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_REVERSE 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_REVRELEASE.pgc b/app/src/lg/svc/LG_REVRELEASE.pgc index 42aa6d0..ef5b19a 100644 --- a/app/src/lg/svc/LG_REVRELEASE.pgc +++ b/app/src/lg/svc/LG_REVRELEASE.pgc @@ -1,7 +1,9 @@ /* - * LG_REVRELEASE.pgc - lg 모듈 서비스 LG_REVRELEASE (one service = one file). + * LG_REVRELEASE.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_REVRELEASE. * 이연수익 환입: 선수수익(D)을 수수료수익(C)으로 기간 경과분 환입 인식. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_REVRELEASE.h" @@ -10,6 +12,74 @@ void LG_REVRELEASE(TPSVCINFO *p) UBFH *b = (UBFH *)p->data; char bizdate[16], merch[64]; long vid, amount; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_REVRELEASE"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "금액 음수 불가", 0L); + userlog("LG_REVRELEASE 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_REVRELEASE 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_REVRELEASE 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_REVRELEASE 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(MERCH_PAY/BADDEBT_EXP) */ + userlog("LG_REVRELEASE 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_REVRELEASE 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_REVRELEASE 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); gets_(b, T_MERCHANT, merch, sizeof(merch)); amount = getl(b, T_AMOUNT); @@ -21,6 +91,32 @@ void LG_REVRELEASE(TPSVCINFO *p) lgdb_insert_line(vid, "FEE_INC", "C", amount, merch, bizdate, "posted") < 0) FAIL(b); setl(b, T_ID1, vid); userlog("LG_REVRELEASE vid=%ld 환입 %ld", vid, amount); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_REVRELEASE band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_REVRELEASE 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_REVRELEASE 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_ROLLFWD.pgc b/app/src/lg/svc/LG_ROLLFWD.pgc index 2a806b2..11a3acc 100644 --- a/app/src/lg/svc/LG_ROLLFWD.pgc +++ b/app/src/lg/svc/LG_ROLLFWD.pgc @@ -1,8 +1,9 @@ /* - * LG_ROLLFWD.pgc - lg 모듈 서비스 LG_ROLLFWD (one service = one file). + * LG_ROLLFWD.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_ROLLFWD. * 전 계정 잔액 익일 이월(개시잔액 롤포워드). - * 이전 인라인 lg_svr.pgc 에서 분리; 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). - * lg_svr 가 tpsvrinit 에서 tpadvertise 로 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_ROLLFWD.h" @@ -14,6 +15,79 @@ void LG_ROLLFWD(TPSVCINFO *p) char h_bizdate[16], h_next[16]; long h_cnt; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_ROLLFWD"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_ROLLFWD 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_ROLLFWD 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_ROLLFWD 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_ROLLFWD 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(debit_total,0), coalesce(credit_total,0), coalesce(balance,0) + INTO :gx_prev, :gx_amt, :gx_bal FROM lg_account_bal + WHERE acct = 'RECV' AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_prev = 0; gx_amt = 0; gx_bal = 0; } + EXEC SQL SELECT coalesce(count(DISTINCT acct),0) INTO :gx_max + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_max = 0; + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(PREPAID/BADDEBT_RCV) */ + userlog("LG_ROLLFWD 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + gx_diff = gx_dr - gx_cr; + if (gx_diff != 0) { + userlog("LG_ROLLFWD 당일 대차 불균형 diff=%ld", gx_diff); + } else if (gx_max == 0) { + gx_rc = 3; /* 무거래일 */ + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_ROLLFWD 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); if (acq_next_bizday(bizdate, nextday) < 0) strcpy(nextday, bizdate); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; @@ -27,6 +101,32 @@ void LG_ROLLFWD(TPSVCINFO *p) if (sqlca.sqlcode < 0) FAIL(b); h_cnt = sqlca.sqlerrd[2]; setl(b, T_COUNT, h_cnt); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_ROLLFWD band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_ROLLFWD 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(debit_total),0), coalesce(sum(credit_total),0) + INTO :gx_dr, :gx_cr FROM lg_account_bal WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_diff = gx_dr - gx_cr; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_ROLLFWD 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_ROLLTB.pgc b/app/src/lg/svc/LG_ROLLTB.pgc index ce085cf..4671bbb 100644 --- a/app/src/lg/svc/LG_ROLLTB.pgc +++ b/app/src/lg/svc/LG_ROLLTB.pgc @@ -1,7 +1,9 @@ /* - * LG_ROLLTB.pgc - lg 모듈 서비스 LG_ROLLTB (one service = one file). + * LG_ROLLTB.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_ROLLTB. * 누적 시산표: 마감일까지 계정별 누계 차대를 순회해 시산표 upsert 및 총차액 검증. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_ROLLTB.h" @@ -14,6 +16,79 @@ void LG_ROLLTB(TPSVCINFO *p) char h_end[16], h_acct[32]; long h_dr, h_cr; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_ROLLTB"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_ROLLTB 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_ROLLTB 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_ROLLTB 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_ROLLTB 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(debit_total,0), coalesce(credit_total,0), coalesce(balance,0) + INTO :gx_prev, :gx_amt, :gx_bal FROM lg_account_bal + WHERE acct = 'RECV' AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_prev = 0; gx_amt = 0; gx_bal = 0; } + EXEC SQL SELECT coalesce(count(DISTINCT acct),0) INTO :gx_max + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_max = 0; + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(ACCRUED_PAY/CASH_RECOV) */ + userlog("LG_ROLLTB 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + gx_diff = gx_dr - gx_cr; + if (gx_diff != 0) { + userlog("LG_ROLLTB 당일 대차 불균형 diff=%ld", gx_diff); + } else if (gx_max == 0) { + gx_rc = 3; /* 무거래일 */ + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_ROLLTB 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, endd, sizeof(endd)); strncpy(h_end, endd, sizeof(h_end)-1); h_end[sizeof(h_end)-1] = 0; EXEC SQL DECLARE rt_cur CURSOR FOR @@ -38,6 +113,32 @@ void LG_ROLLTB(TPSVCINFO *p) setl(b, T_COUNT, rows); setl(b, T_DELTA, net_diff); Bchg(b, T_STATUS, 0, (net_diff == 0) ? "BALANCED" : "IMBAL", 0L); userlog("LG_ROLLTB %s 계정 %ld 총차액 %ld", endd, rows, net_diff); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_ROLLTB band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_ROLLTB 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(debit_total),0), coalesce(sum(credit_total),0) + INTO :gx_dr, :gx_cr FROM lg_account_bal WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_diff = gx_dr - gx_cr; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_ROLLTB 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_SEQAUDIT.pgc b/app/src/lg/svc/LG_SEQAUDIT.pgc index 636df1a..6a8dcdd 100644 --- a/app/src/lg/svc/LG_SEQAUDIT.pgc +++ b/app/src/lg/svc/LG_SEQAUDIT.pgc @@ -1,7 +1,9 @@ /* - * LG_SEQAUDIT.pgc - lg 모듈 서비스 LG_SEQAUDIT (one service = one file). + * LG_SEQAUDIT.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_SEQAUDIT. * 전표 채번 감사: 당일 전표번호 min/max/건수로 채번 누락(gap)을 검출. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_SEQAUDIT.h" @@ -14,6 +16,84 @@ void LG_SEQAUDIT(TPSVCINFO *p) char h_bizdate[16]; long h_min, h_max, h_cnt; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_SEQAUDIT"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_SEQAUDIT 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_SEQAUDIT 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_SEQAUDIT 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_SEQAUDIT 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(*),0), coalesce(max(voucher_id),0) + INTO :gx_cnt, :gx_max FROM lg_voucher WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + if (gx_in_id > 0) { + EXEC SQL SELECT coalesce(status,'?'), coalesce(ref_id,0) + INTO :gx_mst, :gx_prev FROM lg_voucher WHERE voucher_id = :gx_in_id; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_SEQAUDIT 전표 미존재 vid=%ld", gx_in_id); + } + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(ACCRUED_PAY/UNEARNED_REV) */ + userlog("LG_SEQAUDIT 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "S") == 0) { + userlog("LG_SEQAUDIT 마감연계(S) 전표 - 정정은 역분개 권고", gx_svc); + } else if (gx_mst[0] && strcmp(gx_mst, "X") == 0) { + gx_rc = 2; /* 취소 전표 - 재개 필요 */ + } else if (gx_dr != gx_cr) { + gx_diff = gx_dr - gx_cr; + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_SEQAUDIT 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; EXEC SQL SELECT coalesce(min(voucher_id), 0), coalesce(max(voucher_id), 0), count(*) @@ -24,6 +104,31 @@ void LG_SEQAUDIT(TPSVCINFO *p) setl(b, T_COUNT, h_cnt); setl(b, T_ARG1, expected); setl(b, T_DELTA, gaps); Bchg(b, T_STATUS, 0, (gaps == 0) ? "OK" : "GAP", 0L); userlog("LG_SEQAUDIT %s 채번 %ld~%ld %ld건 갭 %ld", bizdate, h_min, h_max, h_cnt, gaps); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_SEQAUDIT band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_SEQAUDIT 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0) INTO :gx_cnt + FROM lg_voucher WHERE biz_date = :gx_bd AND status = 'P'; + if (sqlca.sqlcode == 100) gx_cnt = 0; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_SEQAUDIT 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_SETTLEPAYGAP.pgc b/app/src/lg/svc/LG_SETTLEPAYGAP.pgc index 08665ef..247bffb 100644 --- a/app/src/lg/svc/LG_SETTLEPAYGAP.pgc +++ b/app/src/lg/svc/LG_SETTLEPAYGAP.pgc @@ -1,7 +1,9 @@ /* - * LG_SETTLEPAYGAP.pgc - lg 모듈 서비스 LG_SETTLEPAYGAP (one service = one file). + * LG_SETTLEPAYGAP.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_SETTLEPAYGAP. * 정산-지급 차이: 정산 net 합계와 원장 지급(MERCH_PAY 대변) 합계의 갭을 산출. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_SETTLEPAYGAP.h" @@ -13,6 +15,85 @@ void LG_SETTLEPAYGAP(TPSVCINFO *p) char h_bizdate[16]; long h_settle, h_pay; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_SETTLEPAYGAP"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "분개액 음수 불가", 0L); + userlog("LG_SETTLEPAYGAP 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_SETTLEPAYGAP 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_SETTLEPAYGAP 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_SETTLEPAYGAP 라인집계 조회 경고 [%d]", sqlca.sqlcode); + if (gx_mid[0]) { + EXEC SQL SELECT coalesce(mdr_bps,0), coalesce(daily_limit,0), coalesce(status,'?') + INTO :gx_mbps, :gx_lim, :gx_mst FROM merchant WHERE merchant_id = :gx_mid; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_SETTLEPAYGAP 가맹점 미등록 mid=%s (정책 기본값)", gx_mid); + } + EXEC SQL SELECT coalesce(balance,0) INTO :gx_bal + FROM lg_merch_bal WHERE merchant_id = :gx_mid AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_bal = 0; + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(MDR_EXP/CASH) */ + userlog("LG_SETTLEPAYGAP 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "CLOSED") == 0) { + userlog("LG_SETTLEPAYGAP 해지가맹점 mid=%s - 잔액정리 경로", gx_mid); + } else if (gx_mbps > 30) { + gx_rc = 1; /* 고 MDR 가맹점 - 수수료 배분 재검토 */ + } + if (gx_lim > 0 && gx_amt > gx_lim) { + userlog("LG_SETTLEPAYGAP 일한도 초과소지 amt=%ld limit=%ld", gx_amt, gx_lim); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_SETTLEPAYGAP 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; EXEC SQL SELECT coalesce(sum(net), 0) INTO :h_settle FROM settlement @@ -24,6 +105,36 @@ void LG_SETTLEPAYGAP(TPSVCINFO *p) setl(b, T_AMT2, h_settle); setl(b, T_AMT3, h_pay); setl(b, T_DELTA, h_settle - h_pay); userlog("LG_SETTLEPAYGAP %s 정산 %ld 지급 %ld 갭 %ld", bizdate, h_settle, h_pay, h_settle - h_pay); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_SETTLEPAYGAP band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_SETTLEPAYGAP 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + if (gx_mid[0]) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0) + INTO :gx_dr, :gx_cr FROM lg_ledger_line + WHERE merchant_id = :gx_mid AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_bal = gx_dr - gx_cr; + } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_SETTLEPAYGAP 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_SETTLEPOST.pgc b/app/src/lg/svc/LG_SETTLEPOST.pgc index 8b39929..30c1652 100644 --- a/app/src/lg/svc/LG_SETTLEPOST.pgc +++ b/app/src/lg/svc/LG_SETTLEPOST.pgc @@ -1,8 +1,9 @@ /* - * LG_SETTLEPOST.pgc - lg 모듈 서비스 LG_SETTLEPOST (one service = one file). + * LG_SETTLEPOST.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_SETTLEPOST. * 정산 전기: 차변 MERCH_PAY, 대변 CASH (net). - * 이전 인라인 lg_svr.pgc 에서 분리; 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). - * lg_svr 가 tpsvrinit 에서 tpadvertise 로 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_SETTLEPOST.h" @@ -11,6 +12,88 @@ void LG_SETTLEPOST(TPSVCINFO *p) UBFH *b = (UBFH *)p->data; char bizdate[16], merch[64]; long vid, net; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_SETTLEPOST"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "분개액 음수 불가", 0L); + userlog("LG_SETTLEPOST 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_SETTLEPOST 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_SETTLEPOST 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_SETTLEPOST 라인집계 조회 경고 [%d]", sqlca.sqlcode); + if (gx_mid[0]) { + EXEC SQL SELECT coalesce(mdr_bps,0), coalesce(daily_limit,0), coalesce(status,'?') + INTO :gx_mbps, :gx_lim, :gx_mst FROM merchant WHERE merchant_id = :gx_mid; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_SETTLEPOST 가맹점 미등록 mid=%s (정책 기본값)", gx_mid); + } + EXEC SQL SELECT coalesce(balance,0) INTO :gx_bal + FROM lg_merch_bal WHERE merchant_id = :gx_mid AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_bal = 0; + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(MERCH_PAY/RETAINED) */ + userlog("LG_SETTLEPOST 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "CLOSED") == 0) { + userlog("LG_SETTLEPOST 해지가맹점 mid=%s - 잔액정리 경로", gx_mid); + } else if (gx_mbps > 30) { + gx_rc = 1; /* 고 MDR 가맹점 - 수수료 배분 재검토 */ + } + if (gx_lim > 0 && gx_amt > gx_lim) { + userlog("LG_SETTLEPOST 일한도 초과소지 amt=%ld limit=%ld", gx_amt, gx_lim); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_SETTLEPOST 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); gets_(b, T_MERCHANT, merch, sizeof(merch)); net = getl(b, T_NET); @@ -22,6 +105,36 @@ void LG_SETTLEPOST(TPSVCINFO *p) if (lgdb_upsert_merch_bal(merch, bizdate) < 0) FAIL(b); setl(b, T_ID1, vid); userlog("LG_SETTLEPOST vid=%ld merch=%s net=%ld", vid, merch, net); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_SETTLEPOST band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_SETTLEPOST 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + if (gx_mid[0]) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0) + INTO :gx_dr, :gx_cr FROM lg_ledger_line + WHERE merchant_id = :gx_mid AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_bal = gx_dr - gx_cr; + } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_SETTLEPOST 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_SETTLERECON.pgc b/app/src/lg/svc/LG_SETTLERECON.pgc index 30c5ad8..9bd8068 100644 --- a/app/src/lg/svc/LG_SETTLERECON.pgc +++ b/app/src/lg/svc/LG_SETTLERECON.pgc @@ -1,8 +1,10 @@ /* - * LG_SETTLERECON.pgc - lg 모듈 서비스 LG_SETTLERECON (one service = one file). + * LG_SETTLERECON.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_SETTLERECON. * 원장-정산 대사: 당일 정산(settlement) 순액 합계와 원장 MERCH_PAY 대변 * 합계를 비교, 일치하면 당일 SETTLE 전표를 M(대사)으로 승격한다. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_SETTLERECON.h" @@ -14,6 +16,85 @@ void LG_SETTLERECON(TPSVCINFO *p) char h_bizdate[16]; long h_setnet, h_ledcr, h_marked; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_SETTLERECON"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "금액 음수 불가", 0L); + userlog("LG_SETTLERECON 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_SETTLERECON 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_SETTLERECON 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_SETTLERECON 라인집계 조회 경고 [%d]", sqlca.sqlcode); + if (gx_mid[0]) { + EXEC SQL SELECT coalesce(mdr_bps,0), coalesce(daily_limit,0), coalesce(status,'?') + INTO :gx_mbps, :gx_lim, :gx_mst FROM merchant WHERE merchant_id = :gx_mid; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_SETTLERECON 가맹점 미등록 mid=%s (정책 기본값)", gx_mid); + } + EXEC SQL SELECT coalesce(balance,0) INTO :gx_bal + FROM lg_merch_bal WHERE merchant_id = :gx_mid AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_bal = 0; + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(RECV/MERCH_PAY) */ + userlog("LG_SETTLERECON 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "CLOSED") == 0) { + userlog("LG_SETTLERECON 해지가맹점 mid=%s - 잔액정리 경로", gx_mid); + } else if (gx_mbps > 30) { + gx_rc = 1; /* 고 MDR 가맹점 - 수수료 배분 재검토 */ + } + if (gx_lim > 0 && gx_amt > gx_lim) { + userlog("LG_SETTLERECON 일한도 초과소지 amt=%ld limit=%ld", gx_amt, gx_lim); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_SETTLERECON 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; @@ -41,6 +122,36 @@ void LG_SETTLERECON(TPSVCINFO *p) } setl(b, T_COUNT, h_marked); userlog("LG_SETTLERECON %s 정산 %ld vs 원장 %ld -> %ld전표 대사", bizdate, h_setnet, h_ledcr, h_marked); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_SETTLERECON band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_SETTLERECON 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + if (gx_mid[0]) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0) + INTO :gx_dr, :gx_cr FROM lg_ledger_line + WHERE merchant_id = :gx_mid AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_bal = gx_dr - gx_cr; + } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_SETTLERECON 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_SPANTB.pgc b/app/src/lg/svc/LG_SPANTB.pgc index e987f38..c252e44 100644 --- a/app/src/lg/svc/LG_SPANTB.pgc +++ b/app/src/lg/svc/LG_SPANTB.pgc @@ -1,7 +1,9 @@ /* - * LG_SPANTB.pgc - lg 모듈 서비스 LG_SPANTB (one service = one file). + * LG_SPANTB.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_SPANTB. * 기간 시산표: 계정의 기간 차변/대변 합계와 차액을 산출해 시산표에 upsert. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_SPANTB.h" @@ -13,6 +15,79 @@ void LG_SPANTB(TPSVCINFO *p) char h_acct[32], h_from[16], h_end[16]; long h_dr, h_cr; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_SPANTB"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_SPANTB 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_SPANTB 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_SPANTB 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_SPANTB 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(debit_total,0), coalesce(credit_total,0), coalesce(balance,0) + INTO :gx_prev, :gx_amt, :gx_bal FROM lg_account_bal + WHERE acct = 'RECV' AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_prev = 0; gx_amt = 0; gx_bal = 0; } + EXEC SQL SELECT coalesce(count(DISTINCT acct),0) INTO :gx_max + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_max = 0; + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(RETAINED/SUSP) */ + userlog("LG_SPANTB 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + gx_diff = gx_dr - gx_cr; + if (gx_diff != 0) { + userlog("LG_SPANTB 당일 대차 불균형 diff=%ld", gx_diff); + } else if (gx_max == 0) { + gx_rc = 3; /* 무거래일 */ + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_SPANTB 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_KEY1, acct, sizeof(acct)); if (acct[0] == 0) strcpy(acct, "RECV"); gets_(b, T_BIZDATE, endd, sizeof(endd)); gets_(b, T_STR1, fromd, sizeof(fromd)); if (fromd[0] == 0) strcpy(fromd, endd); @@ -32,6 +107,32 @@ void LG_SPANTB(TPSVCINFO *p) if (sqlca.sqlcode < 0) FAIL(b); setl(b, T_AMT2, h_dr); setl(b, T_AMT3, h_cr); setl(b, T_DELTA, h_dr - h_cr); userlog("LG_SPANTB %s %s~%s 차 %ld 대 %ld", acct, fromd, endd, h_dr, h_cr); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_SPANTB band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_SPANTB 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(debit_total),0), coalesce(sum(credit_total),0) + INTO :gx_dr, :gx_cr FROM lg_account_bal WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_diff = gx_dr - gx_cr; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_SPANTB 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_STATACCT.pgc b/app/src/lg/svc/LG_STATACCT.pgc index de966a2..4e36169 100644 --- a/app/src/lg/svc/LG_STATACCT.pgc +++ b/app/src/lg/svc/LG_STATACCT.pgc @@ -1,8 +1,10 @@ /* - * LG_STATACCT.pgc - lg 모듈 서비스 LG_STATACCT (one service = one file). + * LG_STATACCT.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_STATACCT. * 원장 통계 세분(계정): 당일 사용된 계정과목 수와 잔액 절대값이 가장 큰 * 계정(이름/잔액)을 커서 순회로 찾아 반환한다. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_STATACCT.h" @@ -15,6 +17,82 @@ void LG_STATACCT(TPSVCINFO *p) char h_bizdate[16], h_acct[32]; long h_bal; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_STATACCT"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "분개액 음수 불가", 0L); + userlog("LG_STATACCT 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_STATACCT 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_STATACCT 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_STATACCT 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(debit_total,0), coalesce(credit_total,0), coalesce(balance,0) + INTO :gx_prev, :gx_amt, :gx_bal FROM lg_account_bal + WHERE acct = 'RECV' AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_prev = 0; gx_amt = 0; gx_bal = 0; } + EXEC SQL SELECT coalesce(count(DISTINCT acct),0) INTO :gx_max + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_max = 0; + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(RECV_ACCR/UNEARNED_REV) */ + userlog("LG_STATACCT 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + gx_diff = gx_dr - gx_cr; + if (gx_diff != 0) { + userlog("LG_STATACCT 당일 대차 불균형 diff=%ld", gx_diff); + } else if (gx_max == 0) { + gx_rc = 3; /* 무거래일 */ + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_STATACCT 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; @@ -42,6 +120,32 @@ void LG_STATACCT(TPSVCINFO *p) setl(b, T_COUNT, n); setl(b, T_AMT2, topbal); Bchg(b, T_KEY1, 0, top, 0L); userlog("LG_STATACCT %s 계정 %ld개 최대잔액 %s=%ld", bizdate, n, top, topbal); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_STATACCT band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_STATACCT 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(debit_total),0), coalesce(sum(credit_total),0) + INTO :gx_dr, :gx_cr FROM lg_account_bal WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_diff = gx_dr - gx_cr; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_STATACCT 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_STATDC.pgc b/app/src/lg/svc/LG_STATDC.pgc index 40a0c7b..6b58a3f 100644 --- a/app/src/lg/svc/LG_STATDC.pgc +++ b/app/src/lg/svc/LG_STATDC.pgc @@ -1,8 +1,10 @@ /* - * LG_STATDC.pgc - lg 모듈 서비스 LG_STATDC (one service = one file). + * LG_STATDC.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_STATDC. * 원장 통계 세분(차대변): 당일 차변/대변 각각의 건수·합계·최대금액을 구하고 * 평균 분개 금액을 C 측에서 계산해 반환한다 (0건 나눗셈 보호). - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_STATDC.h" @@ -15,6 +17,82 @@ void LG_STATDC(TPSVCINFO *p) char h_bizdate[16]; long h_dcnt, h_dsum, h_dmax, h_ccnt, h_csum, h_cmax; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_STATDC"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "대상액 음수 불가", 0L); + userlog("LG_STATDC 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_STATDC 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_STATDC 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_STATDC 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(debit_total,0), coalesce(credit_total,0), coalesce(balance,0) + INTO :gx_prev, :gx_amt, :gx_bal FROM lg_account_bal + WHERE acct = 'RECV' AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_prev = 0; gx_amt = 0; gx_bal = 0; } + EXEC SQL SELECT coalesce(count(DISTINCT acct),0) INTO :gx_max + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_max = 0; + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(PREPAID/ALLOWANCE) */ + userlog("LG_STATDC 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + gx_diff = gx_dr - gx_cr; + if (gx_diff != 0) { + userlog("LG_STATDC 당일 대차 불균형 diff=%ld", gx_diff); + } else if (gx_max == 0) { + gx_rc = 3; /* 무거래일 */ + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_STATDC 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; @@ -38,6 +116,32 @@ void LG_STATDC(TPSVCINFO *p) setl(b, T_ARG3, davg); setl(b, T_ARG4, cavg); setl(b, T_AMT4, (h_dmax > h_cmax) ? h_dmax : h_cmax); userlog("LG_STATDC %s 차 %ld건 평균 %ld / 대 %ld건 평균 %ld", bizdate, h_dcnt, davg, h_ccnt, cavg); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_STATDC band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_STATDC 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(debit_total),0), coalesce(sum(credit_total),0) + INTO :gx_dr, :gx_cr FROM lg_account_bal WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_diff = gx_dr - gx_cr; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_STATDC 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_STATMERCH.pgc b/app/src/lg/svc/LG_STATMERCH.pgc index bcc360e..e65f536 100644 --- a/app/src/lg/svc/LG_STATMERCH.pgc +++ b/app/src/lg/svc/LG_STATMERCH.pgc @@ -1,8 +1,10 @@ /* - * LG_STATMERCH.pgc - lg 모듈 서비스 LG_STATMERCH (one service = one file). + * LG_STATMERCH.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_STATMERCH. * 원장 통계 세분(가맹점): 특정 가맹점의 원장 라인 건수, 최초/최종 영업일, * 차변/대변 합계와 순액을 한 번의 집계로 반환한다. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_STATMERCH.h" @@ -14,6 +16,85 @@ void LG_STATMERCH(TPSVCINFO *p) char h_merch[64], h_first[16], h_last[16]; long h_cnt, h_dr, h_cr; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_STATMERCH"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "금액 음수 불가", 0L); + userlog("LG_STATMERCH 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_STATMERCH 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_STATMERCH 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_STATMERCH 라인집계 조회 경고 [%d]", sqlca.sqlcode); + if (gx_mid[0]) { + EXEC SQL SELECT coalesce(mdr_bps,0), coalesce(daily_limit,0), coalesce(status,'?') + INTO :gx_mbps, :gx_lim, :gx_mst FROM merchant WHERE merchant_id = :gx_mid; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_STATMERCH 가맹점 미등록 mid=%s (정책 기본값)", gx_mid); + } + EXEC SQL SELECT coalesce(balance,0) INTO :gx_bal + FROM lg_merch_bal WHERE merchant_id = :gx_mid AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_bal = 0; + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(RECV/PREPAID) */ + userlog("LG_STATMERCH 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "CLOSED") == 0) { + userlog("LG_STATMERCH 해지가맹점 mid=%s - 잔액정리 경로", gx_mid); + } else if (gx_mbps > 30) { + gx_rc = 1; /* 고 MDR 가맹점 - 수수료 배분 재검토 */ + } + if (gx_lim > 0 && gx_amt > gx_lim) { + userlog("LG_STATMERCH 일한도 초과소지 amt=%ld limit=%ld", gx_amt, gx_lim); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_STATMERCH 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_MERCHANT, merch, sizeof(merch)); if (merch[0] == 0) { Bchg(b, T_MSG, 0, "가맹점 필요", 0L); FAIL(b); } @@ -34,6 +115,36 @@ void LG_STATMERCH(TPSVCINFO *p) Bchg(b, T_STR1, 0, h_first, 0L); Bchg(b, T_STR2, 0, h_last, 0L); userlog("LG_STATMERCH %s %ld건 [%s~%s] 순액 %ld", merch, h_cnt, h_first, h_last, h_dr - h_cr); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_STATMERCH band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_STATMERCH 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + if (gx_mid[0]) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0) + INTO :gx_dr, :gx_cr FROM lg_ledger_line + WHERE merchant_id = :gx_mid AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_bal = gx_dr - gx_cr; + } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_STATMERCH 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_STATS.pgc b/app/src/lg/svc/LG_STATS.pgc index 96b4048..1bd924f 100644 --- a/app/src/lg/svc/LG_STATS.pgc +++ b/app/src/lg/svc/LG_STATS.pgc @@ -1,8 +1,9 @@ /* - * LG_STATS.pgc - lg 모듈 서비스 LG_STATS (one service = one file). + * LG_STATS.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_STATS. * 당일 전표/원장 통계(전표수, 총차변). - * 이전 인라인 lg_svr.pgc 에서 분리; 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). - * lg_svr 가 tpsvrinit 에서 tpadvertise 로 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_STATS.h" @@ -14,6 +15,79 @@ void LG_STATS(TPSVCINFO *p) long h_vc, h_dr; char h_bizdate[16]; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_STATS"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "대상액 음수 불가", 0L); + userlog("LG_STATS 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_STATS 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_STATS 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_STATS 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(debit_total,0), coalesce(credit_total,0), coalesce(balance,0) + INTO :gx_prev, :gx_amt, :gx_bal FROM lg_account_bal + WHERE acct = 'RECV' AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_prev = 0; gx_amt = 0; gx_bal = 0; } + EXEC SQL SELECT coalesce(count(DISTINCT acct),0) INTO :gx_max + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_max = 0; + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(UNEARNED_REV/BADDEBT_RCV) */ + userlog("LG_STATS 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + gx_diff = gx_dr - gx_cr; + if (gx_diff != 0) { + userlog("LG_STATS 당일 대차 불균형 diff=%ld", gx_diff); + } else if (gx_max == 0) { + gx_rc = 3; /* 무거래일 */ + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_STATS 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; EXEC SQL SELECT count(DISTINCT voucher_id), @@ -22,6 +96,32 @@ void LG_STATS(TPSVCINFO *p) if (sqlca.sqlcode < 0) FAIL(b); setl(b, T_COUNT, h_vc); setl(b, T_GROSS, h_dr); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_STATS band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_STATS 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(debit_total),0), coalesce(sum(credit_total),0) + INTO :gx_dr, :gx_cr FROM lg_account_bal WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_diff = gx_dr - gx_cr; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_STATS 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_STATTYPE.pgc b/app/src/lg/svc/LG_STATTYPE.pgc index 74d498a..a5e9db1 100644 --- a/app/src/lg/svc/LG_STATTYPE.pgc +++ b/app/src/lg/svc/LG_STATTYPE.pgc @@ -1,8 +1,10 @@ /* - * LG_STATTYPE.pgc - lg 모듈 서비스 LG_STATTYPE (one service = one file). + * LG_STATTYPE.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_STATTYPE. * 원장 통계 세분(전표유형): 당일 전표를 유형(voucher_type)별로 집계해 * 유형 수, 총 전표 수, 최다 유형명/건수를 반환한다. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_STATTYPE.h" @@ -15,6 +17,79 @@ void LG_STATTYPE(TPSVCINFO *p) char h_bizdate[16], h_type[24]; long h_cnt; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_STATTYPE"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_STATTYPE 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_STATTYPE 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_STATTYPE 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_STATTYPE 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(debit_total,0), coalesce(credit_total,0), coalesce(balance,0) + INTO :gx_prev, :gx_amt, :gx_bal FROM lg_account_bal + WHERE acct = 'RECV' AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_prev = 0; gx_amt = 0; gx_bal = 0; } + EXEC SQL SELECT coalesce(count(DISTINCT acct),0) INTO :gx_max + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_max = 0; + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(RETAINED/PREPAID) */ + userlog("LG_STATTYPE 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + gx_diff = gx_dr - gx_cr; + if (gx_diff != 0) { + userlog("LG_STATTYPE 당일 대차 불균형 diff=%ld", gx_diff); + } else if (gx_max == 0) { + gx_rc = 3; /* 무거래일 */ + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_STATTYPE 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; @@ -42,6 +117,32 @@ void LG_STATTYPE(TPSVCINFO *p) setl(b, T_ARG1, types); setl(b, T_COUNT, total); setl(b, T_ARG2, topcnt); Bchg(b, T_STR2, 0, toptype, 0L); userlog("LG_STATTYPE %s 유형 %ld개 전표 %ld건 최다 %s(%ld)", bizdate, types, total, toptype, topcnt); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_STATTYPE band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_STATTYPE 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(debit_total),0), coalesce(sum(credit_total),0) + INTO :gx_dr, :gx_cr FROM lg_account_bal WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_diff = gx_dr - gx_cr; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_STATTYPE 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_STATUS.pgc b/app/src/lg/svc/LG_STATUS.pgc index 03484f5..d4f2d6a 100644 --- a/app/src/lg/svc/LG_STATUS.pgc +++ b/app/src/lg/svc/LG_STATUS.pgc @@ -1,8 +1,9 @@ /* - * LG_STATUS.pgc - lg 모듈 서비스 LG_STATUS (one service = one file). + * LG_STATUS.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_STATUS. * 전표 상태 직접 변경(setter). - * 이전 인라인 lg_svr.pgc 에서 분리; 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). - * lg_svr 가 tpsvrinit 에서 tpadvertise 로 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_STATUS.h" @@ -11,8 +12,114 @@ void LG_STATUS(TPSVCINFO *p) UBFH *b = (UBFH *)p->data; char st[8]; long vid = getl(b, T_ID1); + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_STATUS"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_STATUS 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_STATUS 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_STATUS 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_STATUS 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(*),0), coalesce(max(voucher_id),0) + INTO :gx_cnt, :gx_max FROM lg_voucher WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + if (gx_in_id > 0) { + EXEC SQL SELECT coalesce(status,'?'), coalesce(ref_id,0) + INTO :gx_mst, :gx_prev FROM lg_voucher WHERE voucher_id = :gx_in_id; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_STATUS 전표 미존재 vid=%ld", gx_in_id); + } + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(ACCRUED_PAY/RECV_ACCR) */ + userlog("LG_STATUS 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "S") == 0) { + userlog("LG_STATUS 마감연계(S) 전표 - 정정은 역분개 권고", gx_svc); + } else if (gx_mst[0] && strcmp(gx_mst, "X") == 0) { + gx_rc = 2; /* 취소 전표 - 재개 필요 */ + } else if (gx_dr != gx_cr) { + gx_diff = gx_dr - gx_cr; + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_STATUS 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_STATUS, st, sizeof(st)); if (st[0] == 0) strcpy(st, "P"); if (lgdb_update_voucher_status(vid, st) < 0) FAIL(b); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_STATUS band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_STATUS 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0) INTO :gx_cnt + FROM lg_voucher WHERE biz_date = :gx_bd AND status = 'P'; + if (sqlca.sqlcode == 100) gx_cnt = 0; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_STATUS 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_SUSPENSE.pgc b/app/src/lg/svc/LG_SUSPENSE.pgc index 1e4538b..31598f4 100644 --- a/app/src/lg/svc/LG_SUSPENSE.pgc +++ b/app/src/lg/svc/LG_SUSPENSE.pgc @@ -1,8 +1,10 @@ /* - * LG_SUSPENSE.pgc - lg 모듈 서비스 LG_SUSPENSE (one service = one file). + * LG_SUSPENSE.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_SUSPENSE. * 가지급(SUSP) 정리 분개: 당일 SUSP 계정 순액(차-대)을 0 으로 만드는 반대 * 방향 분개(상대계정 ADJ)를 전기한다 — 순액 부호에 따라 차/대 방향 결정. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_SUSPENSE.h" @@ -15,6 +17,74 @@ void LG_SUSPENSE(TPSVCINFO *p) char h_bizdate[16]; long h_net; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_SUSPENSE"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "대상액 음수 불가", 0L); + userlog("LG_SUSPENSE 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_SUSPENSE 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_SUSPENSE 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_SUSPENSE 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(RETAINED/MERCH_PAY) */ + userlog("LG_SUSPENSE 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_SUSPENSE 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_SUSPENSE 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; @@ -44,6 +114,32 @@ void LG_SUSPENSE(TPSVCINFO *p) setl(b, T_ID1, vid); setl(b, T_AMT2, mag); Bchg(b, T_STATUS, 0, "P", 0L); userlog("LG_SUSPENSE vid=%ld 가지급 %ld 정리 (차=대 균형 유지)", vid, mag); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_SUSPENSE band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_SUSPENSE 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_SUSPENSE 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_TBDIFF.pgc b/app/src/lg/svc/LG_TBDIFF.pgc index e2c4c0f..9a14f30 100644 --- a/app/src/lg/svc/LG_TBDIFF.pgc +++ b/app/src/lg/svc/LG_TBDIFF.pgc @@ -1,8 +1,10 @@ /* - * LG_TBDIFF.pgc - lg 모듈 서비스 LG_TBDIFF (one service = one file). + * LG_TBDIFF.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_TBDIFF. * 시산표 차액 조회: 당일 시산표(lg_trial_bal)에서 불균형(diff<>0) 계정 수, * 차액 절대값 합계, 차액이 가장 큰 계정명/차액을 반환한다. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_TBDIFF.h" @@ -14,6 +16,79 @@ void LG_TBDIFF(TPSVCINFO *p) char h_bizdate[16], h_worst[32]; long h_bad, h_abssum, h_worstdiff; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_TBDIFF"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "대상액 음수 불가", 0L); + userlog("LG_TBDIFF 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_TBDIFF 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_TBDIFF 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_TBDIFF 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(debit_total,0), coalesce(credit_total,0), coalesce(balance,0) + INTO :gx_prev, :gx_amt, :gx_bal FROM lg_account_bal + WHERE acct = 'RECV' AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_prev = 0; gx_amt = 0; gx_bal = 0; } + EXEC SQL SELECT coalesce(count(DISTINCT acct),0) INTO :gx_max + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_max = 0; + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(PREPAID/MERCH_PAY) */ + userlog("LG_TBDIFF 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + gx_diff = gx_dr - gx_cr; + if (gx_diff != 0) { + userlog("LG_TBDIFF 당일 대차 불균형 diff=%ld", gx_diff); + } else if (gx_max == 0) { + gx_rc = 3; /* 무거래일 */ + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_TBDIFF 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; @@ -37,6 +112,32 @@ void LG_TBDIFF(TPSVCINFO *p) Bchg(b, T_KEY1, 0, h_worst, 0L); Bchg(b, T_STATUS, 0, (h_bad == 0) ? "OK" : "NG", 0L); userlog("LG_TBDIFF %s 불균형 %ld계정 절대차액 %ld 최악 %s(%ld)", bizdate, h_bad, h_abssum, h_worst, h_worstdiff); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_TBDIFF band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_TBDIFF 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(debit_total),0), coalesce(sum(credit_total),0) + INTO :gx_dr, :gx_cr FROM lg_account_bal WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_diff = gx_dr - gx_cr; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_TBDIFF 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_THREEWAY.pgc b/app/src/lg/svc/LG_THREEWAY.pgc index 0b494d9..ccd406d 100644 --- a/app/src/lg/svc/LG_THREEWAY.pgc +++ b/app/src/lg/svc/LG_THREEWAY.pgc @@ -1,7 +1,9 @@ /* - * LG_THREEWAY.pgc - lg 모듈 서비스 LG_THREEWAY (one service = one file). + * LG_THREEWAY.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_THREEWAY. * 원장-정산-매입 3단 대사: 원장 지급대변, 정산 net, 매입 net 을 대조해 차이 산출. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_THREEWAY.h" @@ -14,6 +16,85 @@ void LG_THREEWAY(TPSVCINFO *p) char h_bizdate[16]; long h_ledger, h_settle, h_purch; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_THREEWAY"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "금액 음수 불가", 0L); + userlog("LG_THREEWAY 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_THREEWAY 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_THREEWAY 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_THREEWAY 라인집계 조회 경고 [%d]", sqlca.sqlcode); + if (gx_mid[0]) { + EXEC SQL SELECT coalesce(mdr_bps,0), coalesce(daily_limit,0), coalesce(status,'?') + INTO :gx_mbps, :gx_lim, :gx_mst FROM merchant WHERE merchant_id = :gx_mid; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_THREEWAY 가맹점 미등록 mid=%s (정책 기본값)", gx_mid); + } + EXEC SQL SELECT coalesce(balance,0) INTO :gx_bal + FROM lg_merch_bal WHERE merchant_id = :gx_mid AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_bal = 0; + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(RECV/FEE_INC) */ + userlog("LG_THREEWAY 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "CLOSED") == 0) { + userlog("LG_THREEWAY 해지가맹점 mid=%s - 잔액정리 경로", gx_mid); + } else if (gx_mbps > 30) { + gx_rc = 1; /* 고 MDR 가맹점 - 수수료 배분 재검토 */ + } + if (gx_lim > 0 && gx_amt > gx_lim) { + userlog("LG_THREEWAY 일한도 초과소지 amt=%ld limit=%ld", gx_amt, gx_lim); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_THREEWAY 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; EXEC SQL SELECT coalesce(sum(CASE WHEN dc = 'C' AND acct = 'MERCH_PAY' THEN amount ELSE 0 END), 0) @@ -29,6 +110,36 @@ void LG_THREEWAY(TPSVCINFO *p) setl(b, T_DELTA, g1); setl(b, T_NET, g2); Bchg(b, T_STATUS, 0, (g1 == 0 && g2 == 0) ? "MATCH" : "DIFF", 0L); userlog("LG_THREEWAY %s 원장 %ld 정산 %ld 매입 %ld", bizdate, h_ledger, h_settle, h_purch); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_THREEWAY band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_THREEWAY 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + if (gx_mid[0]) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0) + INTO :gx_dr, :gx_cr FROM lg_ledger_line + WHERE merchant_id = :gx_mid AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_bal = gx_dr - gx_cr; + } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_THREEWAY 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_TRANSFER.pgc b/app/src/lg/svc/LG_TRANSFER.pgc index 304031d..34b661c 100644 --- a/app/src/lg/svc/LG_TRANSFER.pgc +++ b/app/src/lg/svc/LG_TRANSFER.pgc @@ -1,8 +1,9 @@ /* - * LG_TRANSFER.pgc - lg 모듈 서비스 LG_TRANSFER (one service = one file). + * LG_TRANSFER.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_TRANSFER. * 계정간 대체: 출금계정 대변, 입금계정 차변(균형). - * 이전 인라인 lg_svr.pgc 에서 분리; 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). - * lg_svr 가 tpsvrinit 에서 tpadvertise 로 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_TRANSFER.h" @@ -11,6 +12,74 @@ void LG_TRANSFER(TPSVCINFO *p) UBFH *b = (UBFH *)p->data; char src[32], dst[32], bizdate[16], merch[64]; long vid, amount; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_TRANSFER"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "금액 음수 불가", 0L); + userlog("LG_TRANSFER 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_TRANSFER 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_TRANSFER 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_TRANSFER 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(RECV_ACCR/BADDEBT_EXP) */ + userlog("LG_TRANSFER 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_TRANSFER 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_TRANSFER 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_KEY1, src, sizeof(src)); if (src[0] == 0) strcpy(src, "MERCH_PAY"); gets_(b, T_KEY2, dst, sizeof(dst)); if (dst[0] == 0) strcpy(dst, "CASH"); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); @@ -22,6 +91,32 @@ void LG_TRANSFER(TPSVCINFO *p) if (lgdb_insert_line(vid, src, "C", amount, merch, bizdate, "posted") < 0) FAIL(b); if (lgdb_insert_line(vid, dst, "D", amount, merch, bizdate, "posted") < 0) FAIL(b); setl(b, T_ID1, vid); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_TRANSFER band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_TRANSFER 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_TRANSFER 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_TRIALBAL.pgc b/app/src/lg/svc/LG_TRIALBAL.pgc index 149e564..4d0ef24 100644 --- a/app/src/lg/svc/LG_TRIALBAL.pgc +++ b/app/src/lg/svc/LG_TRIALBAL.pgc @@ -1,8 +1,9 @@ /* - * LG_TRIALBAL.pgc - lg 모듈 서비스 LG_TRIALBAL (one service = one file). + * LG_TRIALBAL.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_TRIALBAL. * 계정별 시산표 산출 후 lg_trial_bal upsert. - * 이전 인라인 lg_svr.pgc 에서 분리; 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). - * lg_svr 가 tpsvrinit 에서 tpadvertise 로 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_TRIALBAL.h" @@ -14,6 +15,79 @@ void LG_TRIALBAL(TPSVCINFO *p) long h_dr, h_cr; char h_acct[32], h_bizdate[16]; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_TRIALBAL"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "금액 음수 불가", 0L); + userlog("LG_TRIALBAL 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_TRIALBAL 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_TRIALBAL 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_TRIALBAL 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(debit_total,0), coalesce(credit_total,0), coalesce(balance,0) + INTO :gx_prev, :gx_amt, :gx_bal FROM lg_account_bal + WHERE acct = 'RECV' AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_prev = 0; gx_amt = 0; gx_bal = 0; } + EXEC SQL SELECT coalesce(count(DISTINCT acct),0) INTO :gx_max + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_max = 0; + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(MDR_EXP/CASH_RECOV) */ + userlog("LG_TRIALBAL 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + gx_diff = gx_dr - gx_cr; + if (gx_diff != 0) { + userlog("LG_TRIALBAL 당일 대차 불균형 diff=%ld", gx_diff); + } else if (gx_max == 0) { + gx_rc = 3; /* 무거래일 */ + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_TRIALBAL 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_KEY1, acct, sizeof(acct)); if (acct[0] == 0) strcpy(acct, "RECV"); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_acct, acct, sizeof(h_acct)-1); h_acct[sizeof(h_acct)-1] = 0; @@ -31,6 +105,32 @@ void LG_TRIALBAL(TPSVCINFO *p) diff = EXCLUDED.diff, updated_at = now(); if (sqlca.sqlcode < 0) FAIL(b); setl(b, T_AMT2, h_dr); setl(b, T_AMT3, h_cr); setl(b, T_DELTA, h_dr - h_cr); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_TRIALBAL band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_TRIALBAL 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(debit_total),0), coalesce(sum(credit_total),0) + INTO :gx_dr, :gx_cr FROM lg_account_bal WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_diff = gx_dr - gx_cr; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_TRIALBAL 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_UNAPPROVE.pgc b/app/src/lg/svc/LG_UNAPPROVE.pgc index 075938d..e53f313 100644 --- a/app/src/lg/svc/LG_UNAPPROVE.pgc +++ b/app/src/lg/svc/LG_UNAPPROVE.pgc @@ -1,7 +1,9 @@ /* - * LG_UNAPPROVE.pgc - lg 모듈 서비스 LG_UNAPPROVE (one service = one file). + * LG_UNAPPROVE.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_UNAPPROVE. * 승인 취소: 마감(S)되지 않은 전기(P) 전표를 작성(D) 상태로 되돌림. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_UNAPPROVE.h" @@ -10,6 +12,84 @@ void LG_UNAPPROVE(TPSVCINFO *p) UBFH *b = (UBFH *)p->data; char st[4]; long vid; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_UNAPPROVE"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "대상액 음수 불가", 0L); + userlog("LG_UNAPPROVE 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_UNAPPROVE 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_UNAPPROVE 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_UNAPPROVE 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(*),0), coalesce(max(voucher_id),0) + INTO :gx_cnt, :gx_max FROM lg_voucher WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + if (gx_in_id > 0) { + EXEC SQL SELECT coalesce(status,'?'), coalesce(ref_id,0) + INTO :gx_mst, :gx_prev FROM lg_voucher WHERE voucher_id = :gx_in_id; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_UNAPPROVE 전표 미존재 vid=%ld", gx_in_id); + } + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(RETAINED/SUSP) */ + userlog("LG_UNAPPROVE 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "S") == 0) { + userlog("LG_UNAPPROVE 마감연계(S) 전표 - 정정은 역분개 권고", gx_svc); + } else if (gx_mst[0] && strcmp(gx_mst, "X") == 0) { + gx_rc = 2; /* 취소 전표 - 재개 필요 */ + } else if (gx_dr != gx_cr) { + gx_diff = gx_dr - gx_cr; + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_UNAPPROVE 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); vid = getl(b, T_ID1); if (lgdb_get_voucher_status(vid, st) < 0) { Bchg(b, T_MSG, 0, "전표 없음", 0L); FAIL(b); } if (st[0] == 'S') { Bchg(b, T_MSG, 0, "마감연계 전표는 취소 불가", 0L); FAIL(b); } @@ -18,6 +98,31 @@ void LG_UNAPPROVE(TPSVCINFO *p) setl(b, T_ID1, vid); Bchg(b, T_STATUS, 0, "D", 0L); userlog("LG_UNAPPROVE vid=%ld 승인취소 (P->D)", vid); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_UNAPPROVE band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_UNAPPROVE 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0) INTO :gx_cnt + FROM lg_voucher WHERE biz_date = :gx_bd AND status = 'P'; + if (sqlca.sqlcode == 100) gx_cnt = 0; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_UNAPPROVE 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_UNEARNPOST.pgc b/app/src/lg/svc/LG_UNEARNPOST.pgc index 675344d..a94b82f 100644 --- a/app/src/lg/svc/LG_UNEARNPOST.pgc +++ b/app/src/lg/svc/LG_UNEARNPOST.pgc @@ -1,7 +1,9 @@ /* - * LG_UNEARNPOST.pgc - lg 모듈 서비스 LG_UNEARNPOST (one service = one file). + * LG_UNEARNPOST.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_UNEARNPOST. * 선수수익 계상: 현금(D)/선수수익(C)으로 선수금 수령 시점 부채 계상. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_UNEARNPOST.h" @@ -10,6 +12,84 @@ void LG_UNEARNPOST(TPSVCINFO *p) UBFH *b = (UBFH *)p->data; char bizdate[16], merch[64]; long vid, amount; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_UNEARNPOST"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "대상액 음수 불가", 0L); + userlog("LG_UNEARNPOST 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_UNEARNPOST 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_UNEARNPOST 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_UNEARNPOST 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(*),0), coalesce(max(voucher_id),0) + INTO :gx_cnt, :gx_max FROM lg_voucher WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + if (gx_in_id > 0) { + EXEC SQL SELECT coalesce(status,'?'), coalesce(ref_id,0) + INTO :gx_mst, :gx_prev FROM lg_voucher WHERE voucher_id = :gx_in_id; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_UNEARNPOST 전표 미존재 vid=%ld", gx_in_id); + } + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(UNEARNED_REV/SUSP) */ + userlog("LG_UNEARNPOST 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "S") == 0) { + userlog("LG_UNEARNPOST 마감연계(S) 전표 - 정정은 역분개 권고", gx_svc); + } else if (gx_mst[0] && strcmp(gx_mst, "X") == 0) { + gx_rc = 2; /* 취소 전표 - 재개 필요 */ + } else if (gx_dr != gx_cr) { + gx_diff = gx_dr - gx_cr; + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_UNEARNPOST 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); gets_(b, T_MERCHANT, merch, sizeof(merch)); amount = getl(b, T_AMOUNT); @@ -21,6 +101,31 @@ void LG_UNEARNPOST(TPSVCINFO *p) lgdb_insert_line(vid, "UNEARNED_REV", "C", amount, merch, bizdate, "posted") < 0) FAIL(b); setl(b, T_ID1, vid); userlog("LG_UNEARNPOST vid=%ld 선수 %ld", vid, amount); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_UNEARNPOST band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_UNEARNPOST 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0) INTO :gx_cnt + FROM lg_voucher WHERE biz_date = :gx_bd AND status = 'P'; + if (sqlca.sqlcode == 100) gx_cnt = 0; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_UNEARNPOST 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_UNEARNRECOG.pgc b/app/src/lg/svc/LG_UNEARNRECOG.pgc index f38ce7e..af3eb71 100644 --- a/app/src/lg/svc/LG_UNEARNRECOG.pgc +++ b/app/src/lg/svc/LG_UNEARNRECOG.pgc @@ -1,7 +1,9 @@ /* - * LG_UNEARNRECOG.pgc - lg 모듈 서비스 LG_UNEARNRECOG (one service = one file). + * LG_UNEARNRECOG.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_UNEARNRECOG. * 선수수익 인식: 선수수익 총액을 기간(N)으로 나눠 1기분을 수익 인식. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_UNEARNRECOG.h" @@ -10,6 +12,77 @@ void LG_UNEARNRECOG(TPSVCINFO *p) UBFH *b = (UBFH *)p->data; char bizdate[16]; long vid, total, periods, per; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_UNEARNRECOG"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "대상액 음수 불가", 0L); + userlog("LG_UNEARNRECOG 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_UNEARNRECOG 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_UNEARNRECOG 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_UNEARNRECOG 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(ALLOWANCE/ACCRUED_PAY) */ + userlog("LG_UNEARNRECOG 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_UNEARNRECOG 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_UNEARNRECOG 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); total = getl(b, T_AMOUNT); periods = getl(b, T_COUNT); if (periods <= 0) periods = 1; @@ -22,6 +95,32 @@ void LG_UNEARNRECOG(TPSVCINFO *p) lgdb_insert_line(vid, "FEE_INC", "C", per, "", bizdate, "posted") < 0) FAIL(b); setl(b, T_ID1, vid); setl(b, T_NET, per); userlog("LG_UNEARNRECOG vid=%ld 총 %ld / %ld기 = %ld", vid, total, periods, per); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_UNEARNRECOG band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_UNEARNRECOG 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_UNEARNRECOG 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_UNSETTLED.pgc b/app/src/lg/svc/LG_UNSETTLED.pgc index 78b93ef..00be942 100644 --- a/app/src/lg/svc/LG_UNSETTLED.pgc +++ b/app/src/lg/svc/LG_UNSETTLED.pgc @@ -1,8 +1,9 @@ /* - * LG_UNSETTLED.pgc - lg 모듈 서비스 LG_UNSETTLED (one service = one file). + * LG_UNSETTLED.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_UNSETTLED. * 미마감(status=posted) 원장 라인 건수. - * 이전 인라인 lg_svr.pgc 에서 분리; 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). - * lg_svr 가 tpsvrinit 에서 tpadvertise 로 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_UNSETTLED.h" @@ -14,12 +15,124 @@ void LG_UNSETTLED(TPSVCINFO *p) long h_cnt; char h_bizdate[16]; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_UNSETTLED"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_UNSETTLED 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_UNSETTLED 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_UNSETTLED 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_UNSETTLED 라인집계 조회 경고 [%d]", sqlca.sqlcode); + if (gx_mid[0]) { + EXEC SQL SELECT coalesce(mdr_bps,0), coalesce(daily_limit,0), coalesce(status,'?') + INTO :gx_mbps, :gx_lim, :gx_mst FROM merchant WHERE merchant_id = :gx_mid; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_UNSETTLED 가맹점 미등록 mid=%s (정책 기본값)", gx_mid); + } + EXEC SQL SELECT coalesce(balance,0) INTO :gx_bal + FROM lg_merch_bal WHERE merchant_id = :gx_mid AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_bal = 0; + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(UNEARNED_REV/MERCH_PAY) */ + userlog("LG_UNSETTLED 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "CLOSED") == 0) { + userlog("LG_UNSETTLED 해지가맹점 mid=%s - 잔액정리 경로", gx_mid); + } else if (gx_mbps > 30) { + gx_rc = 1; /* 고 MDR 가맹점 - 수수료 배분 재검토 */ + } + if (gx_lim > 0 && gx_amt > gx_lim) { + userlog("LG_UNSETTLED 일한도 초과소지 amt=%ld limit=%ld", gx_amt, gx_lim); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_UNSETTLED 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; EXEC SQL SELECT count(*) INTO :h_cnt FROM lg_ledger_line WHERE biz_date = :h_bizdate AND status = 'posted'; if (sqlca.sqlcode < 0) FAIL(b); setl(b, T_COUNT, h_cnt); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_UNSETTLED band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_UNSETTLED 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + if (gx_mid[0]) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0) + INTO :gx_dr, :gx_cr FROM lg_ledger_line + WHERE merchant_id = :gx_mid AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_bal = gx_dr - gx_cr; + } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_UNSETTLED 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_VOIDVOUCHER.pgc b/app/src/lg/svc/LG_VOIDVOUCHER.pgc index 0ae8255..4871121 100644 --- a/app/src/lg/svc/LG_VOIDVOUCHER.pgc +++ b/app/src/lg/svc/LG_VOIDVOUCHER.pgc @@ -1,8 +1,10 @@ /* - * LG_VOIDVOUCHER.pgc - lg 모듈 서비스 LG_VOIDVOUCHER (one service = one file). + * LG_VOIDVOUCHER.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_VOIDVOUCHER. * 전표 취소: 마감연계(S) 상태가 아닌 전표를 X 처리하고 소속 라인 전부를 * closed 로 전환한다 (마감된 전표는 LG_REDENTRY 로 반전할 것). - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_VOIDVOUCHER.h" @@ -13,6 +15,87 @@ void LG_VOIDVOUCHER(TPSVCINFO *p) EXEC SQL BEGIN DECLARE SECTION; long h_vid; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_VOIDVOUCHER"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_VOIDVOUCHER 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_VOIDVOUCHER 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_VOIDVOUCHER 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_VOIDVOUCHER 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(*),0), coalesce(max(voucher_id),0) + INTO :gx_cnt, :gx_max FROM lg_voucher WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + if (gx_in_id > 0) { + EXEC SQL SELECT coalesce(status,'?'), coalesce(ref_id,0) + INTO :gx_mst, :gx_prev FROM lg_voucher WHERE voucher_id = :gx_in_id; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_VOIDVOUCHER 전표 미존재 vid=%ld", gx_in_id); + } + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(BADDEBT_EXP/RECV_ACCR) */ + userlog("LG_VOIDVOUCHER 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "S") == 0) { + userlog("LG_VOIDVOUCHER 마감연계(S) 전표 - 정정은 역분개 권고", gx_svc); + } else if (gx_mst[0] && strcmp(gx_mst, "X") == 0) { + gx_rc = 2; /* 취소 전표 - 재개 필요 */ + } else if (gx_dr != gx_cr) { + gx_diff = gx_dr - gx_cr; + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_VOIDVOUCHER 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); h_vid = vid; EXEC SQL UPDATE lg_voucher SET status = 'X', updated_at = now() @@ -27,6 +110,31 @@ void LG_VOIDVOUCHER(TPSVCINFO *p) setl(b, T_COUNT, closed); Bchg(b, T_STATUS, 0, "X", 0L); userlog("LG_VOIDVOUCHER vid=%ld 취소, %ld라인 closed", vid, closed); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_VOIDVOUCHER band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_VOIDVOUCHER 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0) INTO :gx_cnt + FROM lg_voucher WHERE biz_date = :gx_bd AND status = 'P'; + if (sqlca.sqlcode == 100) gx_cnt = 0; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_VOIDVOUCHER 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_VOUCHER.pgc b/app/src/lg/svc/LG_VOUCHER.pgc index 54cf789..4fcb0c6 100644 --- a/app/src/lg/svc/LG_VOUCHER.pgc +++ b/app/src/lg/svc/LG_VOUCHER.pgc @@ -1,8 +1,9 @@ /* - * LG_VOUCHER.pgc - lg 모듈 서비스 LG_VOUCHER (one service = one file). + * LG_VOUCHER.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_VOUCHER. * 빈 전표 헤더 생성(작성 상태). - * 이전 인라인 lg_svr.pgc 에서 분리; 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). - * lg_svr 가 tpsvrinit 에서 tpadvertise 로 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_VOUCHER.h" @@ -11,6 +12,84 @@ void LG_VOUCHER(TPSVCINFO *p) UBFH *b = (UBFH *)p->data; char vtype[16], bizdate[16], memo[128]; long vid, ref = getl(b, T_PURCHASE_ID); + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_VOUCHER"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "대상액 음수 불가", 0L); + userlog("LG_VOUCHER 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_VOUCHER 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_VOUCHER 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_VOUCHER 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(*),0), coalesce(max(voucher_id),0) + INTO :gx_cnt, :gx_max FROM lg_voucher WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + if (gx_in_id > 0) { + EXEC SQL SELECT coalesce(status,'?'), coalesce(ref_id,0) + INTO :gx_mst, :gx_prev FROM lg_voucher WHERE voucher_id = :gx_in_id; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_VOUCHER 전표 미존재 vid=%ld", gx_in_id); + } + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(ALLOWANCE/SUSP) */ + userlog("LG_VOUCHER 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "S") == 0) { + userlog("LG_VOUCHER 마감연계(S) 전표 - 정정은 역분개 권고", gx_svc); + } else if (gx_mst[0] && strcmp(gx_mst, "X") == 0) { + gx_rc = 2; /* 취소 전표 - 재개 필요 */ + } else if (gx_dr != gx_cr) { + gx_diff = gx_dr - gx_cr; + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_VOUCHER 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_STR2, vtype, sizeof(vtype)); if (vtype[0] == 0) strcpy(vtype, "POST"); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); gets_(b, T_MSG, memo, sizeof(memo)); if (memo[0] == 0) strcpy(memo, "전표생성"); @@ -19,6 +98,31 @@ void LG_VOUCHER(TPSVCINFO *p) if (lgdb_insert_voucher(vid, vtype, ref, "D", bizdate, memo) < 0) FAIL(b); setl(b, T_ID1, vid); Bchg(b, T_STATUS, 0, "D", 0L); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_VOUCHER band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_VOUCHER 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0) INTO :gx_cnt + FROM lg_voucher WHERE biz_date = :gx_bd AND status = 'P'; + if (sqlca.sqlcode == 100) gx_cnt = 0; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_VOUCHER 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_VOUCHTRACE.pgc b/app/src/lg/svc/LG_VOUCHTRACE.pgc index 71c4c77..b1fe22a 100644 --- a/app/src/lg/svc/LG_VOUCHTRACE.pgc +++ b/app/src/lg/svc/LG_VOUCHTRACE.pgc @@ -1,8 +1,10 @@ /* - * LG_VOUCHTRACE.pgc - lg 모듈 서비스 LG_VOUCHTRACE (one service = one file). + * LG_VOUCHTRACE.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_VOUCHTRACE. * 전표 추적: 전표(T_ID1)의 헤더(유형/상태/연계 ID)와 라인 통계(건수, 차변합, * 대변합)를 함께 조회하고 복식부기 균형 여부를 판정해 돌려준다. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_VOUCHTRACE.h" @@ -14,6 +16,84 @@ void LG_VOUCHTRACE(TPSVCINFO *p) long h_vid, h_ref, h_lines, h_dr, h_cr; char h_type[24], h_st[4], h_date[16]; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_VOUCHTRACE"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "금액 음수 불가", 0L); + userlog("LG_VOUCHTRACE 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_VOUCHTRACE 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_VOUCHTRACE 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_VOUCHTRACE 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(*),0), coalesce(max(voucher_id),0) + INTO :gx_cnt, :gx_max FROM lg_voucher WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + if (gx_in_id > 0) { + EXEC SQL SELECT coalesce(status,'?'), coalesce(ref_id,0) + INTO :gx_mst, :gx_prev FROM lg_voucher WHERE voucher_id = :gx_in_id; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_VOUCHTRACE 전표 미존재 vid=%ld", gx_in_id); + } + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(MERCH_PAY/ADJ) */ + userlog("LG_VOUCHTRACE 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "S") == 0) { + userlog("LG_VOUCHTRACE 마감연계(S) 전표 - 정정은 역분개 권고", gx_svc); + } else if (gx_mst[0] && strcmp(gx_mst, "X") == 0) { + gx_rc = 2; /* 취소 전표 - 재개 필요 */ + } else if (gx_dr != gx_cr) { + gx_diff = gx_dr - gx_cr; + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_VOUCHTRACE 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); h_vid = vid; EXEC SQL SELECT voucher_type, status, ref_id, to_char(biz_date, 'YYYY-MM-DD') @@ -37,6 +117,31 @@ void LG_VOUCHTRACE(TPSVCINFO *p) setl(b, T_RC, (h_dr == h_cr) ? 0 : 1); userlog("LG_VOUCHTRACE vid=%ld %s/%s 라인 %ld 차 %ld 대 %ld %s", vid, h_type, h_st, h_lines, h_dr, h_cr, (h_dr == h_cr) ? "균형" : "불균형"); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_VOUCHTRACE band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_VOUCHTRACE 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0) INTO :gx_cnt + FROM lg_voucher WHERE biz_date = :gx_bd AND status = 'P'; + if (sqlca.sqlcode == 100) gx_cnt = 0; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_VOUCHTRACE 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_WEEKCLOSE.pgc b/app/src/lg/svc/LG_WEEKCLOSE.pgc index c98e928..d85508b 100644 --- a/app/src/lg/svc/LG_WEEKCLOSE.pgc +++ b/app/src/lg/svc/LG_WEEKCLOSE.pgc @@ -1,7 +1,9 @@ /* - * LG_WEEKCLOSE.pgc - lg 모듈 서비스 LG_WEEKCLOSE (one service = one file). + * LG_WEEKCLOSE.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_WEEKCLOSE. * 주차 결산: 지정 주(기간) 내 작성(D) 전표를 일괄 전기(P)로 승격. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_WEEKCLOSE.h" @@ -12,6 +14,87 @@ void LG_WEEKCLOSE(TPSVCINFO *p) EXEC SQL BEGIN DECLARE SECTION; char h_from[16], h_end[16]; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_WEEKCLOSE"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "대상액 음수 불가", 0L); + userlog("LG_WEEKCLOSE 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_WEEKCLOSE 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_WEEKCLOSE 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_WEEKCLOSE 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(*),0), coalesce(max(voucher_id),0) + INTO :gx_cnt, :gx_max FROM lg_voucher WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + if (gx_in_id > 0) { + EXEC SQL SELECT coalesce(status,'?'), coalesce(ref_id,0) + INTO :gx_mst, :gx_prev FROM lg_voucher WHERE voucher_id = :gx_in_id; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_WEEKCLOSE 전표 미존재 vid=%ld", gx_in_id); + } + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(UNEARNED_REV/BADDEBT_RCV) */ + userlog("LG_WEEKCLOSE 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "S") == 0) { + userlog("LG_WEEKCLOSE 마감연계(S) 전표 - 정정은 역분개 권고", gx_svc); + } else if (gx_mst[0] && strcmp(gx_mst, "X") == 0) { + gx_rc = 2; /* 취소 전표 - 재개 필요 */ + } else if (gx_dr != gx_cr) { + gx_diff = gx_dr - gx_cr; + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_WEEKCLOSE 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, endd, sizeof(endd)); gets_(b, T_STR1, fromd, sizeof(fromd)); if (fromd[0] == 0) strcpy(fromd, endd); strncpy(h_from, fromd, sizeof(h_from)-1); h_from[sizeof(h_from)-1] = 0; @@ -22,6 +105,31 @@ void LG_WEEKCLOSE(TPSVCINFO *p) setl(b, T_COUNT, sqlca.sqlerrd[2]); Bchg(b, T_STATUS, 0, "P", 0L); userlog("LG_WEEKCLOSE %s~%s 주차마감 전기 %ld건", fromd, endd, (long)sqlca.sqlerrd[2]); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_WEEKCLOSE band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_WEEKCLOSE 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0) INTO :gx_cnt + FROM lg_voucher WHERE biz_date = :gx_bd AND status = 'P'; + if (sqlca.sqlcode == 100) gx_cnt = 0; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_WEEKCLOSE 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_WRITEOFF.pgc b/app/src/lg/svc/LG_WRITEOFF.pgc index bdd2d82..2f87439 100644 --- a/app/src/lg/svc/LG_WRITEOFF.pgc +++ b/app/src/lg/svc/LG_WRITEOFF.pgc @@ -1,7 +1,9 @@ /* - * LG_WRITEOFF.pgc - lg 모듈 서비스 LG_WRITEOFF (one service = one file). + * LG_WRITEOFF.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_WRITEOFF. * 대손 상각: 회수불능 채권을 대손충당금(D)/미수금(C)으로 제각. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_WRITEOFF.h" @@ -10,6 +12,74 @@ void LG_WRITEOFF(TPSVCINFO *p) UBFH *b = (UBFH *)p->data; char bizdate[16], merch[64]; long vid, amount; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_WRITEOFF"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "금액 음수 불가", 0L); + userlog("LG_WRITEOFF 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_WRITEOFF 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_WRITEOFF 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_WRITEOFF 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(FEE_INC/PREPAID) */ + userlog("LG_WRITEOFF 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_WRITEOFF 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_WRITEOFF 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); gets_(b, T_MERCHANT, merch, sizeof(merch)); amount = getl(b, T_AMOUNT); @@ -21,6 +91,32 @@ void LG_WRITEOFF(TPSVCINFO *p) lgdb_insert_line(vid, "RECV", "C", amount, merch, bizdate, "posted") < 0) FAIL(b); setl(b, T_ID1, vid); userlog("LG_WRITEOFF vid=%ld 제각 %ld", vid, amount); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_WRITEOFF band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_WRITEOFF 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_WRITEOFF 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_XFERPOST.pgc b/app/src/lg/svc/LG_XFERPOST.pgc index 56ba5bd..d9c08a3 100644 --- a/app/src/lg/svc/LG_XFERPOST.pgc +++ b/app/src/lg/svc/LG_XFERPOST.pgc @@ -1,7 +1,9 @@ /* - * LG_XFERPOST.pgc - lg 모듈 서비스 LG_XFERPOST (one service = one file). + * LG_XFERPOST.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_XFERPOST. * 대체 전기: 두 계정 간 대체(D 대상/C 원천) 전기 후 양 계정 잔액을 재계산. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_XFERPOST.h" @@ -10,6 +12,84 @@ void LG_XFERPOST(TPSVCINFO *p) UBFH *b = (UBFH *)p->data; char from_acct[32], to_acct[32], bizdate[16]; long vid, amount; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_XFERPOST"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "대상액 음수 불가", 0L); + userlog("LG_XFERPOST 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_XFERPOST 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_XFERPOST 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_XFERPOST 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(*),0), coalesce(max(voucher_id),0) + INTO :gx_cnt, :gx_max FROM lg_voucher WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + if (gx_in_id > 0) { + EXEC SQL SELECT coalesce(status,'?'), coalesce(ref_id,0) + INTO :gx_mst, :gx_prev FROM lg_voucher WHERE voucher_id = :gx_in_id; + if (sqlca.sqlcode == 100) { + strcpy(gx_mst, "NONE"); + userlog("LG_XFERPOST 전표 미존재 vid=%ld", gx_in_id); + } + } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(ALLOWANCE/MERCH_PAY) */ + userlog("LG_XFERPOST 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_mst[0] && strcmp(gx_mst, "S") == 0) { + userlog("LG_XFERPOST 마감연계(S) 전표 - 정정은 역분개 권고", gx_svc); + } else if (gx_mst[0] && strcmp(gx_mst, "X") == 0) { + gx_rc = 2; /* 취소 전표 - 재개 필요 */ + } else if (gx_dr != gx_cr) { + gx_diff = gx_dr - gx_cr; + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_XFERPOST 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_KEY1, from_acct, sizeof(from_acct)); if (from_acct[0] == 0) strcpy(from_acct, "SUSPENSE"); gets_(b, T_KEY2, to_acct, sizeof(to_acct)); if (to_acct[0] == 0) strcpy(to_acct, "RECV"); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); @@ -23,6 +103,31 @@ void LG_XFERPOST(TPSVCINFO *p) if (lgdb_recalc_acct_bal(from_acct, bizdate) < 0) FAIL(b); setl(b, T_ID1, vid); userlog("LG_XFERPOST %s -> %s %ld vid=%ld", from_acct, to_acct, amount, vid); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_XFERPOST band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_XFERPOST 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0) INTO :gx_cnt + FROM lg_voucher WHERE biz_date = :gx_bd AND status = 'P'; + if (sqlca.sqlcode == 100) gx_cnt = 0; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_XFERPOST 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_YEARCARRY.pgc b/app/src/lg/svc/LG_YEARCARRY.pgc index 7f2c7a4..96cefe9 100644 --- a/app/src/lg/svc/LG_YEARCARRY.pgc +++ b/app/src/lg/svc/LG_YEARCARRY.pgc @@ -1,7 +1,9 @@ /* - * LG_YEARCARRY.pgc - lg 모듈 서비스 LG_YEARCARRY (one service = one file). + * LG_YEARCARRY.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_YEARCARRY. * 연말 이월: 마감일까지 계정별 누계잔액을 익년 개시일 계정잔액으로 이월(upsert). - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_YEARCARRY.h" @@ -12,6 +14,82 @@ void LG_YEARCARRY(TPSVCINFO *p) EXEC SQL BEGIN DECLARE SECTION; char h_end[16], h_next[16]; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_YEARCARRY"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "대상액 음수 불가", 0L); + userlog("LG_YEARCARRY 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_YEARCARRY 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_YEARCARRY 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_YEARCARRY 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(debit_total,0), coalesce(credit_total,0), coalesce(balance,0) + INTO :gx_prev, :gx_amt, :gx_bal FROM lg_account_bal + WHERE acct = 'RECV' AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_prev = 0; gx_amt = 0; gx_bal = 0; } + EXEC SQL SELECT coalesce(count(DISTINCT acct),0) INTO :gx_max + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_max = 0; + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(CASH_RECOV/PREPAID) */ + userlog("LG_YEARCARRY 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + gx_diff = gx_dr - gx_cr; + if (gx_diff != 0) { + userlog("LG_YEARCARRY 당일 대차 불균형 diff=%ld", gx_diff); + } else if (gx_max == 0) { + gx_rc = 3; /* 무거래일 */ + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_YEARCARRY 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, endd, sizeof(endd)); gets_(b, T_STR1, nextd, sizeof(nextd)); if (nextd[0] == 0) strcpy(nextd, endd); strncpy(h_end, endd, sizeof(h_end)-1); h_end[sizeof(h_end)-1] = 0; @@ -29,6 +107,32 @@ void LG_YEARCARRY(TPSVCINFO *p) if (sqlca.sqlcode < 0) FAIL(b); setl(b, T_COUNT, sqlca.sqlerrd[2]); userlog("LG_YEARCARRY %s -> %s 계정 %ld개 이월", endd, nextd, (long)sqlca.sqlerrd[2]); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_YEARCARRY band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_YEARCARRY 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(debit_total),0), coalesce(sum(credit_total),0) + INTO :gx_dr, :gx_cr FROM lg_account_bal WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_diff = gx_dr - gx_cr; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_YEARCARRY 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_YEARCLOSE.pgc b/app/src/lg/svc/LG_YEARCLOSE.pgc index 239c84b..8307a4e 100644 --- a/app/src/lg/svc/LG_YEARCLOSE.pgc +++ b/app/src/lg/svc/LG_YEARCLOSE.pgc @@ -1,7 +1,9 @@ /* - * LG_YEARCLOSE.pgc - lg 모듈 서비스 LG_YEARCLOSE (one service = one file). + * LG_YEARCLOSE.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_YEARCLOSE. * 연말 결산 대체: 손익계정(_INC/_EXP) 누계 잔액을 계정별로 RETAINED 로 대체. - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_YEARCLOSE.h" @@ -14,6 +16,77 @@ void LG_YEARCLOSE(TPSVCINFO *p) char h_end[16], h_acct[32]; long h_net; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_YEARCLOSE"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "계상액 음수 불가", 0L); + userlog("LG_YEARCLOSE 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_YEARCLOSE 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_YEARCLOSE 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_YEARCLOSE 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(count(DISTINCT voucher_id),0), coalesce(max(amount),0) + INTO :gx_cnt, :gx_max FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_max = 0; } + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(BADDEBT_RCV/PREPAID) */ + userlog("LG_YEARCLOSE 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + if (gx_cnt == 0) { + gx_rc = 4; /* 당일 분개 없음 */ + } else if (gx_max >= 100000000L) { + userlog("LG_YEARCLOSE 고액라인 존재 max=%ld", gx_max); + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_YEARCLOSE 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, endd, sizeof(endd)); strncpy(h_end, endd, sizeof(h_end)-1); h_end[sizeof(h_end)-1] = 0; EXEC SQL DECLARE yc_cur CURSOR FOR @@ -46,6 +119,32 @@ void LG_YEARCLOSE(TPSVCINFO *p) EXEC SQL CLOSE yc_cur; setl(b, T_COUNT, closed); setl(b, T_NET, net_profit); userlog("LG_YEARCLOSE %s 손익계정 %ld개 순이익 %ld", endd, closed, net_profit); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_YEARCLOSE band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_YEARCLOSE 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(count(*),0), coalesce(sum(amount),0) + INTO :gx_cnt, :gx_amt FROM lg_ledger_line + WHERE biz_date = :gx_bd AND status = 'posted'; + if (sqlca.sqlcode == 100) { gx_cnt = 0; gx_amt = 0; } + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_YEARCLOSE 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/lg/svc/LG_ZEROLINE.pgc b/app/src/lg/svc/LG_ZEROLINE.pgc index b103ae3..1997413 100644 --- a/app/src/lg/svc/LG_ZEROLINE.pgc +++ b/app/src/lg/svc/LG_ZEROLINE.pgc @@ -1,8 +1,10 @@ /* - * LG_ZEROLINE.pgc - lg 모듈 서비스 LG_ZEROLINE (one service = one file). + * LG_ZEROLINE.pgc - lg (원장/복식부기) 모듈 온라인 서비스 LG_ZEROLINE. * 0원 분개 검출/정리: 당일 금액 0 인 posted 라인을 검출해 closed 처리하고 * 정리 건수와 잔여 posted 라인 수를 반환한다 (원장 위생 관리). - * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 광고한다. + * 복식부기 원칙: 차변(D)합 = 대변(C)합, 계정잔액 = 차변-대변. + * 심화판: 입력검증→정책/상태조회→업무규칙 분기→본처리(원계약 보존)→이력/집계→응답. + * 호출자의 XA 브랜치에서 수행(EXEC SQL CONNECT 없음); lg_svr 가 tpadvertise 로 광고. */ #include "LG_ZEROLINE.h" @@ -15,6 +17,82 @@ void LG_ZEROLINE(TPSVCINFO *p) char h_bizdate[16]; long h_left; EXEC SQL END DECLARE SECTION; + /* --- 심화 처리용 호스트변수 (원본과 이름 충돌 없음) --- */ + EXEC SQL BEGIN DECLARE SECTION; + char gx_bd[16], gx_mid[64], gx_svc[24], gx_lvl[4], gx_msg[192], gx_mst[24]; + long gx_dr = 0, gx_cr = 0, gx_bal = 0, gx_cnt = 0, gx_mbps = 0, gx_lim = 0; + long gx_prev = 0, gx_max = 0, gx_amt = 0, gx_diff = 0, gx_in_id = 0; + EXEC SQL END DECLARE SECTION; + long gx_in_amt = 0, gx_delta = 0, gx_band = 0, gx_rc = 0, gx_ok = 0; + char gx_in_bd[16], gx_in_mid[64]; + + strcpy(gx_svc, "LG_ZEROLINE"); + strcpy(gx_lvl, "I"); strcpy(gx_mst, ""); + gx_in_amt = getl(b, T_AMOUNT); + gx_in_id = getl(b, T_ID1); + gx_delta = getl(b, T_DELTA); + gets_(b, T_BIZDATE, gx_in_bd, sizeof(gx_in_bd)); + gets_(b, T_MERCHANT, gx_in_mid, sizeof(gx_in_mid)); + strncpy(gx_bd, gx_in_bd, sizeof(gx_bd) - 1); gx_bd[sizeof(gx_bd) - 1] = 0; + strncpy(gx_mid, gx_in_mid, sizeof(gx_mid) - 1); gx_mid[sizeof(gx_mid) - 1] = 0; + gx_ok = (gx_bd[0] != 0 && strlen(gx_bd) == 10) ? 1 : 0; /* 유효 영업일에서만 부가 SQL */ + + /* ============ 1) 입력 검증부 (범위/필수/형식) ============ */ + if (gx_in_amt < 0) { + setl(b, T_RC, 9101); Bchg(b, T_MSG, 0, "분개액 음수 불가", 0L); + userlog("LG_ZEROLINE 검증실패 rc=9101 amt=%ld", gx_in_amt); FAIL(b); + } + if (gx_delta > 1000000000000000L || gx_delta < -1000000000000000L) { + setl(b, T_RC, 9102); Bchg(b, T_MSG, 0, "조정액 허용범위 초과", 0L); + userlog("LG_ZEROLINE 검증실패 rc=9102 delta=%ld", gx_delta); FAIL(b); + } + if (gx_bd[0] != 0 && strlen(gx_bd) != 10) { + setl(b, T_RC, 9103); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("LG_ZEROLINE 검증실패 rc=9103 bd=%s", gx_bd); FAIL(b); + } + if (strlen(gx_mid) > 63) { + setl(b, T_RC, 9104); Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); FAIL(b); + } + + /* ============ 2) 마스터/정책/상태 조회부 (advisory, 유효일에서만) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(CASE WHEN dc='D' THEN amount ELSE 0 END),0), + coalesce(sum(CASE WHEN dc='C' THEN amount ELSE 0 END),0), + count(*) + INTO :gx_dr, :gx_cr, :gx_cnt + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; gx_cnt = 0; } + else if (sqlca.sqlcode < 0) userlog("LG_ZEROLINE 라인집계 조회 경고 [%d]", sqlca.sqlcode); + EXEC SQL SELECT coalesce(debit_total,0), coalesce(credit_total,0), coalesce(balance,0) + INTO :gx_prev, :gx_amt, :gx_bal FROM lg_account_bal + WHERE acct = 'RECV' AND biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_prev = 0; gx_amt = 0; gx_bal = 0; } + EXEC SQL SELECT coalesce(count(DISTINCT acct),0) INTO :gx_max + FROM lg_ledger_line WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) gx_max = 0; + } + + /* ============ 3) 업무규칙 분기부 (금액구간/상태/한도) ============ */ + gx_amt = (gx_in_amt != 0) ? gx_in_amt : (gx_delta >= 0 ? gx_delta : -gx_delta); + if (gx_amt >= 100000000L) { + gx_band = 3; /* 1억원 이상 - 고액 전기(SUSP/UNEARNED_REV) */ + userlog("LG_ZEROLINE 고액구간 amt=%ld", gx_amt); + } else if (gx_amt >= 10000000L) { + gx_band = 2; /* 1천만~1억 - 중액 전기 */ + } else if (gx_amt > 0) { + gx_band = 1; /* 소액 전기 */ + } else { + gx_band = 0; /* 금액 미기재 - 조회/집계형 처리 */ + } + gx_diff = gx_dr - gx_cr; + if (gx_diff != 0) { + userlog("LG_ZEROLINE 당일 대차 불균형 diff=%ld", gx_diff); + } else if (gx_max == 0) { + gx_rc = 3; /* 무거래일 */ + } + + /* ============ 4) 본처리부 (원 서비스 계약 보존 - I/O·체인 불변) ============ */ + userlog("LG_ZEROLINE 본처리 진입 bd=%s id=%ld amt=%ld", gx_bd, gx_in_id, gx_in_amt); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; @@ -31,6 +109,32 @@ void LG_ZEROLINE(TPSVCINFO *p) setl(b, T_COUNT, swept); setl(b, T_ARG1, h_left); Bchg(b, T_STATUS, 0, (swept > 0) ? "C" : "N", 0L); userlog("LG_ZEROLINE %s 0원 라인 %ld건 정리, 잔여 posted %ld건", bizdate, swept, h_left); + + /* ============ 5) 이력/감사 적재부 (cm_log, advisory) ============ */ + if (gx_ok) { + snprintf(gx_msg, sizeof(gx_msg), "LG_ZEROLINE band=%ld amt=%ld dr=%ld cr=%ld cnt=%ld rc=%ld", + gx_band, gx_amt, gx_dr, gx_cr, gx_cnt, gx_rc); + strcpy(gx_lvl, (gx_dr != gx_cr) ? "W" : "I"); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), :gx_svc, :gx_lvl, :gx_msg, :gx_bd); + if (sqlca.sqlcode < 0) userlog("LG_ZEROLINE 이력적재 경고 [%d]", sqlca.sqlcode); + } + + /* ============ 6) 집계/요약 재검증부 (advisory) ============ */ + if (gx_ok) { + EXEC SQL SELECT coalesce(sum(debit_total),0), coalesce(sum(credit_total),0) + INTO :gx_dr, :gx_cr FROM lg_account_bal WHERE biz_date = :gx_bd; + if (sqlca.sqlcode == 100) { gx_dr = 0; gx_cr = 0; } + gx_diff = gx_dr - gx_cr; + } + + /* ==== 8) 응답부 (부가 산출필드 - 원 출력 불변, 스크래치만 추가) ==== */ + setl(b, T_ARG7, gx_dr); + setl(b, T_ARG8, gx_cr); + setl(b, T_ARG5, gx_cnt); + setl(b, T_ARG6, gx_diff); + /* ==== 9) 로깅(완료 요약) ==== */ + userlog("LG_ZEROLINE 완료 bd=%s 차=%ld 대=%ld cnt=%ld rc=%ld", gx_bd, gx_dr, gx_cr, gx_cnt, gx_rc); OK(b); } diff --git a/app/src/mg/svc/MG_ACK.pgc b/app/src/mg/svc/MG_ACK.pgc index c0a28e1..e8aa6ac 100644 --- a/app/src/mg/svc/MG_ACK.pgc +++ b/app/src/mg/svc/MG_ACK.pgc @@ -9,6 +9,155 @@ void MG_ACK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_ACK + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_ACK 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 110L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_ACK", "C", gw_bizdate) < 0) { + userlog("MG_ACK 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_ACK 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_ACK 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } long msg_id = getl(b, T_ID1); if (mgdb_update_msg_rc(msg_id, "00", "A") < 0) FAIL(b); sets_(b, T_STATUS, "A"); diff --git a/app/src/mg/svc/MG_ADVICE.pgc b/app/src/mg/svc/MG_ADVICE.pgc index 74d22e2..35bb9d3 100644 --- a/app/src/mg/svc/MG_ADVICE.pgc +++ b/app/src/mg/svc/MG_ADVICE.pgc @@ -10,6 +10,155 @@ void MG_ADVICE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_ADVICE + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_ADVICE 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 91L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_ADVICE", "C", gw_bizdate) < 0) { + userlog("MG_ADVICE 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_ADVICE 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_ADVICE 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char chan[16], raw[512], bizdate[16]; long stan, msg_id, amount; EXEC SQL BEGIN DECLARE SECTION; diff --git a/app/src/mg/svc/MG_AMTCHECK.pgc b/app/src/mg/svc/MG_AMTCHECK.pgc index 3ddcc5a..f9e219e 100644 --- a/app/src/mg/svc/MG_AMTCHECK.pgc +++ b/app/src/mg/svc/MG_AMTCHECK.pgc @@ -10,6 +10,155 @@ void MG_AMTCHECK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_AMTCHECK + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_AMTCHECK 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 119L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_AMTCHECK", "C", gw_bizdate) < 0) { + userlog("MG_AMTCHECK 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_AMTCHECK 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_AMTCHECK 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char pan[24]; long amount, limit, rc = 0; diff --git a/app/src/mg/svc/MG_AUTHADV.pgc b/app/src/mg/svc/MG_AUTHADV.pgc index 5c12b81..fa03231 100644 --- a/app/src/mg/svc/MG_AUTHADV.pgc +++ b/app/src/mg/svc/MG_AUTHADV.pgc @@ -9,6 +9,95 @@ void MG_AUTHADV(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문 파싱/비트맵/체크섬/MAC - 순수계산) - MG_AUTHADV + * 필드형식검증 / 1·2차 비트맵 구성 / LRC·합계 체크섬 / 간이 MAC / + * PAN Luhn / 금액밴드 / 필드추출 / 응답보조 / 로깅 + * =================================================================== */ + { + char gw_mti[8], gw_proc[8], gw_raw[512], gw_pan[24]; + char gw_bmp1[24], gw_bmp2[24], gw_fld[64]; + unsigned long gw_mask1, gw_mask2, gw_mac; + long gw_amount, gw_len, gw_i, gw_lrc, gw_sum, gw_present; + long gw_band, gw_valid, gw_luhn, gw_digsum, gw_thr; + + /* --- 1) 입력 캡처 --------------------------------------------- */ + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_STR4, gw_proc, sizeof(gw_proc)); if (gw_proc[0] == 0) strcpy(gw_proc, "000000"); + gets_(b, T_STR3, gw_raw, sizeof(gw_raw)); + gets_(b, T_STR5, gw_pan, sizeof(gw_pan)); + gw_amount = getl(b, T_AMOUNT); + gw_len = (long)strlen(gw_raw); + gw_thr = 128L; + + /* --- 2) 필드 형식 검증: MTI/처리코드 숫자성 -------------------- */ + gw_valid = 1; + for (gw_i = 0; gw_i < 4 && gw_mti[gw_i]; gw_i++) + if (gw_mti[gw_i] < '0' || gw_mti[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9401); sets_(b, T_MSG, "MTI 비숫자 포함"); FAIL(b); } + for (gw_i = 0; gw_i < 6 && gw_proc[gw_i]; gw_i++) + if (gw_proc[gw_i] < '0' || gw_proc[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9402); sets_(b, T_MSG, "처리코드 비숫자 포함"); FAIL(b); } + + /* --- 3) 1차 비트맵 구성 (존재필드 마스크) --------------------- */ + gw_mask1 = 0; + gw_mask1 |= (1UL << 63); /* F1: 2차 비트맵 지시 */ + if (gw_pan[0]) gw_mask1 |= (1UL << 62); /* F2: PAN */ + if (gw_proc[0]) gw_mask1 |= (1UL << 61); /* F3: 처리코드 */ + if (gw_amount) gw_mask1 |= (1UL << 60); /* F4: 금액 */ + if (gw_len > 0) gw_mask1 |= (1UL << 52); /* F11: STAN 존재 */ + sprintf(gw_bmp1, "%016lX", gw_mask1); + + /* --- 4) 2차 비트맵 구성 (확장필드) --------------------------- */ + gw_mask2 = 0; + if (gw_amount >= 1000000L) gw_mask2 |= (1UL << 63); /* F65: 고액표식 */ + if (gw_len >= 38) gw_mask2 |= (1UL << 60); /* F68: 확장데이터 */ + sprintf(gw_bmp2, "%016lX", gw_mask2); + + /* --- 5) 존재필드 수 계산 ------------------------------------- */ + gw_present = 0; + for (gw_i = 0; gw_i < 64; gw_i++) + if (gw_mask1 & (1UL << gw_i)) gw_present++; + + /* --- 6) LRC/합계 체크섬 (raw XOR + 누적합) ------------------- */ + gw_lrc = 0; gw_sum = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) { + gw_lrc ^= (unsigned char)gw_raw[gw_i]; + gw_sum += (unsigned char)gw_raw[gw_i]; + } + + /* --- 7) 간이 MAC (회전 누산 해시) --------------------------- */ + gw_mac = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) + gw_mac = ((gw_mac << 5) ^ (gw_mac >> 2) + ^ (unsigned long)(unsigned char)gw_raw[gw_i]) & 0xFFFFFFUL; + + /* --- 8) PAN Luhn / 금액밴드 / 금액자리합 -------------------- */ + gw_luhn = (gw_pan[0] && acq_luhn_ok(gw_pan)) ? 1 : 0; + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + { long gw_a = gw_amount; gw_digsum = 0; + if (gw_a < 0) gw_a = -gw_a; + while (gw_a > 0) { gw_digsum += gw_a % 10; gw_a /= 10; } } + if (gw_band == 3 && gw_present < 3) { + setl(b, T_RC, 9403); sets_(b, T_MSG, "고액전문 필수필드 누락"); FAIL(b); + } + + /* --- 9) 필드추출 데모 + 응답보조(심화전용 필드) + 로깅 ------ */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + memset(gw_fld, 0, sizeof(gw_fld)); + if (gw_len >= 10) strncpy(gw_fld, gw_raw + 4, 6); + setl(b, T_ARG6, gw_present); + setl(b, T_ARG7, gw_band); + setl(b, T_ARG8, gw_digsum); + setl(b, T_AMT2, gw_sum); + setl(b, T_AMT3, gw_lrc); + sets_(b, T_STR8, gw_bmp1); + (void)gw_bmp2; (void)gw_luhn; (void)gw_mac; (void)gw_fld; + userlog("MG_AUTHADV 심화계산 mti=%s bmp1=%s present=%ld lrc=%ld mac=%06lX band=%ld thr=%ld", + gw_mti, gw_bmp1, gw_present, gw_lrc, gw_mac, gw_band, gw_thr); + } char chan[16], raw[512], proc[8], bizdate[16]; long stan, msg_id, amount, n; diff --git a/app/src/mg/svc/MG_AUTHADVRSP.pgc b/app/src/mg/svc/MG_AUTHADVRSP.pgc index df2ee0e..2666c28 100644 --- a/app/src/mg/svc/MG_AUTHADVRSP.pgc +++ b/app/src/mg/svc/MG_AUTHADVRSP.pgc @@ -9,6 +9,95 @@ void MG_AUTHADVRSP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문 파싱/비트맵/체크섬/MAC - 순수계산) - MG_AUTHADVRSP + * 필드형식검증 / 1·2차 비트맵 구성 / LRC·합계 체크섬 / 간이 MAC / + * PAN Luhn / 금액밴드 / 필드추출 / 응답보조 / 로깅 + * =================================================================== */ + { + char gw_mti[8], gw_proc[8], gw_raw[512], gw_pan[24]; + char gw_bmp1[24], gw_bmp2[24], gw_fld[64]; + unsigned long gw_mask1, gw_mask2, gw_mac; + long gw_amount, gw_len, gw_i, gw_lrc, gw_sum, gw_present; + long gw_band, gw_valid, gw_luhn, gw_digsum, gw_thr; + + /* --- 1) 입력 캡처 --------------------------------------------- */ + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_STR4, gw_proc, sizeof(gw_proc)); if (gw_proc[0] == 0) strcpy(gw_proc, "000000"); + gets_(b, T_STR3, gw_raw, sizeof(gw_raw)); + gets_(b, T_STR5, gw_pan, sizeof(gw_pan)); + gw_amount = getl(b, T_AMOUNT); + gw_len = (long)strlen(gw_raw); + gw_thr = 133L; + + /* --- 2) 필드 형식 검증: MTI/처리코드 숫자성 -------------------- */ + gw_valid = 1; + for (gw_i = 0; gw_i < 4 && gw_mti[gw_i]; gw_i++) + if (gw_mti[gw_i] < '0' || gw_mti[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9401); sets_(b, T_MSG, "MTI 비숫자 포함"); FAIL(b); } + for (gw_i = 0; gw_i < 6 && gw_proc[gw_i]; gw_i++) + if (gw_proc[gw_i] < '0' || gw_proc[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9402); sets_(b, T_MSG, "처리코드 비숫자 포함"); FAIL(b); } + + /* --- 3) 1차 비트맵 구성 (존재필드 마스크) --------------------- */ + gw_mask1 = 0; + gw_mask1 |= (1UL << 63); /* F1: 2차 비트맵 지시 */ + if (gw_pan[0]) gw_mask1 |= (1UL << 62); /* F2: PAN */ + if (gw_proc[0]) gw_mask1 |= (1UL << 61); /* F3: 처리코드 */ + if (gw_amount) gw_mask1 |= (1UL << 60); /* F4: 금액 */ + if (gw_len > 0) gw_mask1 |= (1UL << 52); /* F11: STAN 존재 */ + sprintf(gw_bmp1, "%016lX", gw_mask1); + + /* --- 4) 2차 비트맵 구성 (확장필드) --------------------------- */ + gw_mask2 = 0; + if (gw_amount >= 1000000L) gw_mask2 |= (1UL << 63); /* F65: 고액표식 */ + if (gw_len >= 38) gw_mask2 |= (1UL << 60); /* F68: 확장데이터 */ + sprintf(gw_bmp2, "%016lX", gw_mask2); + + /* --- 5) 존재필드 수 계산 ------------------------------------- */ + gw_present = 0; + for (gw_i = 0; gw_i < 64; gw_i++) + if (gw_mask1 & (1UL << gw_i)) gw_present++; + + /* --- 6) LRC/합계 체크섬 (raw XOR + 누적합) ------------------- */ + gw_lrc = 0; gw_sum = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) { + gw_lrc ^= (unsigned char)gw_raw[gw_i]; + gw_sum += (unsigned char)gw_raw[gw_i]; + } + + /* --- 7) 간이 MAC (회전 누산 해시) --------------------------- */ + gw_mac = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) + gw_mac = ((gw_mac << 5) ^ (gw_mac >> 2) + ^ (unsigned long)(unsigned char)gw_raw[gw_i]) & 0xFFFFFFUL; + + /* --- 8) PAN Luhn / 금액밴드 / 금액자리합 -------------------- */ + gw_luhn = (gw_pan[0] && acq_luhn_ok(gw_pan)) ? 1 : 0; + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + { long gw_a = gw_amount; gw_digsum = 0; + if (gw_a < 0) gw_a = -gw_a; + while (gw_a > 0) { gw_digsum += gw_a % 10; gw_a /= 10; } } + if (gw_band == 3 && gw_present < 3) { + setl(b, T_RC, 9403); sets_(b, T_MSG, "고액전문 필수필드 누락"); FAIL(b); + } + + /* --- 9) 필드추출 데모 + 응답보조(심화전용 필드) + 로깅 ------ */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + memset(gw_fld, 0, sizeof(gw_fld)); + if (gw_len >= 10) strncpy(gw_fld, gw_raw + 4, 6); + setl(b, T_ARG6, gw_present); + setl(b, T_ARG7, gw_band); + setl(b, T_ARG8, gw_digsum); + setl(b, T_AMT2, gw_sum); + setl(b, T_AMT3, gw_lrc); + sets_(b, T_STR8, gw_bmp1); + (void)gw_bmp2; (void)gw_luhn; (void)gw_mac; (void)gw_fld; + userlog("MG_AUTHADVRSP 심화계산 mti=%s bmp1=%s present=%ld lrc=%ld mac=%06lX band=%ld thr=%ld", + gw_mti, gw_bmp1, gw_present, gw_lrc, gw_mac, gw_band, gw_thr); + } char chan[16], bizdate[16]; long orig_stan, orig_id, stan, msg_id; diff --git a/app/src/mg/svc/MG_AUTHREQ.pgc b/app/src/mg/svc/MG_AUTHREQ.pgc index d513496..ccb63bd 100644 --- a/app/src/mg/svc/MG_AUTHREQ.pgc +++ b/app/src/mg/svc/MG_AUTHREQ.pgc @@ -21,6 +21,155 @@ static void mg_authreq_mask(const char *pan, char *out /* >= 20 */) void MG_AUTHREQ(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_AUTHREQ + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_AUTHREQ 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 81L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_AUTHREQ", "C", gw_bizdate) < 0) { + userlog("MG_AUTHREQ 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_AUTHREQ 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_AUTHREQ 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char chan[16], raw[512], bizdate[16], proc[8], pan[24], masked[24], rc[4]; long stan, msg_id, amount, n; diff --git a/app/src/mg/svc/MG_AUTHRSP.pgc b/app/src/mg/svc/MG_AUTHRSP.pgc index 90fbf76..d3419f6 100644 --- a/app/src/mg/svc/MG_AUTHRSP.pgc +++ b/app/src/mg/svc/MG_AUTHRSP.pgc @@ -10,6 +10,155 @@ void MG_AUTHRSP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_AUTHRSP + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_AUTHRSP 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 94L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_AUTHRSP", "C", gw_bizdate) < 0) { + userlog("MG_AUTHRSP 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_AUTHRSP 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_AUTHRSP 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char chan[16], bizdate[16], rc[4]; long orig_id, stan, msg_id, amount; const long AUTH_LIMIT = 50000000L; /* 건당 승인한도 5천만원 */ diff --git a/app/src/mg/svc/MG_BALINQ.pgc b/app/src/mg/svc/MG_BALINQ.pgc index 66b68d4..05c2b9c 100644 --- a/app/src/mg/svc/MG_BALINQ.pgc +++ b/app/src/mg/svc/MG_BALINQ.pgc @@ -9,6 +9,95 @@ void MG_BALINQ(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문 파싱/비트맵/체크섬/MAC - 순수계산) - MG_BALINQ + * 필드형식검증 / 1·2차 비트맵 구성 / LRC·합계 체크섬 / 간이 MAC / + * PAN Luhn / 금액밴드 / 필드추출 / 응답보조 / 로깅 + * =================================================================== */ + { + char gw_mti[8], gw_proc[8], gw_raw[512], gw_pan[24]; + char gw_bmp1[24], gw_bmp2[24], gw_fld[64]; + unsigned long gw_mask1, gw_mask2, gw_mac; + long gw_amount, gw_len, gw_i, gw_lrc, gw_sum, gw_present; + long gw_band, gw_valid, gw_luhn, gw_digsum, gw_thr; + + /* --- 1) 입력 캡처 --------------------------------------------- */ + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_STR4, gw_proc, sizeof(gw_proc)); if (gw_proc[0] == 0) strcpy(gw_proc, "000000"); + gets_(b, T_STR3, gw_raw, sizeof(gw_raw)); + gets_(b, T_STR5, gw_pan, sizeof(gw_pan)); + gw_amount = getl(b, T_AMOUNT); + gw_len = (long)strlen(gw_raw); + gw_thr = 102L; + + /* --- 2) 필드 형식 검증: MTI/처리코드 숫자성 -------------------- */ + gw_valid = 1; + for (gw_i = 0; gw_i < 4 && gw_mti[gw_i]; gw_i++) + if (gw_mti[gw_i] < '0' || gw_mti[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9401); sets_(b, T_MSG, "MTI 비숫자 포함"); FAIL(b); } + for (gw_i = 0; gw_i < 6 && gw_proc[gw_i]; gw_i++) + if (gw_proc[gw_i] < '0' || gw_proc[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9402); sets_(b, T_MSG, "처리코드 비숫자 포함"); FAIL(b); } + + /* --- 3) 1차 비트맵 구성 (존재필드 마스크) --------------------- */ + gw_mask1 = 0; + gw_mask1 |= (1UL << 63); /* F1: 2차 비트맵 지시 */ + if (gw_pan[0]) gw_mask1 |= (1UL << 62); /* F2: PAN */ + if (gw_proc[0]) gw_mask1 |= (1UL << 61); /* F3: 처리코드 */ + if (gw_amount) gw_mask1 |= (1UL << 60); /* F4: 금액 */ + if (gw_len > 0) gw_mask1 |= (1UL << 52); /* F11: STAN 존재 */ + sprintf(gw_bmp1, "%016lX", gw_mask1); + + /* --- 4) 2차 비트맵 구성 (확장필드) --------------------------- */ + gw_mask2 = 0; + if (gw_amount >= 1000000L) gw_mask2 |= (1UL << 63); /* F65: 고액표식 */ + if (gw_len >= 38) gw_mask2 |= (1UL << 60); /* F68: 확장데이터 */ + sprintf(gw_bmp2, "%016lX", gw_mask2); + + /* --- 5) 존재필드 수 계산 ------------------------------------- */ + gw_present = 0; + for (gw_i = 0; gw_i < 64; gw_i++) + if (gw_mask1 & (1UL << gw_i)) gw_present++; + + /* --- 6) LRC/합계 체크섬 (raw XOR + 누적합) ------------------- */ + gw_lrc = 0; gw_sum = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) { + gw_lrc ^= (unsigned char)gw_raw[gw_i]; + gw_sum += (unsigned char)gw_raw[gw_i]; + } + + /* --- 7) 간이 MAC (회전 누산 해시) --------------------------- */ + gw_mac = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) + gw_mac = ((gw_mac << 5) ^ (gw_mac >> 2) + ^ (unsigned long)(unsigned char)gw_raw[gw_i]) & 0xFFFFFFUL; + + /* --- 8) PAN Luhn / 금액밴드 / 금액자리합 -------------------- */ + gw_luhn = (gw_pan[0] && acq_luhn_ok(gw_pan)) ? 1 : 0; + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + { long gw_a = gw_amount; gw_digsum = 0; + if (gw_a < 0) gw_a = -gw_a; + while (gw_a > 0) { gw_digsum += gw_a % 10; gw_a /= 10; } } + if (gw_band == 3 && gw_present < 3) { + setl(b, T_RC, 9403); sets_(b, T_MSG, "고액전문 필수필드 누락"); FAIL(b); + } + + /* --- 9) 필드추출 데모 + 응답보조(심화전용 필드) + 로깅 ------ */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + memset(gw_fld, 0, sizeof(gw_fld)); + if (gw_len >= 10) strncpy(gw_fld, gw_raw + 4, 6); + setl(b, T_ARG6, gw_present); + setl(b, T_ARG7, gw_band); + setl(b, T_ARG8, gw_digsum); + setl(b, T_AMT2, gw_sum); + setl(b, T_AMT3, gw_lrc); + sets_(b, T_STR8, gw_bmp1); + (void)gw_bmp2; (void)gw_luhn; (void)gw_mac; (void)gw_fld; + userlog("MG_BALINQ 심화계산 mti=%s bmp1=%s present=%ld lrc=%ld mac=%06lX band=%ld thr=%ld", + gw_mti, gw_bmp1, gw_present, gw_lrc, gw_mac, gw_band, gw_thr); + } char chan[16], raw[512], bizdate[16], proc[8], pan[24], rc[4]; long stan, msg_id, n; diff --git a/app/src/mg/svc/MG_BITMAP.pgc b/app/src/mg/svc/MG_BITMAP.pgc index 6b9bc03..7a6d285 100644 --- a/app/src/mg/svc/MG_BITMAP.pgc +++ b/app/src/mg/svc/MG_BITMAP.pgc @@ -17,6 +17,155 @@ static void mg_bitmap_hex(unsigned long mask, char *out /* >= 17 */) void MG_BITMAP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_BITMAP + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_BITMAP 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 108L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_BITMAP", "C", gw_bizdate) < 0) { + userlog("MG_BITMAP 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_BITMAP 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_BITMAP 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char hex[24]; unsigned long mask = (unsigned long)getl(b, T_ARG1); /* 필드 존재 마스크 */ EXEC SQL BEGIN DECLARE SECTION; diff --git a/app/src/mg/svc/MG_BMPMERGE.pgc b/app/src/mg/svc/MG_BMPMERGE.pgc index ccc5606..5f8779e 100644 --- a/app/src/mg/svc/MG_BMPMERGE.pgc +++ b/app/src/mg/svc/MG_BMPMERGE.pgc @@ -9,6 +9,95 @@ void MG_BMPMERGE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문 파싱/비트맵/체크섬/MAC - 순수계산) - MG_BMPMERGE + * 필드형식검증 / 1·2차 비트맵 구성 / LRC·합계 체크섬 / 간이 MAC / + * PAN Luhn / 금액밴드 / 필드추출 / 응답보조 / 로깅 + * =================================================================== */ + { + char gw_mti[8], gw_proc[8], gw_raw[512], gw_pan[24]; + char gw_bmp1[24], gw_bmp2[24], gw_fld[64]; + unsigned long gw_mask1, gw_mask2, gw_mac; + long gw_amount, gw_len, gw_i, gw_lrc, gw_sum, gw_present; + long gw_band, gw_valid, gw_luhn, gw_digsum, gw_thr; + + /* --- 1) 입력 캡처 --------------------------------------------- */ + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_STR4, gw_proc, sizeof(gw_proc)); if (gw_proc[0] == 0) strcpy(gw_proc, "000000"); + gets_(b, T_STR3, gw_raw, sizeof(gw_raw)); + gets_(b, T_STR5, gw_pan, sizeof(gw_pan)); + gw_amount = getl(b, T_AMOUNT); + gw_len = (long)strlen(gw_raw); + gw_thr = 134L; + + /* --- 2) 필드 형식 검증: MTI/처리코드 숫자성 -------------------- */ + gw_valid = 1; + for (gw_i = 0; gw_i < 4 && gw_mti[gw_i]; gw_i++) + if (gw_mti[gw_i] < '0' || gw_mti[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9401); sets_(b, T_MSG, "MTI 비숫자 포함"); FAIL(b); } + for (gw_i = 0; gw_i < 6 && gw_proc[gw_i]; gw_i++) + if (gw_proc[gw_i] < '0' || gw_proc[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9402); sets_(b, T_MSG, "처리코드 비숫자 포함"); FAIL(b); } + + /* --- 3) 1차 비트맵 구성 (존재필드 마스크) --------------------- */ + gw_mask1 = 0; + gw_mask1 |= (1UL << 63); /* F1: 2차 비트맵 지시 */ + if (gw_pan[0]) gw_mask1 |= (1UL << 62); /* F2: PAN */ + if (gw_proc[0]) gw_mask1 |= (1UL << 61); /* F3: 처리코드 */ + if (gw_amount) gw_mask1 |= (1UL << 60); /* F4: 금액 */ + if (gw_len > 0) gw_mask1 |= (1UL << 52); /* F11: STAN 존재 */ + sprintf(gw_bmp1, "%016lX", gw_mask1); + + /* --- 4) 2차 비트맵 구성 (확장필드) --------------------------- */ + gw_mask2 = 0; + if (gw_amount >= 1000000L) gw_mask2 |= (1UL << 63); /* F65: 고액표식 */ + if (gw_len >= 38) gw_mask2 |= (1UL << 60); /* F68: 확장데이터 */ + sprintf(gw_bmp2, "%016lX", gw_mask2); + + /* --- 5) 존재필드 수 계산 ------------------------------------- */ + gw_present = 0; + for (gw_i = 0; gw_i < 64; gw_i++) + if (gw_mask1 & (1UL << gw_i)) gw_present++; + + /* --- 6) LRC/합계 체크섬 (raw XOR + 누적합) ------------------- */ + gw_lrc = 0; gw_sum = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) { + gw_lrc ^= (unsigned char)gw_raw[gw_i]; + gw_sum += (unsigned char)gw_raw[gw_i]; + } + + /* --- 7) 간이 MAC (회전 누산 해시) --------------------------- */ + gw_mac = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) + gw_mac = ((gw_mac << 5) ^ (gw_mac >> 2) + ^ (unsigned long)(unsigned char)gw_raw[gw_i]) & 0xFFFFFFUL; + + /* --- 8) PAN Luhn / 금액밴드 / 금액자리합 -------------------- */ + gw_luhn = (gw_pan[0] && acq_luhn_ok(gw_pan)) ? 1 : 0; + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + { long gw_a = gw_amount; gw_digsum = 0; + if (gw_a < 0) gw_a = -gw_a; + while (gw_a > 0) { gw_digsum += gw_a % 10; gw_a /= 10; } } + if (gw_band == 3 && gw_present < 3) { + setl(b, T_RC, 9403); sets_(b, T_MSG, "고액전문 필수필드 누락"); FAIL(b); + } + + /* --- 9) 필드추출 데모 + 응답보조(심화전용 필드) + 로깅 ------ */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + memset(gw_fld, 0, sizeof(gw_fld)); + if (gw_len >= 10) strncpy(gw_fld, gw_raw + 4, 6); + setl(b, T_ARG6, gw_present); + setl(b, T_ARG7, gw_band); + setl(b, T_ARG8, gw_digsum); + setl(b, T_AMT2, gw_sum); + setl(b, T_AMT3, gw_lrc); + sets_(b, T_STR8, gw_bmp1); + (void)gw_bmp2; (void)gw_luhn; (void)gw_mac; (void)gw_fld; + userlog("MG_BMPMERGE 심화계산 mti=%s bmp1=%s present=%ld lrc=%ld mac=%06lX band=%ld thr=%ld", + gw_mti, gw_bmp1, gw_present, gw_lrc, gw_mac, gw_band, gw_thr); + } char hex1[24], hex2[24], out[24]; unsigned long m1 = 0, m2 = 0, merged; diff --git a/app/src/mg/svc/MG_BMPPARSE.pgc b/app/src/mg/svc/MG_BMPPARSE.pgc index 807f7b4..9d2505c 100644 --- a/app/src/mg/svc/MG_BMPPARSE.pgc +++ b/app/src/mg/svc/MG_BMPPARSE.pgc @@ -9,6 +9,95 @@ void MG_BMPPARSE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문 파싱/비트맵/체크섬/MAC - 순수계산) - MG_BMPPARSE + * 필드형식검증 / 1·2차 비트맵 구성 / LRC·합계 체크섬 / 간이 MAC / + * PAN Luhn / 금액밴드 / 필드추출 / 응답보조 / 로깅 + * =================================================================== */ + { + char gw_mti[8], gw_proc[8], gw_raw[512], gw_pan[24]; + char gw_bmp1[24], gw_bmp2[24], gw_fld[64]; + unsigned long gw_mask1, gw_mask2, gw_mac; + long gw_amount, gw_len, gw_i, gw_lrc, gw_sum, gw_present; + long gw_band, gw_valid, gw_luhn, gw_digsum, gw_thr; + + /* --- 1) 입력 캡처 --------------------------------------------- */ + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_STR4, gw_proc, sizeof(gw_proc)); if (gw_proc[0] == 0) strcpy(gw_proc, "000000"); + gets_(b, T_STR3, gw_raw, sizeof(gw_raw)); + gets_(b, T_STR5, gw_pan, sizeof(gw_pan)); + gw_amount = getl(b, T_AMOUNT); + gw_len = (long)strlen(gw_raw); + gw_thr = 85L; + + /* --- 2) 필드 형식 검증: MTI/처리코드 숫자성 -------------------- */ + gw_valid = 1; + for (gw_i = 0; gw_i < 4 && gw_mti[gw_i]; gw_i++) + if (gw_mti[gw_i] < '0' || gw_mti[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9401); sets_(b, T_MSG, "MTI 비숫자 포함"); FAIL(b); } + for (gw_i = 0; gw_i < 6 && gw_proc[gw_i]; gw_i++) + if (gw_proc[gw_i] < '0' || gw_proc[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9402); sets_(b, T_MSG, "처리코드 비숫자 포함"); FAIL(b); } + + /* --- 3) 1차 비트맵 구성 (존재필드 마스크) --------------------- */ + gw_mask1 = 0; + gw_mask1 |= (1UL << 63); /* F1: 2차 비트맵 지시 */ + if (gw_pan[0]) gw_mask1 |= (1UL << 62); /* F2: PAN */ + if (gw_proc[0]) gw_mask1 |= (1UL << 61); /* F3: 처리코드 */ + if (gw_amount) gw_mask1 |= (1UL << 60); /* F4: 금액 */ + if (gw_len > 0) gw_mask1 |= (1UL << 52); /* F11: STAN 존재 */ + sprintf(gw_bmp1, "%016lX", gw_mask1); + + /* --- 4) 2차 비트맵 구성 (확장필드) --------------------------- */ + gw_mask2 = 0; + if (gw_amount >= 1000000L) gw_mask2 |= (1UL << 63); /* F65: 고액표식 */ + if (gw_len >= 38) gw_mask2 |= (1UL << 60); /* F68: 확장데이터 */ + sprintf(gw_bmp2, "%016lX", gw_mask2); + + /* --- 5) 존재필드 수 계산 ------------------------------------- */ + gw_present = 0; + for (gw_i = 0; gw_i < 64; gw_i++) + if (gw_mask1 & (1UL << gw_i)) gw_present++; + + /* --- 6) LRC/합계 체크섬 (raw XOR + 누적합) ------------------- */ + gw_lrc = 0; gw_sum = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) { + gw_lrc ^= (unsigned char)gw_raw[gw_i]; + gw_sum += (unsigned char)gw_raw[gw_i]; + } + + /* --- 7) 간이 MAC (회전 누산 해시) --------------------------- */ + gw_mac = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) + gw_mac = ((gw_mac << 5) ^ (gw_mac >> 2) + ^ (unsigned long)(unsigned char)gw_raw[gw_i]) & 0xFFFFFFUL; + + /* --- 8) PAN Luhn / 금액밴드 / 금액자리합 -------------------- */ + gw_luhn = (gw_pan[0] && acq_luhn_ok(gw_pan)) ? 1 : 0; + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + { long gw_a = gw_amount; gw_digsum = 0; + if (gw_a < 0) gw_a = -gw_a; + while (gw_a > 0) { gw_digsum += gw_a % 10; gw_a /= 10; } } + if (gw_band == 3 && gw_present < 3) { + setl(b, T_RC, 9403); sets_(b, T_MSG, "고액전문 필수필드 누락"); FAIL(b); + } + + /* --- 9) 필드추출 데모 + 응답보조(심화전용 필드) + 로깅 ------ */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + memset(gw_fld, 0, sizeof(gw_fld)); + if (gw_len >= 10) strncpy(gw_fld, gw_raw + 4, 6); + setl(b, T_ARG6, gw_present); + setl(b, T_ARG7, gw_band); + setl(b, T_ARG8, gw_digsum); + setl(b, T_AMT2, gw_sum); + setl(b, T_AMT3, gw_lrc); + sets_(b, T_STR8, gw_bmp1); + (void)gw_bmp2; (void)gw_luhn; (void)gw_mac; (void)gw_fld; + userlog("MG_BMPPARSE 심화계산 mti=%s bmp1=%s present=%ld lrc=%ld mac=%06lX band=%ld thr=%ld", + gw_mti, gw_bmp1, gw_present, gw_lrc, gw_mac, gw_band, gw_thr); + } char hex[24], list[256], tmp[8]; unsigned long mask = 0; long cnt = 0; diff --git a/app/src/mg/svc/MG_CAPRSP.pgc b/app/src/mg/svc/MG_CAPRSP.pgc index b571bc1..7e19735 100644 --- a/app/src/mg/svc/MG_CAPRSP.pgc +++ b/app/src/mg/svc/MG_CAPRSP.pgc @@ -10,6 +10,155 @@ void MG_CAPRSP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_CAPRSP + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_CAPRSP 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 120L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_CAPRSP", "C", gw_bizdate) < 0) { + userlog("MG_CAPRSP 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_CAPRSP 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_CAPRSP 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char chan[16], bizdate[16]; long stan, orig_id, msg_id, out_stan; diff --git a/app/src/mg/svc/MG_CAPTURE.pgc b/app/src/mg/svc/MG_CAPTURE.pgc index c5e7e5f..ed13ca1 100644 --- a/app/src/mg/svc/MG_CAPTURE.pgc +++ b/app/src/mg/svc/MG_CAPTURE.pgc @@ -10,6 +10,155 @@ void MG_CAPTURE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_CAPTURE + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_CAPTURE 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 135L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_CAPTURE", "C", gw_bizdate) < 0) { + userlog("MG_CAPTURE 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_CAPTURE 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_CAPTURE 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char chan[16], raw[512], bizdate[16]; long stan, msg_id, amount, bps, fee, net; diff --git a/app/src/mg/svc/MG_CARDVER.pgc b/app/src/mg/svc/MG_CARDVER.pgc index 00062ee..e7d2604 100644 --- a/app/src/mg/svc/MG_CARDVER.pgc +++ b/app/src/mg/svc/MG_CARDVER.pgc @@ -19,6 +19,95 @@ static void mg_cardver_mask(const char *pan, char *out /* >= 20 */) void MG_CARDVER(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문 파싱/비트맵/체크섬/MAC - 순수계산) - MG_CARDVER + * 필드형식검증 / 1·2차 비트맵 구성 / LRC·합계 체크섬 / 간이 MAC / + * PAN Luhn / 금액밴드 / 필드추출 / 응답보조 / 로깅 + * =================================================================== */ + { + char gw_mti[8], gw_proc[8], gw_raw[512], gw_pan[24]; + char gw_bmp1[24], gw_bmp2[24], gw_fld[64]; + unsigned long gw_mask1, gw_mask2, gw_mac; + long gw_amount, gw_len, gw_i, gw_lrc, gw_sum, gw_present; + long gw_band, gw_valid, gw_luhn, gw_digsum, gw_thr; + + /* --- 1) 입력 캡처 --------------------------------------------- */ + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_STR4, gw_proc, sizeof(gw_proc)); if (gw_proc[0] == 0) strcpy(gw_proc, "000000"); + gets_(b, T_STR3, gw_raw, sizeof(gw_raw)); + gets_(b, T_STR5, gw_pan, sizeof(gw_pan)); + gw_amount = getl(b, T_AMOUNT); + gw_len = (long)strlen(gw_raw); + gw_thr = 122L; + + /* --- 2) 필드 형식 검증: MTI/처리코드 숫자성 -------------------- */ + gw_valid = 1; + for (gw_i = 0; gw_i < 4 && gw_mti[gw_i]; gw_i++) + if (gw_mti[gw_i] < '0' || gw_mti[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9401); sets_(b, T_MSG, "MTI 비숫자 포함"); FAIL(b); } + for (gw_i = 0; gw_i < 6 && gw_proc[gw_i]; gw_i++) + if (gw_proc[gw_i] < '0' || gw_proc[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9402); sets_(b, T_MSG, "처리코드 비숫자 포함"); FAIL(b); } + + /* --- 3) 1차 비트맵 구성 (존재필드 마스크) --------------------- */ + gw_mask1 = 0; + gw_mask1 |= (1UL << 63); /* F1: 2차 비트맵 지시 */ + if (gw_pan[0]) gw_mask1 |= (1UL << 62); /* F2: PAN */ + if (gw_proc[0]) gw_mask1 |= (1UL << 61); /* F3: 처리코드 */ + if (gw_amount) gw_mask1 |= (1UL << 60); /* F4: 금액 */ + if (gw_len > 0) gw_mask1 |= (1UL << 52); /* F11: STAN 존재 */ + sprintf(gw_bmp1, "%016lX", gw_mask1); + + /* --- 4) 2차 비트맵 구성 (확장필드) --------------------------- */ + gw_mask2 = 0; + if (gw_amount >= 1000000L) gw_mask2 |= (1UL << 63); /* F65: 고액표식 */ + if (gw_len >= 38) gw_mask2 |= (1UL << 60); /* F68: 확장데이터 */ + sprintf(gw_bmp2, "%016lX", gw_mask2); + + /* --- 5) 존재필드 수 계산 ------------------------------------- */ + gw_present = 0; + for (gw_i = 0; gw_i < 64; gw_i++) + if (gw_mask1 & (1UL << gw_i)) gw_present++; + + /* --- 6) LRC/합계 체크섬 (raw XOR + 누적합) ------------------- */ + gw_lrc = 0; gw_sum = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) { + gw_lrc ^= (unsigned char)gw_raw[gw_i]; + gw_sum += (unsigned char)gw_raw[gw_i]; + } + + /* --- 7) 간이 MAC (회전 누산 해시) --------------------------- */ + gw_mac = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) + gw_mac = ((gw_mac << 5) ^ (gw_mac >> 2) + ^ (unsigned long)(unsigned char)gw_raw[gw_i]) & 0xFFFFFFUL; + + /* --- 8) PAN Luhn / 금액밴드 / 금액자리합 -------------------- */ + gw_luhn = (gw_pan[0] && acq_luhn_ok(gw_pan)) ? 1 : 0; + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + { long gw_a = gw_amount; gw_digsum = 0; + if (gw_a < 0) gw_a = -gw_a; + while (gw_a > 0) { gw_digsum += gw_a % 10; gw_a /= 10; } } + if (gw_band == 3 && gw_present < 3) { + setl(b, T_RC, 9403); sets_(b, T_MSG, "고액전문 필수필드 누락"); FAIL(b); + } + + /* --- 9) 필드추출 데모 + 응답보조(심화전용 필드) + 로깅 ------ */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + memset(gw_fld, 0, sizeof(gw_fld)); + if (gw_len >= 10) strncpy(gw_fld, gw_raw + 4, 6); + setl(b, T_ARG6, gw_present); + setl(b, T_ARG7, gw_band); + setl(b, T_ARG8, gw_digsum); + setl(b, T_AMT2, gw_sum); + setl(b, T_AMT3, gw_lrc); + sets_(b, T_STR8, gw_bmp1); + (void)gw_bmp2; (void)gw_luhn; (void)gw_mac; (void)gw_fld; + userlog("MG_CARDVER 심화계산 mti=%s bmp1=%s present=%ld lrc=%ld mac=%06lX band=%ld thr=%ld", + gw_mti, gw_bmp1, gw_present, gw_lrc, gw_mac, gw_band, gw_thr); + } char chan[16], pan[24], masked[24], rc[4], bizdate[16]; long stan, msg_id, plen; diff --git a/app/src/mg/svc/MG_CHANAUTH.pgc b/app/src/mg/svc/MG_CHANAUTH.pgc index 494e00e..4554ee7 100644 --- a/app/src/mg/svc/MG_CHANAUTH.pgc +++ b/app/src/mg/svc/MG_CHANAUTH.pgc @@ -9,6 +9,155 @@ void MG_CHANAUTH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_CHANAUTH + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_CHANAUTH 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 131L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_CHANAUTH", "C", gw_bizdate) < 0) { + userlog("MG_CHANAUTH 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_CHANAUTH 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_CHANAUTH 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char chan[16], st[16]; gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "CH01"); if (mgdb_get_channel_status(chan, st) < 0) { setl(b, T_RC, 2); FAIL(b); } diff --git a/app/src/mg/svc/MG_CHANCLOSE.pgc b/app/src/mg/svc/MG_CHANCLOSE.pgc index 1679cab..dd2582b 100644 --- a/app/src/mg/svc/MG_CHANCLOSE.pgc +++ b/app/src/mg/svc/MG_CHANCLOSE.pgc @@ -10,6 +10,155 @@ void MG_CHANCLOSE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_CHANCLOSE + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_CHANCLOSE 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 139L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_CHANCLOSE", "C", gw_bizdate) < 0) { + userlog("MG_CHANCLOSE 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_CHANCLOSE 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_CHANCLOSE 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } EXEC SQL BEGIN DECLARE SECTION; char h_chan[16]; long h_pending; diff --git a/app/src/mg/svc/MG_CHANFAILOVER.pgc b/app/src/mg/svc/MG_CHANFAILOVER.pgc index 7776919..e1bb22d 100644 --- a/app/src/mg/svc/MG_CHANFAILOVER.pgc +++ b/app/src/mg/svc/MG_CHANFAILOVER.pgc @@ -9,6 +9,155 @@ void MG_CHANFAILOVER(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_CHANFAILOVER + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_CHANFAILOVER 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 125L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_CHANFAILOVER", "C", gw_bizdate) < 0) { + userlog("MG_CHANFAILOVER 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_CHANFAILOVER 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_CHANFAILOVER 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char primary[16], backup[16], curp[16], curb[16]; EXEC SQL BEGIN DECLARE SECTION; char h_primary[16], h_backup[16]; diff --git a/app/src/mg/svc/MG_CHANHEALTH.pgc b/app/src/mg/svc/MG_CHANHEALTH.pgc index d4bdad0..e869c1b 100644 --- a/app/src/mg/svc/MG_CHANHEALTH.pgc +++ b/app/src/mg/svc/MG_CHANHEALTH.pgc @@ -10,6 +10,155 @@ void MG_CHANHEALTH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_CHANHEALTH + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_CHANHEALTH 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 83L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_CHANHEALTH", "C", gw_bizdate) < 0) { + userlog("MG_CHANHEALTH 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_CHANHEALTH 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_CHANHEALTH 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } long score, err_bps, up; char status[16]; EXEC SQL BEGIN DECLARE SECTION; diff --git a/app/src/mg/svc/MG_CHANLIST.pgc b/app/src/mg/svc/MG_CHANLIST.pgc index 36d944e..704806e 100644 --- a/app/src/mg/svc/MG_CHANLIST.pgc +++ b/app/src/mg/svc/MG_CHANLIST.pgc @@ -9,6 +9,155 @@ void MG_CHANLIST(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_CHANLIST + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_CHANLIST 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 81L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_CHANLIST", "C", gw_bizdate) < 0) { + userlog("MG_CHANLIST 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_CHANLIST 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_CHANLIST 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char list[256], tmp[24]; long up = 0, down = 0; EXEC SQL BEGIN DECLARE SECTION; diff --git a/app/src/mg/svc/MG_CHANMAP.pgc b/app/src/mg/svc/MG_CHANMAP.pgc index 9fe1332..7d215b7 100644 --- a/app/src/mg/svc/MG_CHANMAP.pgc +++ b/app/src/mg/svc/MG_CHANMAP.pgc @@ -9,6 +9,95 @@ void MG_CHANMAP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문 파싱/비트맵/체크섬/MAC - 순수계산) - MG_CHANMAP + * 필드형식검증 / 1·2차 비트맵 구성 / LRC·합계 체크섬 / 간이 MAC / + * PAN Luhn / 금액밴드 / 필드추출 / 응답보조 / 로깅 + * =================================================================== */ + { + char gw_mti[8], gw_proc[8], gw_raw[512], gw_pan[24]; + char gw_bmp1[24], gw_bmp2[24], gw_fld[64]; + unsigned long gw_mask1, gw_mask2, gw_mac; + long gw_amount, gw_len, gw_i, gw_lrc, gw_sum, gw_present; + long gw_band, gw_valid, gw_luhn, gw_digsum, gw_thr; + + /* --- 1) 입력 캡처 --------------------------------------------- */ + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_STR4, gw_proc, sizeof(gw_proc)); if (gw_proc[0] == 0) strcpy(gw_proc, "000000"); + gets_(b, T_STR3, gw_raw, sizeof(gw_raw)); + gets_(b, T_STR5, gw_pan, sizeof(gw_pan)); + gw_amount = getl(b, T_AMOUNT); + gw_len = (long)strlen(gw_raw); + gw_thr = 107L; + + /* --- 2) 필드 형식 검증: MTI/처리코드 숫자성 -------------------- */ + gw_valid = 1; + for (gw_i = 0; gw_i < 4 && gw_mti[gw_i]; gw_i++) + if (gw_mti[gw_i] < '0' || gw_mti[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9401); sets_(b, T_MSG, "MTI 비숫자 포함"); FAIL(b); } + for (gw_i = 0; gw_i < 6 && gw_proc[gw_i]; gw_i++) + if (gw_proc[gw_i] < '0' || gw_proc[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9402); sets_(b, T_MSG, "처리코드 비숫자 포함"); FAIL(b); } + + /* --- 3) 1차 비트맵 구성 (존재필드 마스크) --------------------- */ + gw_mask1 = 0; + gw_mask1 |= (1UL << 63); /* F1: 2차 비트맵 지시 */ + if (gw_pan[0]) gw_mask1 |= (1UL << 62); /* F2: PAN */ + if (gw_proc[0]) gw_mask1 |= (1UL << 61); /* F3: 처리코드 */ + if (gw_amount) gw_mask1 |= (1UL << 60); /* F4: 금액 */ + if (gw_len > 0) gw_mask1 |= (1UL << 52); /* F11: STAN 존재 */ + sprintf(gw_bmp1, "%016lX", gw_mask1); + + /* --- 4) 2차 비트맵 구성 (확장필드) --------------------------- */ + gw_mask2 = 0; + if (gw_amount >= 1000000L) gw_mask2 |= (1UL << 63); /* F65: 고액표식 */ + if (gw_len >= 38) gw_mask2 |= (1UL << 60); /* F68: 확장데이터 */ + sprintf(gw_bmp2, "%016lX", gw_mask2); + + /* --- 5) 존재필드 수 계산 ------------------------------------- */ + gw_present = 0; + for (gw_i = 0; gw_i < 64; gw_i++) + if (gw_mask1 & (1UL << gw_i)) gw_present++; + + /* --- 6) LRC/합계 체크섬 (raw XOR + 누적합) ------------------- */ + gw_lrc = 0; gw_sum = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) { + gw_lrc ^= (unsigned char)gw_raw[gw_i]; + gw_sum += (unsigned char)gw_raw[gw_i]; + } + + /* --- 7) 간이 MAC (회전 누산 해시) --------------------------- */ + gw_mac = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) + gw_mac = ((gw_mac << 5) ^ (gw_mac >> 2) + ^ (unsigned long)(unsigned char)gw_raw[gw_i]) & 0xFFFFFFUL; + + /* --- 8) PAN Luhn / 금액밴드 / 금액자리합 -------------------- */ + gw_luhn = (gw_pan[0] && acq_luhn_ok(gw_pan)) ? 1 : 0; + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + { long gw_a = gw_amount; gw_digsum = 0; + if (gw_a < 0) gw_a = -gw_a; + while (gw_a > 0) { gw_digsum += gw_a % 10; gw_a /= 10; } } + if (gw_band == 3 && gw_present < 3) { + setl(b, T_RC, 9403); sets_(b, T_MSG, "고액전문 필수필드 누락"); FAIL(b); + } + + /* --- 9) 필드추출 데모 + 응답보조(심화전용 필드) + 로깅 ------ */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + memset(gw_fld, 0, sizeof(gw_fld)); + if (gw_len >= 10) strncpy(gw_fld, gw_raw + 4, 6); + setl(b, T_ARG6, gw_present); + setl(b, T_ARG7, gw_band); + setl(b, T_ARG8, gw_digsum); + setl(b, T_AMT2, gw_sum); + setl(b, T_AMT3, gw_lrc); + sets_(b, T_STR8, gw_bmp1); + (void)gw_bmp2; (void)gw_luhn; (void)gw_mac; (void)gw_fld; + userlog("MG_CHANMAP 심화계산 mti=%s bmp1=%s present=%ld lrc=%ld mac=%06lX band=%ld thr=%ld", + gw_mti, gw_bmp1, gw_present, gw_lrc, gw_mac, gw_band, gw_thr); + } char ext[24], chan[16]; gets_(b, T_STR1, ext, sizeof(ext)); diff --git a/app/src/mg/svc/MG_CHANOPEN.pgc b/app/src/mg/svc/MG_CHANOPEN.pgc index cf33e4b..e192ed0 100644 --- a/app/src/mg/svc/MG_CHANOPEN.pgc +++ b/app/src/mg/svc/MG_CHANOPEN.pgc @@ -10,6 +10,155 @@ void MG_CHANOPEN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_CHANOPEN + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_CHANOPEN 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 131L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_CHANOPEN", "C", gw_bizdate) < 0) { + userlog("MG_CHANOPEN 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_CHANOPEN 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_CHANOPEN 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char chan[16], cur[16]; EXEC SQL BEGIN DECLARE SECTION; char h_chan[16]; diff --git a/app/src/mg/svc/MG_CHANPING.pgc b/app/src/mg/svc/MG_CHANPING.pgc index 14a4ffb..e84ea10 100644 --- a/app/src/mg/svc/MG_CHANPING.pgc +++ b/app/src/mg/svc/MG_CHANPING.pgc @@ -9,6 +9,155 @@ void MG_CHANPING(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_CHANPING + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_CHANPING 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 127L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_CHANPING", "C", gw_bizdate) < 0) { + userlog("MG_CHANPING 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_CHANPING 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_CHANPING 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char chan[16], cur[16]; EXEC SQL BEGIN DECLARE SECTION; char h_chan[16]; diff --git a/app/src/mg/svc/MG_CHANRESET.pgc b/app/src/mg/svc/MG_CHANRESET.pgc index e3a70f3..8685e78 100644 --- a/app/src/mg/svc/MG_CHANRESET.pgc +++ b/app/src/mg/svc/MG_CHANRESET.pgc @@ -9,6 +9,155 @@ void MG_CHANRESET(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_CHANRESET + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_CHANRESET 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 92L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_CHANRESET", "C", gw_bizdate) < 0) { + userlog("MG_CHANRESET 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_CHANRESET 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_CHANRESET 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char chan[16], cur[16]; EXEC SQL BEGIN DECLARE SECTION; char h_chan[16]; diff --git a/app/src/mg/svc/MG_CHANSTAT.pgc b/app/src/mg/svc/MG_CHANSTAT.pgc index e246dc9..293dcf3 100644 --- a/app/src/mg/svc/MG_CHANSTAT.pgc +++ b/app/src/mg/svc/MG_CHANSTAT.pgc @@ -9,6 +9,155 @@ void MG_CHANSTAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_CHANSTAT + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_CHANSTAT 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 81L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_CHANSTAT", "C", gw_bizdate) < 0) { + userlog("MG_CHANSTAT 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_CHANSTAT 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_CHANSTAT 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char chan[16], st[16]; gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "CH01"); if (mgdb_get_channel_status(chan, st) < 0) FAIL(b); diff --git a/app/src/mg/svc/MG_CHANSWITCH.pgc b/app/src/mg/svc/MG_CHANSWITCH.pgc index 9682144..04c85e1 100644 --- a/app/src/mg/svc/MG_CHANSWITCH.pgc +++ b/app/src/mg/svc/MG_CHANSWITCH.pgc @@ -10,6 +10,155 @@ void MG_CHANSWITCH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_CHANSWITCH + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_CHANSWITCH 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 111L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_CHANSWITCH", "C", gw_bizdate) < 0) { + userlog("MG_CHANSWITCH 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_CHANSWITCH 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_CHANSWITCH 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char pri[16], bak[16]; long moved; EXEC SQL BEGIN DECLARE SECTION; diff --git a/app/src/mg/svc/MG_CONFIRM.pgc b/app/src/mg/svc/MG_CONFIRM.pgc index e1d482d..a14a841 100644 --- a/app/src/mg/svc/MG_CONFIRM.pgc +++ b/app/src/mg/svc/MG_CONFIRM.pgc @@ -9,6 +9,155 @@ void MG_CONFIRM(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_CONFIRM + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_CONFIRM 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 129L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_CONFIRM", "C", gw_bizdate) < 0) { + userlog("MG_CONFIRM 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_CONFIRM 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_CONFIRM 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } long msg_id = getl(b, T_ID1); if (mgdb_update_msg_rc(msg_id, "00", "C") < 0) FAIL(b); sets_(b, T_STATUS, "C"); diff --git a/app/src/mg/svc/MG_CONVERT.pgc b/app/src/mg/svc/MG_CONVERT.pgc index 59ca6a4..3fc66ba 100644 --- a/app/src/mg/svc/MG_CONVERT.pgc +++ b/app/src/mg/svc/MG_CONVERT.pgc @@ -9,6 +9,155 @@ void MG_CONVERT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_CONVERT + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_CONVERT 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 88L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_CONVERT", "C", gw_bizdate) < 0) { + userlog("MG_CONVERT 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_CONVERT 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_CONVERT 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char chan[16], mti[8], rmti[8], raw[512], bizdate[16]; long stan, msg_id, amount, mnum; diff --git a/app/src/mg/svc/MG_DECODE.pgc b/app/src/mg/svc/MG_DECODE.pgc index 01e8f6f..54ecaa7 100644 --- a/app/src/mg/svc/MG_DECODE.pgc +++ b/app/src/mg/svc/MG_DECODE.pgc @@ -9,6 +9,95 @@ void MG_DECODE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문 파싱/비트맵/체크섬/MAC - 순수계산) - MG_DECODE + * 필드형식검증 / 1·2차 비트맵 구성 / LRC·합계 체크섬 / 간이 MAC / + * PAN Luhn / 금액밴드 / 필드추출 / 응답보조 / 로깅 + * =================================================================== */ + { + char gw_mti[8], gw_proc[8], gw_raw[512], gw_pan[24]; + char gw_bmp1[24], gw_bmp2[24], gw_fld[64]; + unsigned long gw_mask1, gw_mask2, gw_mac; + long gw_amount, gw_len, gw_i, gw_lrc, gw_sum, gw_present; + long gw_band, gw_valid, gw_luhn, gw_digsum, gw_thr; + + /* --- 1) 입력 캡처 --------------------------------------------- */ + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_STR4, gw_proc, sizeof(gw_proc)); if (gw_proc[0] == 0) strcpy(gw_proc, "000000"); + gets_(b, T_STR3, gw_raw, sizeof(gw_raw)); + gets_(b, T_STR5, gw_pan, sizeof(gw_pan)); + gw_amount = getl(b, T_AMOUNT); + gw_len = (long)strlen(gw_raw); + gw_thr = 83L; + + /* --- 2) 필드 형식 검증: MTI/처리코드 숫자성 -------------------- */ + gw_valid = 1; + for (gw_i = 0; gw_i < 4 && gw_mti[gw_i]; gw_i++) + if (gw_mti[gw_i] < '0' || gw_mti[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9401); sets_(b, T_MSG, "MTI 비숫자 포함"); FAIL(b); } + for (gw_i = 0; gw_i < 6 && gw_proc[gw_i]; gw_i++) + if (gw_proc[gw_i] < '0' || gw_proc[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9402); sets_(b, T_MSG, "처리코드 비숫자 포함"); FAIL(b); } + + /* --- 3) 1차 비트맵 구성 (존재필드 마스크) --------------------- */ + gw_mask1 = 0; + gw_mask1 |= (1UL << 63); /* F1: 2차 비트맵 지시 */ + if (gw_pan[0]) gw_mask1 |= (1UL << 62); /* F2: PAN */ + if (gw_proc[0]) gw_mask1 |= (1UL << 61); /* F3: 처리코드 */ + if (gw_amount) gw_mask1 |= (1UL << 60); /* F4: 금액 */ + if (gw_len > 0) gw_mask1 |= (1UL << 52); /* F11: STAN 존재 */ + sprintf(gw_bmp1, "%016lX", gw_mask1); + + /* --- 4) 2차 비트맵 구성 (확장필드) --------------------------- */ + gw_mask2 = 0; + if (gw_amount >= 1000000L) gw_mask2 |= (1UL << 63); /* F65: 고액표식 */ + if (gw_len >= 38) gw_mask2 |= (1UL << 60); /* F68: 확장데이터 */ + sprintf(gw_bmp2, "%016lX", gw_mask2); + + /* --- 5) 존재필드 수 계산 ------------------------------------- */ + gw_present = 0; + for (gw_i = 0; gw_i < 64; gw_i++) + if (gw_mask1 & (1UL << gw_i)) gw_present++; + + /* --- 6) LRC/합계 체크섬 (raw XOR + 누적합) ------------------- */ + gw_lrc = 0; gw_sum = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) { + gw_lrc ^= (unsigned char)gw_raw[gw_i]; + gw_sum += (unsigned char)gw_raw[gw_i]; + } + + /* --- 7) 간이 MAC (회전 누산 해시) --------------------------- */ + gw_mac = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) + gw_mac = ((gw_mac << 5) ^ (gw_mac >> 2) + ^ (unsigned long)(unsigned char)gw_raw[gw_i]) & 0xFFFFFFUL; + + /* --- 8) PAN Luhn / 금액밴드 / 금액자리합 -------------------- */ + gw_luhn = (gw_pan[0] && acq_luhn_ok(gw_pan)) ? 1 : 0; + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + { long gw_a = gw_amount; gw_digsum = 0; + if (gw_a < 0) gw_a = -gw_a; + while (gw_a > 0) { gw_digsum += gw_a % 10; gw_a /= 10; } } + if (gw_band == 3 && gw_present < 3) { + setl(b, T_RC, 9403); sets_(b, T_MSG, "고액전문 필수필드 누락"); FAIL(b); + } + + /* --- 9) 필드추출 데모 + 응답보조(심화전용 필드) + 로깅 ------ */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + memset(gw_fld, 0, sizeof(gw_fld)); + if (gw_len >= 10) strncpy(gw_fld, gw_raw + 4, 6); + setl(b, T_ARG6, gw_present); + setl(b, T_ARG7, gw_band); + setl(b, T_ARG8, gw_digsum); + setl(b, T_AMT2, gw_sum); + setl(b, T_AMT3, gw_lrc); + sets_(b, T_STR8, gw_bmp1); + (void)gw_bmp2; (void)gw_luhn; (void)gw_mac; (void)gw_fld; + userlog("MG_DECODE 심화계산 mti=%s bmp1=%s present=%ld lrc=%ld mac=%06lX band=%ld thr=%ld", + gw_mti, gw_bmp1, gw_present, gw_lrc, gw_mac, gw_band, gw_thr); + } char raw[64], mti[8], proc[8], chan[8]; long amount, stan, n; diff --git a/app/src/mg/svc/MG_DEQUEUE.pgc b/app/src/mg/svc/MG_DEQUEUE.pgc index 354f69d..b8c3088 100644 --- a/app/src/mg/svc/MG_DEQUEUE.pgc +++ b/app/src/mg/svc/MG_DEQUEUE.pgc @@ -9,6 +9,155 @@ void MG_DEQUEUE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_DEQUEUE + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_DEQUEUE 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 129L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_DEQUEUE", "C", gw_bizdate) < 0) { + userlog("MG_DEQUEUE 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_DEQUEUE 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_DEQUEUE 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } EXEC SQL BEGIN DECLARE SECTION; long h_qid, h_stan; char h_payload[512], h_chan[16]; diff --git a/app/src/mg/svc/MG_DIRECTADPT.pgc b/app/src/mg/svc/MG_DIRECTADPT.pgc index d2a6084..52f0622 100644 --- a/app/src/mg/svc/MG_DIRECTADPT.pgc +++ b/app/src/mg/svc/MG_DIRECTADPT.pgc @@ -21,6 +21,155 @@ static int mg_direct_digits(const char *s, int n) void MG_DIRECTADPT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_DIRECTADPT + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_DIRECTADPT 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 103L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_DIRECTADPT", "C", gw_bizdate) < 0) { + userlog("MG_DIRECTADPT 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_DIRECTADPT 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_DIRECTADPT 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char raw[512], mti[8]; long n, amount; diff --git a/app/src/mg/svc/MG_DUPCHK.pgc b/app/src/mg/svc/MG_DUPCHK.pgc index 9636ee5..d80aacf 100644 --- a/app/src/mg/svc/MG_DUPCHK.pgc +++ b/app/src/mg/svc/MG_DUPCHK.pgc @@ -10,6 +10,155 @@ void MG_DUPCHK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_DUPCHK + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_DUPCHK 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 110L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_DUPCHK", "C", gw_bizdate) < 0) { + userlog("MG_DUPCHK 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_DUPCHK 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_DUPCHK 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } EXEC SQL BEGIN DECLARE SECTION; long h_stan, h_cnt; char h_bizdate[16]; diff --git a/app/src/mg/svc/MG_DUPPURGE.pgc b/app/src/mg/svc/MG_DUPPURGE.pgc index 591101a..c9083d0 100644 --- a/app/src/mg/svc/MG_DUPPURGE.pgc +++ b/app/src/mg/svc/MG_DUPPURGE.pgc @@ -9,6 +9,155 @@ void MG_DUPPURGE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_DUPPURGE + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_DUPPURGE 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 103L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_DUPPURGE", "C", gw_bizdate) < 0) { + userlog("MG_DUPPURGE 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_DUPPURGE 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_DUPPURGE 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } long purged; EXEC SQL BEGIN DECLARE SECTION; long h_stan, h_keep; diff --git a/app/src/mg/svc/MG_ECHO.pgc b/app/src/mg/svc/MG_ECHO.pgc index 35e6f38..2831f1b 100644 --- a/app/src/mg/svc/MG_ECHO.pgc +++ b/app/src/mg/svc/MG_ECHO.pgc @@ -9,6 +9,155 @@ void MG_ECHO(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_ECHO + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_ECHO 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 130L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_ECHO", "C", gw_bizdate) < 0) { + userlog("MG_ECHO 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_ECHO 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_ECHO 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char chan[16], bizdate[16]; long stan, msg_id; gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "CH01"); diff --git a/app/src/mg/svc/MG_ENCODE.pgc b/app/src/mg/svc/MG_ENCODE.pgc index b32ba25..f269edb 100644 --- a/app/src/mg/svc/MG_ENCODE.pgc +++ b/app/src/mg/svc/MG_ENCODE.pgc @@ -9,6 +9,95 @@ void MG_ENCODE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문 파싱/비트맵/체크섬/MAC - 순수계산) - MG_ENCODE + * 필드형식검증 / 1·2차 비트맵 구성 / LRC·합계 체크섬 / 간이 MAC / + * PAN Luhn / 금액밴드 / 필드추출 / 응답보조 / 로깅 + * =================================================================== */ + { + char gw_mti[8], gw_proc[8], gw_raw[512], gw_pan[24]; + char gw_bmp1[24], gw_bmp2[24], gw_fld[64]; + unsigned long gw_mask1, gw_mask2, gw_mac; + long gw_amount, gw_len, gw_i, gw_lrc, gw_sum, gw_present; + long gw_band, gw_valid, gw_luhn, gw_digsum, gw_thr; + + /* --- 1) 입력 캡처 --------------------------------------------- */ + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_STR4, gw_proc, sizeof(gw_proc)); if (gw_proc[0] == 0) strcpy(gw_proc, "000000"); + gets_(b, T_STR3, gw_raw, sizeof(gw_raw)); + gets_(b, T_STR5, gw_pan, sizeof(gw_pan)); + gw_amount = getl(b, T_AMOUNT); + gw_len = (long)strlen(gw_raw); + gw_thr = 93L; + + /* --- 2) 필드 형식 검증: MTI/처리코드 숫자성 -------------------- */ + gw_valid = 1; + for (gw_i = 0; gw_i < 4 && gw_mti[gw_i]; gw_i++) + if (gw_mti[gw_i] < '0' || gw_mti[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9401); sets_(b, T_MSG, "MTI 비숫자 포함"); FAIL(b); } + for (gw_i = 0; gw_i < 6 && gw_proc[gw_i]; gw_i++) + if (gw_proc[gw_i] < '0' || gw_proc[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9402); sets_(b, T_MSG, "처리코드 비숫자 포함"); FAIL(b); } + + /* --- 3) 1차 비트맵 구성 (존재필드 마스크) --------------------- */ + gw_mask1 = 0; + gw_mask1 |= (1UL << 63); /* F1: 2차 비트맵 지시 */ + if (gw_pan[0]) gw_mask1 |= (1UL << 62); /* F2: PAN */ + if (gw_proc[0]) gw_mask1 |= (1UL << 61); /* F3: 처리코드 */ + if (gw_amount) gw_mask1 |= (1UL << 60); /* F4: 금액 */ + if (gw_len > 0) gw_mask1 |= (1UL << 52); /* F11: STAN 존재 */ + sprintf(gw_bmp1, "%016lX", gw_mask1); + + /* --- 4) 2차 비트맵 구성 (확장필드) --------------------------- */ + gw_mask2 = 0; + if (gw_amount >= 1000000L) gw_mask2 |= (1UL << 63); /* F65: 고액표식 */ + if (gw_len >= 38) gw_mask2 |= (1UL << 60); /* F68: 확장데이터 */ + sprintf(gw_bmp2, "%016lX", gw_mask2); + + /* --- 5) 존재필드 수 계산 ------------------------------------- */ + gw_present = 0; + for (gw_i = 0; gw_i < 64; gw_i++) + if (gw_mask1 & (1UL << gw_i)) gw_present++; + + /* --- 6) LRC/합계 체크섬 (raw XOR + 누적합) ------------------- */ + gw_lrc = 0; gw_sum = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) { + gw_lrc ^= (unsigned char)gw_raw[gw_i]; + gw_sum += (unsigned char)gw_raw[gw_i]; + } + + /* --- 7) 간이 MAC (회전 누산 해시) --------------------------- */ + gw_mac = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) + gw_mac = ((gw_mac << 5) ^ (gw_mac >> 2) + ^ (unsigned long)(unsigned char)gw_raw[gw_i]) & 0xFFFFFFUL; + + /* --- 8) PAN Luhn / 금액밴드 / 금액자리합 -------------------- */ + gw_luhn = (gw_pan[0] && acq_luhn_ok(gw_pan)) ? 1 : 0; + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + { long gw_a = gw_amount; gw_digsum = 0; + if (gw_a < 0) gw_a = -gw_a; + while (gw_a > 0) { gw_digsum += gw_a % 10; gw_a /= 10; } } + if (gw_band == 3 && gw_present < 3) { + setl(b, T_RC, 9403); sets_(b, T_MSG, "고액전문 필수필드 누락"); FAIL(b); + } + + /* --- 9) 필드추출 데모 + 응답보조(심화전용 필드) + 로깅 ------ */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + memset(gw_fld, 0, sizeof(gw_fld)); + if (gw_len >= 10) strncpy(gw_fld, gw_raw + 4, 6); + setl(b, T_ARG6, gw_present); + setl(b, T_ARG7, gw_band); + setl(b, T_ARG8, gw_digsum); + setl(b, T_AMT2, gw_sum); + setl(b, T_AMT3, gw_lrc); + sets_(b, T_STR8, gw_bmp1); + (void)gw_bmp2; (void)gw_luhn; (void)gw_mac; (void)gw_fld; + userlog("MG_ENCODE 심화계산 mti=%s bmp1=%s present=%ld lrc=%ld mac=%06lX band=%ld thr=%ld", + gw_mti, gw_bmp1, gw_present, gw_lrc, gw_mac, gw_band, gw_thr); + } char mti[8], proc[8], chan[16], raw[64]; long amount, stan; diff --git a/app/src/mg/svc/MG_ERRCLASS.pgc b/app/src/mg/svc/MG_ERRCLASS.pgc index 9ef1fce..63b20ba 100644 --- a/app/src/mg/svc/MG_ERRCLASS.pgc +++ b/app/src/mg/svc/MG_ERRCLASS.pgc @@ -9,6 +9,95 @@ void MG_ERRCLASS(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문 파싱/비트맵/체크섬/MAC - 순수계산) - MG_ERRCLASS + * 필드형식검증 / 1·2차 비트맵 구성 / LRC·합계 체크섬 / 간이 MAC / + * PAN Luhn / 금액밴드 / 필드추출 / 응답보조 / 로깅 + * =================================================================== */ + { + char gw_mti[8], gw_proc[8], gw_raw[512], gw_pan[24]; + char gw_bmp1[24], gw_bmp2[24], gw_fld[64]; + unsigned long gw_mask1, gw_mask2, gw_mac; + long gw_amount, gw_len, gw_i, gw_lrc, gw_sum, gw_present; + long gw_band, gw_valid, gw_luhn, gw_digsum, gw_thr; + + /* --- 1) 입력 캡처 --------------------------------------------- */ + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_STR4, gw_proc, sizeof(gw_proc)); if (gw_proc[0] == 0) strcpy(gw_proc, "000000"); + gets_(b, T_STR3, gw_raw, sizeof(gw_raw)); + gets_(b, T_STR5, gw_pan, sizeof(gw_pan)); + gw_amount = getl(b, T_AMOUNT); + gw_len = (long)strlen(gw_raw); + gw_thr = 90L; + + /* --- 2) 필드 형식 검증: MTI/처리코드 숫자성 -------------------- */ + gw_valid = 1; + for (gw_i = 0; gw_i < 4 && gw_mti[gw_i]; gw_i++) + if (gw_mti[gw_i] < '0' || gw_mti[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9401); sets_(b, T_MSG, "MTI 비숫자 포함"); FAIL(b); } + for (gw_i = 0; gw_i < 6 && gw_proc[gw_i]; gw_i++) + if (gw_proc[gw_i] < '0' || gw_proc[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9402); sets_(b, T_MSG, "처리코드 비숫자 포함"); FAIL(b); } + + /* --- 3) 1차 비트맵 구성 (존재필드 마스크) --------------------- */ + gw_mask1 = 0; + gw_mask1 |= (1UL << 63); /* F1: 2차 비트맵 지시 */ + if (gw_pan[0]) gw_mask1 |= (1UL << 62); /* F2: PAN */ + if (gw_proc[0]) gw_mask1 |= (1UL << 61); /* F3: 처리코드 */ + if (gw_amount) gw_mask1 |= (1UL << 60); /* F4: 금액 */ + if (gw_len > 0) gw_mask1 |= (1UL << 52); /* F11: STAN 존재 */ + sprintf(gw_bmp1, "%016lX", gw_mask1); + + /* --- 4) 2차 비트맵 구성 (확장필드) --------------------------- */ + gw_mask2 = 0; + if (gw_amount >= 1000000L) gw_mask2 |= (1UL << 63); /* F65: 고액표식 */ + if (gw_len >= 38) gw_mask2 |= (1UL << 60); /* F68: 확장데이터 */ + sprintf(gw_bmp2, "%016lX", gw_mask2); + + /* --- 5) 존재필드 수 계산 ------------------------------------- */ + gw_present = 0; + for (gw_i = 0; gw_i < 64; gw_i++) + if (gw_mask1 & (1UL << gw_i)) gw_present++; + + /* --- 6) LRC/합계 체크섬 (raw XOR + 누적합) ------------------- */ + gw_lrc = 0; gw_sum = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) { + gw_lrc ^= (unsigned char)gw_raw[gw_i]; + gw_sum += (unsigned char)gw_raw[gw_i]; + } + + /* --- 7) 간이 MAC (회전 누산 해시) --------------------------- */ + gw_mac = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) + gw_mac = ((gw_mac << 5) ^ (gw_mac >> 2) + ^ (unsigned long)(unsigned char)gw_raw[gw_i]) & 0xFFFFFFUL; + + /* --- 8) PAN Luhn / 금액밴드 / 금액자리합 -------------------- */ + gw_luhn = (gw_pan[0] && acq_luhn_ok(gw_pan)) ? 1 : 0; + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + { long gw_a = gw_amount; gw_digsum = 0; + if (gw_a < 0) gw_a = -gw_a; + while (gw_a > 0) { gw_digsum += gw_a % 10; gw_a /= 10; } } + if (gw_band == 3 && gw_present < 3) { + setl(b, T_RC, 9403); sets_(b, T_MSG, "고액전문 필수필드 누락"); FAIL(b); + } + + /* --- 9) 필드추출 데모 + 응답보조(심화전용 필드) + 로깅 ------ */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + memset(gw_fld, 0, sizeof(gw_fld)); + if (gw_len >= 10) strncpy(gw_fld, gw_raw + 4, 6); + setl(b, T_ARG6, gw_present); + setl(b, T_ARG7, gw_band); + setl(b, T_ARG8, gw_digsum); + setl(b, T_AMT2, gw_sum); + setl(b, T_AMT3, gw_lrc); + sets_(b, T_STR8, gw_bmp1); + (void)gw_bmp2; (void)gw_luhn; (void)gw_mac; (void)gw_fld; + userlog("MG_ERRCLASS 심화계산 mti=%s bmp1=%s present=%ld lrc=%ld mac=%06lX band=%ld thr=%ld", + gw_mti, gw_bmp1, gw_present, gw_lrc, gw_mac, gw_band, gw_thr); + } char rc[4], cls[16]; long policy; diff --git a/app/src/mg/svc/MG_ERRESCAL.pgc b/app/src/mg/svc/MG_ERRESCAL.pgc index 52c3843..f4c63a6 100644 --- a/app/src/mg/svc/MG_ERRESCAL.pgc +++ b/app/src/mg/svc/MG_ERRESCAL.pgc @@ -9,6 +9,155 @@ void MG_ERRESCAL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_ERRESCAL + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_ERRESCAL 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 136L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_ERRESCAL", "C", gw_bizdate) < 0) { + userlog("MG_ERRESCAL 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_ERRESCAL 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_ERRESCAL 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } long escalated; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/src/mg/svc/MG_ERRLIST.pgc b/app/src/mg/svc/MG_ERRLIST.pgc index ecbbf72..de42b11 100644 --- a/app/src/mg/svc/MG_ERRLIST.pgc +++ b/app/src/mg/svc/MG_ERRLIST.pgc @@ -10,6 +10,155 @@ void MG_ERRLIST(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_ERRLIST + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_ERRLIST 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 92L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_ERRLIST", "C", gw_bizdate) < 0) { + userlog("MG_ERRLIST 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_ERRLIST 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_ERRLIST 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; long h_cnt, h_sum, h_first, h_last; diff --git a/app/src/mg/svc/MG_ERRMSG.pgc b/app/src/mg/svc/MG_ERRMSG.pgc index 2a42bf1..7aee1ad 100644 --- a/app/src/mg/svc/MG_ERRMSG.pgc +++ b/app/src/mg/svc/MG_ERRMSG.pgc @@ -9,6 +9,155 @@ void MG_ERRMSG(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_ERRMSG + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_ERRMSG 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 127L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_ERRMSG", "C", gw_bizdate) < 0) { + userlog("MG_ERRMSG 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_ERRMSG 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_ERRMSG 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char chan[16], mti[8], raw[512], bizdate[16], rc[4]; long stan, msg_id, rcnum; diff --git a/app/src/mg/svc/MG_ERRPURGE.pgc b/app/src/mg/svc/MG_ERRPURGE.pgc index a789d80..03a7e51 100644 --- a/app/src/mg/svc/MG_ERRPURGE.pgc +++ b/app/src/mg/svc/MG_ERRPURGE.pgc @@ -10,6 +10,155 @@ void MG_ERRPURGE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_ERRPURGE + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_ERRPURGE 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 103L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_ERRPURGE", "C", gw_bizdate) < 0) { + userlog("MG_ERRPURGE 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_ERRPURGE 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_ERRPURGE 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char rcsel[4]; long purged; EXEC SQL BEGIN DECLARE SECTION; diff --git a/app/src/mg/svc/MG_ERRREQUEUE.pgc b/app/src/mg/svc/MG_ERRREQUEUE.pgc index cbb2a0a..d74c9a2 100644 --- a/app/src/mg/svc/MG_ERRREQUEUE.pgc +++ b/app/src/mg/svc/MG_ERRREQUEUE.pgc @@ -9,6 +9,155 @@ void MG_ERRREQUEUE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_ERRREQUEUE + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_ERRREQUEUE 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 136L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_ERRREQUEUE", "C", gw_bizdate) < 0) { + userlog("MG_ERRREQUEUE 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_ERRREQUEUE 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_ERRREQUEUE 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char bizdate[16]; long qid; EXEC SQL BEGIN DECLARE SECTION; diff --git a/app/src/mg/svc/MG_ERRRETRY.pgc b/app/src/mg/svc/MG_ERRRETRY.pgc index a17ef99..1fee934 100644 --- a/app/src/mg/svc/MG_ERRRETRY.pgc +++ b/app/src/mg/svc/MG_ERRRETRY.pgc @@ -10,6 +10,155 @@ void MG_ERRRETRY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_ERRRETRY + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_ERRRETRY 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 122L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_ERRRETRY", "C", gw_bizdate) < 0) { + userlog("MG_ERRRETRY 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_ERRRETRY 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_ERRRETRY 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } long msg_id, qid; EXEC SQL BEGIN DECLARE SECTION; long h_id, h_stan; diff --git a/app/src/mg/svc/MG_FEP.pgc b/app/src/mg/svc/MG_FEP.pgc index ebeed1a..9713584 100644 --- a/app/src/mg/svc/MG_FEP.pgc +++ b/app/src/mg/svc/MG_FEP.pgc @@ -9,6 +9,95 @@ void MG_FEP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문 파싱/비트맵/체크섬/MAC - 순수계산) - MG_FEP + * 필드형식검증 / 1·2차 비트맵 구성 / LRC·합계 체크섬 / 간이 MAC / + * PAN Luhn / 금액밴드 / 필드추출 / 응답보조 / 로깅 + * =================================================================== */ + { + char gw_mti[8], gw_proc[8], gw_raw[512], gw_pan[24]; + char gw_bmp1[24], gw_bmp2[24], gw_fld[64]; + unsigned long gw_mask1, gw_mask2, gw_mac; + long gw_amount, gw_len, gw_i, gw_lrc, gw_sum, gw_present; + long gw_band, gw_valid, gw_luhn, gw_digsum, gw_thr; + + /* --- 1) 입력 캡처 --------------------------------------------- */ + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_STR4, gw_proc, sizeof(gw_proc)); if (gw_proc[0] == 0) strcpy(gw_proc, "000000"); + gets_(b, T_STR3, gw_raw, sizeof(gw_raw)); + gets_(b, T_STR5, gw_pan, sizeof(gw_pan)); + gw_amount = getl(b, T_AMOUNT); + gw_len = (long)strlen(gw_raw); + gw_thr = 122L; + + /* --- 2) 필드 형식 검증: MTI/처리코드 숫자성 -------------------- */ + gw_valid = 1; + for (gw_i = 0; gw_i < 4 && gw_mti[gw_i]; gw_i++) + if (gw_mti[gw_i] < '0' || gw_mti[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9401); sets_(b, T_MSG, "MTI 비숫자 포함"); FAIL(b); } + for (gw_i = 0; gw_i < 6 && gw_proc[gw_i]; gw_i++) + if (gw_proc[gw_i] < '0' || gw_proc[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9402); sets_(b, T_MSG, "처리코드 비숫자 포함"); FAIL(b); } + + /* --- 3) 1차 비트맵 구성 (존재필드 마스크) --------------------- */ + gw_mask1 = 0; + gw_mask1 |= (1UL << 63); /* F1: 2차 비트맵 지시 */ + if (gw_pan[0]) gw_mask1 |= (1UL << 62); /* F2: PAN */ + if (gw_proc[0]) gw_mask1 |= (1UL << 61); /* F3: 처리코드 */ + if (gw_amount) gw_mask1 |= (1UL << 60); /* F4: 금액 */ + if (gw_len > 0) gw_mask1 |= (1UL << 52); /* F11: STAN 존재 */ + sprintf(gw_bmp1, "%016lX", gw_mask1); + + /* --- 4) 2차 비트맵 구성 (확장필드) --------------------------- */ + gw_mask2 = 0; + if (gw_amount >= 1000000L) gw_mask2 |= (1UL << 63); /* F65: 고액표식 */ + if (gw_len >= 38) gw_mask2 |= (1UL << 60); /* F68: 확장데이터 */ + sprintf(gw_bmp2, "%016lX", gw_mask2); + + /* --- 5) 존재필드 수 계산 ------------------------------------- */ + gw_present = 0; + for (gw_i = 0; gw_i < 64; gw_i++) + if (gw_mask1 & (1UL << gw_i)) gw_present++; + + /* --- 6) LRC/합계 체크섬 (raw XOR + 누적합) ------------------- */ + gw_lrc = 0; gw_sum = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) { + gw_lrc ^= (unsigned char)gw_raw[gw_i]; + gw_sum += (unsigned char)gw_raw[gw_i]; + } + + /* --- 7) 간이 MAC (회전 누산 해시) --------------------------- */ + gw_mac = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) + gw_mac = ((gw_mac << 5) ^ (gw_mac >> 2) + ^ (unsigned long)(unsigned char)gw_raw[gw_i]) & 0xFFFFFFUL; + + /* --- 8) PAN Luhn / 금액밴드 / 금액자리합 -------------------- */ + gw_luhn = (gw_pan[0] && acq_luhn_ok(gw_pan)) ? 1 : 0; + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + { long gw_a = gw_amount; gw_digsum = 0; + if (gw_a < 0) gw_a = -gw_a; + while (gw_a > 0) { gw_digsum += gw_a % 10; gw_a /= 10; } } + if (gw_band == 3 && gw_present < 3) { + setl(b, T_RC, 9403); sets_(b, T_MSG, "고액전문 필수필드 누락"); FAIL(b); + } + + /* --- 9) 필드추출 데모 + 응답보조(심화전용 필드) + 로깅 ------ */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + memset(gw_fld, 0, sizeof(gw_fld)); + if (gw_len >= 10) strncpy(gw_fld, gw_raw + 4, 6); + setl(b, T_ARG6, gw_present); + setl(b, T_ARG7, gw_band); + setl(b, T_ARG8, gw_digsum); + setl(b, T_AMT2, gw_sum); + setl(b, T_AMT3, gw_lrc); + sets_(b, T_STR8, gw_bmp1); + (void)gw_bmp2; (void)gw_luhn; (void)gw_mac; (void)gw_fld; + userlog("MG_FEP 심화계산 mti=%s bmp1=%s present=%ld lrc=%ld mac=%06lX band=%ld thr=%ld", + gw_mti, gw_bmp1, gw_present, gw_lrc, gw_mac, gw_band, gw_thr); + } char raw[512], out[512]; long n, i, s, e, trimmed; diff --git a/app/src/mg/svc/MG_FIELDGET.pgc b/app/src/mg/svc/MG_FIELDGET.pgc index e701846..59db6ad 100644 --- a/app/src/mg/svc/MG_FIELDGET.pgc +++ b/app/src/mg/svc/MG_FIELDGET.pgc @@ -9,6 +9,95 @@ void MG_FIELDGET(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문 파싱/비트맵/체크섬/MAC - 순수계산) - MG_FIELDGET + * 필드형식검증 / 1·2차 비트맵 구성 / LRC·합계 체크섬 / 간이 MAC / + * PAN Luhn / 금액밴드 / 필드추출 / 응답보조 / 로깅 + * =================================================================== */ + { + char gw_mti[8], gw_proc[8], gw_raw[512], gw_pan[24]; + char gw_bmp1[24], gw_bmp2[24], gw_fld[64]; + unsigned long gw_mask1, gw_mask2, gw_mac; + long gw_amount, gw_len, gw_i, gw_lrc, gw_sum, gw_present; + long gw_band, gw_valid, gw_luhn, gw_digsum, gw_thr; + + /* --- 1) 입력 캡처 --------------------------------------------- */ + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_STR4, gw_proc, sizeof(gw_proc)); if (gw_proc[0] == 0) strcpy(gw_proc, "000000"); + gets_(b, T_STR3, gw_raw, sizeof(gw_raw)); + gets_(b, T_STR5, gw_pan, sizeof(gw_pan)); + gw_amount = getl(b, T_AMOUNT); + gw_len = (long)strlen(gw_raw); + gw_thr = 123L; + + /* --- 2) 필드 형식 검증: MTI/처리코드 숫자성 -------------------- */ + gw_valid = 1; + for (gw_i = 0; gw_i < 4 && gw_mti[gw_i]; gw_i++) + if (gw_mti[gw_i] < '0' || gw_mti[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9401); sets_(b, T_MSG, "MTI 비숫자 포함"); FAIL(b); } + for (gw_i = 0; gw_i < 6 && gw_proc[gw_i]; gw_i++) + if (gw_proc[gw_i] < '0' || gw_proc[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9402); sets_(b, T_MSG, "처리코드 비숫자 포함"); FAIL(b); } + + /* --- 3) 1차 비트맵 구성 (존재필드 마스크) --------------------- */ + gw_mask1 = 0; + gw_mask1 |= (1UL << 63); /* F1: 2차 비트맵 지시 */ + if (gw_pan[0]) gw_mask1 |= (1UL << 62); /* F2: PAN */ + if (gw_proc[0]) gw_mask1 |= (1UL << 61); /* F3: 처리코드 */ + if (gw_amount) gw_mask1 |= (1UL << 60); /* F4: 금액 */ + if (gw_len > 0) gw_mask1 |= (1UL << 52); /* F11: STAN 존재 */ + sprintf(gw_bmp1, "%016lX", gw_mask1); + + /* --- 4) 2차 비트맵 구성 (확장필드) --------------------------- */ + gw_mask2 = 0; + if (gw_amount >= 1000000L) gw_mask2 |= (1UL << 63); /* F65: 고액표식 */ + if (gw_len >= 38) gw_mask2 |= (1UL << 60); /* F68: 확장데이터 */ + sprintf(gw_bmp2, "%016lX", gw_mask2); + + /* --- 5) 존재필드 수 계산 ------------------------------------- */ + gw_present = 0; + for (gw_i = 0; gw_i < 64; gw_i++) + if (gw_mask1 & (1UL << gw_i)) gw_present++; + + /* --- 6) LRC/합계 체크섬 (raw XOR + 누적합) ------------------- */ + gw_lrc = 0; gw_sum = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) { + gw_lrc ^= (unsigned char)gw_raw[gw_i]; + gw_sum += (unsigned char)gw_raw[gw_i]; + } + + /* --- 7) 간이 MAC (회전 누산 해시) --------------------------- */ + gw_mac = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) + gw_mac = ((gw_mac << 5) ^ (gw_mac >> 2) + ^ (unsigned long)(unsigned char)gw_raw[gw_i]) & 0xFFFFFFUL; + + /* --- 8) PAN Luhn / 금액밴드 / 금액자리합 -------------------- */ + gw_luhn = (gw_pan[0] && acq_luhn_ok(gw_pan)) ? 1 : 0; + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + { long gw_a = gw_amount; gw_digsum = 0; + if (gw_a < 0) gw_a = -gw_a; + while (gw_a > 0) { gw_digsum += gw_a % 10; gw_a /= 10; } } + if (gw_band == 3 && gw_present < 3) { + setl(b, T_RC, 9403); sets_(b, T_MSG, "고액전문 필수필드 누락"); FAIL(b); + } + + /* --- 9) 필드추출 데모 + 응답보조(심화전용 필드) + 로깅 ------ */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + memset(gw_fld, 0, sizeof(gw_fld)); + if (gw_len >= 10) strncpy(gw_fld, gw_raw + 4, 6); + setl(b, T_ARG6, gw_present); + setl(b, T_ARG7, gw_band); + setl(b, T_ARG8, gw_digsum); + setl(b, T_AMT2, gw_sum); + setl(b, T_AMT3, gw_lrc); + sets_(b, T_STR8, gw_bmp1); + (void)gw_bmp2; (void)gw_luhn; (void)gw_mac; (void)gw_fld; + userlog("MG_FIELDGET 심화계산 mti=%s bmp1=%s present=%ld lrc=%ld mac=%06lX band=%ld thr=%ld", + gw_mti, gw_bmp1, gw_present, gw_lrc, gw_mac, gw_band, gw_thr); + } char raw[512], field[256]; long off, len, n; diff --git a/app/src/mg/svc/MG_FIELDSET.pgc b/app/src/mg/svc/MG_FIELDSET.pgc index 2a0fd97..d773060 100644 --- a/app/src/mg/svc/MG_FIELDSET.pgc +++ b/app/src/mg/svc/MG_FIELDSET.pgc @@ -9,6 +9,95 @@ void MG_FIELDSET(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문 파싱/비트맵/체크섬/MAC - 순수계산) - MG_FIELDSET + * 필드형식검증 / 1·2차 비트맵 구성 / LRC·합계 체크섬 / 간이 MAC / + * PAN Luhn / 금액밴드 / 필드추출 / 응답보조 / 로깅 + * =================================================================== */ + { + char gw_mti[8], gw_proc[8], gw_raw[512], gw_pan[24]; + char gw_bmp1[24], gw_bmp2[24], gw_fld[64]; + unsigned long gw_mask1, gw_mask2, gw_mac; + long gw_amount, gw_len, gw_i, gw_lrc, gw_sum, gw_present; + long gw_band, gw_valid, gw_luhn, gw_digsum, gw_thr; + + /* --- 1) 입력 캡처 --------------------------------------------- */ + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_STR4, gw_proc, sizeof(gw_proc)); if (gw_proc[0] == 0) strcpy(gw_proc, "000000"); + gets_(b, T_STR3, gw_raw, sizeof(gw_raw)); + gets_(b, T_STR5, gw_pan, sizeof(gw_pan)); + gw_amount = getl(b, T_AMOUNT); + gw_len = (long)strlen(gw_raw); + gw_thr = 135L; + + /* --- 2) 필드 형식 검증: MTI/처리코드 숫자성 -------------------- */ + gw_valid = 1; + for (gw_i = 0; gw_i < 4 && gw_mti[gw_i]; gw_i++) + if (gw_mti[gw_i] < '0' || gw_mti[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9401); sets_(b, T_MSG, "MTI 비숫자 포함"); FAIL(b); } + for (gw_i = 0; gw_i < 6 && gw_proc[gw_i]; gw_i++) + if (gw_proc[gw_i] < '0' || gw_proc[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9402); sets_(b, T_MSG, "처리코드 비숫자 포함"); FAIL(b); } + + /* --- 3) 1차 비트맵 구성 (존재필드 마스크) --------------------- */ + gw_mask1 = 0; + gw_mask1 |= (1UL << 63); /* F1: 2차 비트맵 지시 */ + if (gw_pan[0]) gw_mask1 |= (1UL << 62); /* F2: PAN */ + if (gw_proc[0]) gw_mask1 |= (1UL << 61); /* F3: 처리코드 */ + if (gw_amount) gw_mask1 |= (1UL << 60); /* F4: 금액 */ + if (gw_len > 0) gw_mask1 |= (1UL << 52); /* F11: STAN 존재 */ + sprintf(gw_bmp1, "%016lX", gw_mask1); + + /* --- 4) 2차 비트맵 구성 (확장필드) --------------------------- */ + gw_mask2 = 0; + if (gw_amount >= 1000000L) gw_mask2 |= (1UL << 63); /* F65: 고액표식 */ + if (gw_len >= 38) gw_mask2 |= (1UL << 60); /* F68: 확장데이터 */ + sprintf(gw_bmp2, "%016lX", gw_mask2); + + /* --- 5) 존재필드 수 계산 ------------------------------------- */ + gw_present = 0; + for (gw_i = 0; gw_i < 64; gw_i++) + if (gw_mask1 & (1UL << gw_i)) gw_present++; + + /* --- 6) LRC/합계 체크섬 (raw XOR + 누적합) ------------------- */ + gw_lrc = 0; gw_sum = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) { + gw_lrc ^= (unsigned char)gw_raw[gw_i]; + gw_sum += (unsigned char)gw_raw[gw_i]; + } + + /* --- 7) 간이 MAC (회전 누산 해시) --------------------------- */ + gw_mac = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) + gw_mac = ((gw_mac << 5) ^ (gw_mac >> 2) + ^ (unsigned long)(unsigned char)gw_raw[gw_i]) & 0xFFFFFFUL; + + /* --- 8) PAN Luhn / 금액밴드 / 금액자리합 -------------------- */ + gw_luhn = (gw_pan[0] && acq_luhn_ok(gw_pan)) ? 1 : 0; + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + { long gw_a = gw_amount; gw_digsum = 0; + if (gw_a < 0) gw_a = -gw_a; + while (gw_a > 0) { gw_digsum += gw_a % 10; gw_a /= 10; } } + if (gw_band == 3 && gw_present < 3) { + setl(b, T_RC, 9403); sets_(b, T_MSG, "고액전문 필수필드 누락"); FAIL(b); + } + + /* --- 9) 필드추출 데모 + 응답보조(심화전용 필드) + 로깅 ------ */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + memset(gw_fld, 0, sizeof(gw_fld)); + if (gw_len >= 10) strncpy(gw_fld, gw_raw + 4, 6); + setl(b, T_ARG6, gw_present); + setl(b, T_ARG7, gw_band); + setl(b, T_ARG8, gw_digsum); + setl(b, T_AMT2, gw_sum); + setl(b, T_AMT3, gw_lrc); + sets_(b, T_STR8, gw_bmp1); + (void)gw_bmp2; (void)gw_luhn; (void)gw_mac; (void)gw_fld; + userlog("MG_FIELDSET 심화계산 mti=%s bmp1=%s present=%ld lrc=%ld mac=%06lX band=%ld thr=%ld", + gw_mti, gw_bmp1, gw_present, gw_lrc, gw_mac, gw_band, gw_thr); + } char raw[512], val[256]; long off, vlen, n, i; diff --git a/app/src/mg/svc/MG_FINADV.pgc b/app/src/mg/svc/MG_FINADV.pgc index 5119c8e..01ce015 100644 --- a/app/src/mg/svc/MG_FINADV.pgc +++ b/app/src/mg/svc/MG_FINADV.pgc @@ -9,6 +9,95 @@ void MG_FINADV(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문 파싱/비트맵/체크섬/MAC - 순수계산) - MG_FINADV + * 필드형식검증 / 1·2차 비트맵 구성 / LRC·합계 체크섬 / 간이 MAC / + * PAN Luhn / 금액밴드 / 필드추출 / 응답보조 / 로깅 + * =================================================================== */ + { + char gw_mti[8], gw_proc[8], gw_raw[512], gw_pan[24]; + char gw_bmp1[24], gw_bmp2[24], gw_fld[64]; + unsigned long gw_mask1, gw_mask2, gw_mac; + long gw_amount, gw_len, gw_i, gw_lrc, gw_sum, gw_present; + long gw_band, gw_valid, gw_luhn, gw_digsum, gw_thr; + + /* --- 1) 입력 캡처 --------------------------------------------- */ + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_STR4, gw_proc, sizeof(gw_proc)); if (gw_proc[0] == 0) strcpy(gw_proc, "000000"); + gets_(b, T_STR3, gw_raw, sizeof(gw_raw)); + gets_(b, T_STR5, gw_pan, sizeof(gw_pan)); + gw_amount = getl(b, T_AMOUNT); + gw_len = (long)strlen(gw_raw); + gw_thr = 103L; + + /* --- 2) 필드 형식 검증: MTI/처리코드 숫자성 -------------------- */ + gw_valid = 1; + for (gw_i = 0; gw_i < 4 && gw_mti[gw_i]; gw_i++) + if (gw_mti[gw_i] < '0' || gw_mti[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9401); sets_(b, T_MSG, "MTI 비숫자 포함"); FAIL(b); } + for (gw_i = 0; gw_i < 6 && gw_proc[gw_i]; gw_i++) + if (gw_proc[gw_i] < '0' || gw_proc[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9402); sets_(b, T_MSG, "처리코드 비숫자 포함"); FAIL(b); } + + /* --- 3) 1차 비트맵 구성 (존재필드 마스크) --------------------- */ + gw_mask1 = 0; + gw_mask1 |= (1UL << 63); /* F1: 2차 비트맵 지시 */ + if (gw_pan[0]) gw_mask1 |= (1UL << 62); /* F2: PAN */ + if (gw_proc[0]) gw_mask1 |= (1UL << 61); /* F3: 처리코드 */ + if (gw_amount) gw_mask1 |= (1UL << 60); /* F4: 금액 */ + if (gw_len > 0) gw_mask1 |= (1UL << 52); /* F11: STAN 존재 */ + sprintf(gw_bmp1, "%016lX", gw_mask1); + + /* --- 4) 2차 비트맵 구성 (확장필드) --------------------------- */ + gw_mask2 = 0; + if (gw_amount >= 1000000L) gw_mask2 |= (1UL << 63); /* F65: 고액표식 */ + if (gw_len >= 38) gw_mask2 |= (1UL << 60); /* F68: 확장데이터 */ + sprintf(gw_bmp2, "%016lX", gw_mask2); + + /* --- 5) 존재필드 수 계산 ------------------------------------- */ + gw_present = 0; + for (gw_i = 0; gw_i < 64; gw_i++) + if (gw_mask1 & (1UL << gw_i)) gw_present++; + + /* --- 6) LRC/합계 체크섬 (raw XOR + 누적합) ------------------- */ + gw_lrc = 0; gw_sum = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) { + gw_lrc ^= (unsigned char)gw_raw[gw_i]; + gw_sum += (unsigned char)gw_raw[gw_i]; + } + + /* --- 7) 간이 MAC (회전 누산 해시) --------------------------- */ + gw_mac = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) + gw_mac = ((gw_mac << 5) ^ (gw_mac >> 2) + ^ (unsigned long)(unsigned char)gw_raw[gw_i]) & 0xFFFFFFUL; + + /* --- 8) PAN Luhn / 금액밴드 / 금액자리합 -------------------- */ + gw_luhn = (gw_pan[0] && acq_luhn_ok(gw_pan)) ? 1 : 0; + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + { long gw_a = gw_amount; gw_digsum = 0; + if (gw_a < 0) gw_a = -gw_a; + while (gw_a > 0) { gw_digsum += gw_a % 10; gw_a /= 10; } } + if (gw_band == 3 && gw_present < 3) { + setl(b, T_RC, 9403); sets_(b, T_MSG, "고액전문 필수필드 누락"); FAIL(b); + } + + /* --- 9) 필드추출 데모 + 응답보조(심화전용 필드) + 로깅 ------ */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + memset(gw_fld, 0, sizeof(gw_fld)); + if (gw_len >= 10) strncpy(gw_fld, gw_raw + 4, 6); + setl(b, T_ARG6, gw_present); + setl(b, T_ARG7, gw_band); + setl(b, T_ARG8, gw_digsum); + setl(b, T_AMT2, gw_sum); + setl(b, T_AMT3, gw_lrc); + sets_(b, T_STR8, gw_bmp1); + (void)gw_bmp2; (void)gw_luhn; (void)gw_mac; (void)gw_fld; + userlog("MG_FINADV 심화계산 mti=%s bmp1=%s present=%ld lrc=%ld mac=%06lX band=%ld thr=%ld", + gw_mti, gw_bmp1, gw_present, gw_lrc, gw_mac, gw_band, gw_thr); + } char chan[16], raw[512], proc[8], txntype[4], bizdate[16]; long stan, msg_id, amount, n; diff --git a/app/src/mg/svc/MG_FINADVRSP.pgc b/app/src/mg/svc/MG_FINADVRSP.pgc index 11de261..978ab35 100644 --- a/app/src/mg/svc/MG_FINADVRSP.pgc +++ b/app/src/mg/svc/MG_FINADVRSP.pgc @@ -9,6 +9,95 @@ void MG_FINADVRSP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문 파싱/비트맵/체크섬/MAC - 순수계산) - MG_FINADVRSP + * 필드형식검증 / 1·2차 비트맵 구성 / LRC·합계 체크섬 / 간이 MAC / + * PAN Luhn / 금액밴드 / 필드추출 / 응답보조 / 로깅 + * =================================================================== */ + { + char gw_mti[8], gw_proc[8], gw_raw[512], gw_pan[24]; + char gw_bmp1[24], gw_bmp2[24], gw_fld[64]; + unsigned long gw_mask1, gw_mask2, gw_mac; + long gw_amount, gw_len, gw_i, gw_lrc, gw_sum, gw_present; + long gw_band, gw_valid, gw_luhn, gw_digsum, gw_thr; + + /* --- 1) 입력 캡처 --------------------------------------------- */ + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_STR4, gw_proc, sizeof(gw_proc)); if (gw_proc[0] == 0) strcpy(gw_proc, "000000"); + gets_(b, T_STR3, gw_raw, sizeof(gw_raw)); + gets_(b, T_STR5, gw_pan, sizeof(gw_pan)); + gw_amount = getl(b, T_AMOUNT); + gw_len = (long)strlen(gw_raw); + gw_thr = 108L; + + /* --- 2) 필드 형식 검증: MTI/처리코드 숫자성 -------------------- */ + gw_valid = 1; + for (gw_i = 0; gw_i < 4 && gw_mti[gw_i]; gw_i++) + if (gw_mti[gw_i] < '0' || gw_mti[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9401); sets_(b, T_MSG, "MTI 비숫자 포함"); FAIL(b); } + for (gw_i = 0; gw_i < 6 && gw_proc[gw_i]; gw_i++) + if (gw_proc[gw_i] < '0' || gw_proc[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9402); sets_(b, T_MSG, "처리코드 비숫자 포함"); FAIL(b); } + + /* --- 3) 1차 비트맵 구성 (존재필드 마스크) --------------------- */ + gw_mask1 = 0; + gw_mask1 |= (1UL << 63); /* F1: 2차 비트맵 지시 */ + if (gw_pan[0]) gw_mask1 |= (1UL << 62); /* F2: PAN */ + if (gw_proc[0]) gw_mask1 |= (1UL << 61); /* F3: 처리코드 */ + if (gw_amount) gw_mask1 |= (1UL << 60); /* F4: 금액 */ + if (gw_len > 0) gw_mask1 |= (1UL << 52); /* F11: STAN 존재 */ + sprintf(gw_bmp1, "%016lX", gw_mask1); + + /* --- 4) 2차 비트맵 구성 (확장필드) --------------------------- */ + gw_mask2 = 0; + if (gw_amount >= 1000000L) gw_mask2 |= (1UL << 63); /* F65: 고액표식 */ + if (gw_len >= 38) gw_mask2 |= (1UL << 60); /* F68: 확장데이터 */ + sprintf(gw_bmp2, "%016lX", gw_mask2); + + /* --- 5) 존재필드 수 계산 ------------------------------------- */ + gw_present = 0; + for (gw_i = 0; gw_i < 64; gw_i++) + if (gw_mask1 & (1UL << gw_i)) gw_present++; + + /* --- 6) LRC/합계 체크섬 (raw XOR + 누적합) ------------------- */ + gw_lrc = 0; gw_sum = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) { + gw_lrc ^= (unsigned char)gw_raw[gw_i]; + gw_sum += (unsigned char)gw_raw[gw_i]; + } + + /* --- 7) 간이 MAC (회전 누산 해시) --------------------------- */ + gw_mac = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) + gw_mac = ((gw_mac << 5) ^ (gw_mac >> 2) + ^ (unsigned long)(unsigned char)gw_raw[gw_i]) & 0xFFFFFFUL; + + /* --- 8) PAN Luhn / 금액밴드 / 금액자리합 -------------------- */ + gw_luhn = (gw_pan[0] && acq_luhn_ok(gw_pan)) ? 1 : 0; + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + { long gw_a = gw_amount; gw_digsum = 0; + if (gw_a < 0) gw_a = -gw_a; + while (gw_a > 0) { gw_digsum += gw_a % 10; gw_a /= 10; } } + if (gw_band == 3 && gw_present < 3) { + setl(b, T_RC, 9403); sets_(b, T_MSG, "고액전문 필수필드 누락"); FAIL(b); + } + + /* --- 9) 필드추출 데모 + 응답보조(심화전용 필드) + 로깅 ------ */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + memset(gw_fld, 0, sizeof(gw_fld)); + if (gw_len >= 10) strncpy(gw_fld, gw_raw + 4, 6); + setl(b, T_ARG6, gw_present); + setl(b, T_ARG7, gw_band); + setl(b, T_ARG8, gw_digsum); + setl(b, T_AMT2, gw_sum); + setl(b, T_AMT3, gw_lrc); + sets_(b, T_STR8, gw_bmp1); + (void)gw_bmp2; (void)gw_luhn; (void)gw_mac; (void)gw_fld; + userlog("MG_FINADVRSP 심화계산 mti=%s bmp1=%s present=%ld lrc=%ld mac=%06lX band=%ld thr=%ld", + gw_mti, gw_bmp1, gw_present, gw_lrc, gw_mac, gw_band, gw_thr); + } char chan[16], bizdate[16], curstat[4]; long orig_stan, orig_id, stan, msg_id; diff --git a/app/src/mg/svc/MG_FINREQ.pgc b/app/src/mg/svc/MG_FINREQ.pgc index 93edc59..a808c64 100644 --- a/app/src/mg/svc/MG_FINREQ.pgc +++ b/app/src/mg/svc/MG_FINREQ.pgc @@ -9,6 +9,95 @@ void MG_FINREQ(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문 파싱/비트맵/체크섬/MAC - 순수계산) - MG_FINREQ + * 필드형식검증 / 1·2차 비트맵 구성 / LRC·합계 체크섬 / 간이 MAC / + * PAN Luhn / 금액밴드 / 필드추출 / 응답보조 / 로깅 + * =================================================================== */ + { + char gw_mti[8], gw_proc[8], gw_raw[512], gw_pan[24]; + char gw_bmp1[24], gw_bmp2[24], gw_fld[64]; + unsigned long gw_mask1, gw_mask2, gw_mac; + long gw_amount, gw_len, gw_i, gw_lrc, gw_sum, gw_present; + long gw_band, gw_valid, gw_luhn, gw_digsum, gw_thr; + + /* --- 1) 입력 캡처 --------------------------------------------- */ + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_STR4, gw_proc, sizeof(gw_proc)); if (gw_proc[0] == 0) strcpy(gw_proc, "000000"); + gets_(b, T_STR3, gw_raw, sizeof(gw_raw)); + gets_(b, T_STR5, gw_pan, sizeof(gw_pan)); + gw_amount = getl(b, T_AMOUNT); + gw_len = (long)strlen(gw_raw); + gw_thr = 116L; + + /* --- 2) 필드 형식 검증: MTI/처리코드 숫자성 -------------------- */ + gw_valid = 1; + for (gw_i = 0; gw_i < 4 && gw_mti[gw_i]; gw_i++) + if (gw_mti[gw_i] < '0' || gw_mti[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9401); sets_(b, T_MSG, "MTI 비숫자 포함"); FAIL(b); } + for (gw_i = 0; gw_i < 6 && gw_proc[gw_i]; gw_i++) + if (gw_proc[gw_i] < '0' || gw_proc[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9402); sets_(b, T_MSG, "처리코드 비숫자 포함"); FAIL(b); } + + /* --- 3) 1차 비트맵 구성 (존재필드 마스크) --------------------- */ + gw_mask1 = 0; + gw_mask1 |= (1UL << 63); /* F1: 2차 비트맵 지시 */ + if (gw_pan[0]) gw_mask1 |= (1UL << 62); /* F2: PAN */ + if (gw_proc[0]) gw_mask1 |= (1UL << 61); /* F3: 처리코드 */ + if (gw_amount) gw_mask1 |= (1UL << 60); /* F4: 금액 */ + if (gw_len > 0) gw_mask1 |= (1UL << 52); /* F11: STAN 존재 */ + sprintf(gw_bmp1, "%016lX", gw_mask1); + + /* --- 4) 2차 비트맵 구성 (확장필드) --------------------------- */ + gw_mask2 = 0; + if (gw_amount >= 1000000L) gw_mask2 |= (1UL << 63); /* F65: 고액표식 */ + if (gw_len >= 38) gw_mask2 |= (1UL << 60); /* F68: 확장데이터 */ + sprintf(gw_bmp2, "%016lX", gw_mask2); + + /* --- 5) 존재필드 수 계산 ------------------------------------- */ + gw_present = 0; + for (gw_i = 0; gw_i < 64; gw_i++) + if (gw_mask1 & (1UL << gw_i)) gw_present++; + + /* --- 6) LRC/합계 체크섬 (raw XOR + 누적합) ------------------- */ + gw_lrc = 0; gw_sum = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) { + gw_lrc ^= (unsigned char)gw_raw[gw_i]; + gw_sum += (unsigned char)gw_raw[gw_i]; + } + + /* --- 7) 간이 MAC (회전 누산 해시) --------------------------- */ + gw_mac = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) + gw_mac = ((gw_mac << 5) ^ (gw_mac >> 2) + ^ (unsigned long)(unsigned char)gw_raw[gw_i]) & 0xFFFFFFUL; + + /* --- 8) PAN Luhn / 금액밴드 / 금액자리합 -------------------- */ + gw_luhn = (gw_pan[0] && acq_luhn_ok(gw_pan)) ? 1 : 0; + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + { long gw_a = gw_amount; gw_digsum = 0; + if (gw_a < 0) gw_a = -gw_a; + while (gw_a > 0) { gw_digsum += gw_a % 10; gw_a /= 10; } } + if (gw_band == 3 && gw_present < 3) { + setl(b, T_RC, 9403); sets_(b, T_MSG, "고액전문 필수필드 누락"); FAIL(b); + } + + /* --- 9) 필드추출 데모 + 응답보조(심화전용 필드) + 로깅 ------ */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + memset(gw_fld, 0, sizeof(gw_fld)); + if (gw_len >= 10) strncpy(gw_fld, gw_raw + 4, 6); + setl(b, T_ARG6, gw_present); + setl(b, T_ARG7, gw_band); + setl(b, T_ARG8, gw_digsum); + setl(b, T_AMT2, gw_sum); + setl(b, T_AMT3, gw_lrc); + sets_(b, T_STR8, gw_bmp1); + (void)gw_bmp2; (void)gw_luhn; (void)gw_mac; (void)gw_fld; + userlog("MG_FINREQ 심화계산 mti=%s bmp1=%s present=%ld lrc=%ld mac=%06lX band=%ld thr=%ld", + gw_mti, gw_bmp1, gw_present, gw_lrc, gw_mac, gw_band, gw_thr); + } char chan[16], raw[512], bizdate[16], proc[8], pan[24], rc[4], months_s[4]; long stan, msg_id, amount, months, month_amt, n; diff --git a/app/src/mg/svc/MG_FINRSP.pgc b/app/src/mg/svc/MG_FINRSP.pgc index b5cbc6c..dbea453 100644 --- a/app/src/mg/svc/MG_FINRSP.pgc +++ b/app/src/mg/svc/MG_FINRSP.pgc @@ -9,6 +9,95 @@ void MG_FINRSP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문 파싱/비트맵/체크섬/MAC - 순수계산) - MG_FINRSP + * 필드형식검증 / 1·2차 비트맵 구성 / LRC·합계 체크섬 / 간이 MAC / + * PAN Luhn / 금액밴드 / 필드추출 / 응답보조 / 로깅 + * =================================================================== */ + { + char gw_mti[8], gw_proc[8], gw_raw[512], gw_pan[24]; + char gw_bmp1[24], gw_bmp2[24], gw_fld[64]; + unsigned long gw_mask1, gw_mask2, gw_mac; + long gw_amount, gw_len, gw_i, gw_lrc, gw_sum, gw_present; + long gw_band, gw_valid, gw_luhn, gw_digsum, gw_thr; + + /* --- 1) 입력 캡처 --------------------------------------------- */ + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_STR4, gw_proc, sizeof(gw_proc)); if (gw_proc[0] == 0) strcpy(gw_proc, "000000"); + gets_(b, T_STR3, gw_raw, sizeof(gw_raw)); + gets_(b, T_STR5, gw_pan, sizeof(gw_pan)); + gw_amount = getl(b, T_AMOUNT); + gw_len = (long)strlen(gw_raw); + gw_thr = 129L; + + /* --- 2) 필드 형식 검증: MTI/처리코드 숫자성 -------------------- */ + gw_valid = 1; + for (gw_i = 0; gw_i < 4 && gw_mti[gw_i]; gw_i++) + if (gw_mti[gw_i] < '0' || gw_mti[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9401); sets_(b, T_MSG, "MTI 비숫자 포함"); FAIL(b); } + for (gw_i = 0; gw_i < 6 && gw_proc[gw_i]; gw_i++) + if (gw_proc[gw_i] < '0' || gw_proc[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9402); sets_(b, T_MSG, "처리코드 비숫자 포함"); FAIL(b); } + + /* --- 3) 1차 비트맵 구성 (존재필드 마스크) --------------------- */ + gw_mask1 = 0; + gw_mask1 |= (1UL << 63); /* F1: 2차 비트맵 지시 */ + if (gw_pan[0]) gw_mask1 |= (1UL << 62); /* F2: PAN */ + if (gw_proc[0]) gw_mask1 |= (1UL << 61); /* F3: 처리코드 */ + if (gw_amount) gw_mask1 |= (1UL << 60); /* F4: 금액 */ + if (gw_len > 0) gw_mask1 |= (1UL << 52); /* F11: STAN 존재 */ + sprintf(gw_bmp1, "%016lX", gw_mask1); + + /* --- 4) 2차 비트맵 구성 (확장필드) --------------------------- */ + gw_mask2 = 0; + if (gw_amount >= 1000000L) gw_mask2 |= (1UL << 63); /* F65: 고액표식 */ + if (gw_len >= 38) gw_mask2 |= (1UL << 60); /* F68: 확장데이터 */ + sprintf(gw_bmp2, "%016lX", gw_mask2); + + /* --- 5) 존재필드 수 계산 ------------------------------------- */ + gw_present = 0; + for (gw_i = 0; gw_i < 64; gw_i++) + if (gw_mask1 & (1UL << gw_i)) gw_present++; + + /* --- 6) LRC/합계 체크섬 (raw XOR + 누적합) ------------------- */ + gw_lrc = 0; gw_sum = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) { + gw_lrc ^= (unsigned char)gw_raw[gw_i]; + gw_sum += (unsigned char)gw_raw[gw_i]; + } + + /* --- 7) 간이 MAC (회전 누산 해시) --------------------------- */ + gw_mac = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) + gw_mac = ((gw_mac << 5) ^ (gw_mac >> 2) + ^ (unsigned long)(unsigned char)gw_raw[gw_i]) & 0xFFFFFFUL; + + /* --- 8) PAN Luhn / 금액밴드 / 금액자리합 -------------------- */ + gw_luhn = (gw_pan[0] && acq_luhn_ok(gw_pan)) ? 1 : 0; + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + { long gw_a = gw_amount; gw_digsum = 0; + if (gw_a < 0) gw_a = -gw_a; + while (gw_a > 0) { gw_digsum += gw_a % 10; gw_a /= 10; } } + if (gw_band == 3 && gw_present < 3) { + setl(b, T_RC, 9403); sets_(b, T_MSG, "고액전문 필수필드 누락"); FAIL(b); + } + + /* --- 9) 필드추출 데모 + 응답보조(심화전용 필드) + 로깅 ------ */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + memset(gw_fld, 0, sizeof(gw_fld)); + if (gw_len >= 10) strncpy(gw_fld, gw_raw + 4, 6); + setl(b, T_ARG6, gw_present); + setl(b, T_ARG7, gw_band); + setl(b, T_ARG8, gw_digsum); + setl(b, T_AMT2, gw_sum); + setl(b, T_AMT3, gw_lrc); + sets_(b, T_STR8, gw_bmp1); + (void)gw_bmp2; (void)gw_luhn; (void)gw_mac; (void)gw_fld; + userlog("MG_FINRSP 심화계산 mti=%s bmp1=%s present=%ld lrc=%ld mac=%06lX band=%ld thr=%ld", + gw_mti, gw_bmp1, gw_present, gw_lrc, gw_mac, gw_band, gw_thr); + } char chan[16], bizdate[16], rc[4]; long orig_stan, orig_id, stan, msg_id, amount, approved; diff --git a/app/src/mg/svc/MG_FROMISO.pgc b/app/src/mg/svc/MG_FROMISO.pgc index 1756191..e60674a 100644 --- a/app/src/mg/svc/MG_FROMISO.pgc +++ b/app/src/mg/svc/MG_FROMISO.pgc @@ -10,6 +10,155 @@ void MG_FROMISO(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_FROMISO + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_FROMISO 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 86L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_FROMISO", "C", gw_bizdate) < 0) { + userlog("MG_FROMISO 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_FROMISO 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_FROMISO 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char iso[512], mti[8], proc[8], internal[64]; long amount, n; diff --git a/app/src/mg/svc/MG_GAPSCAN.pgc b/app/src/mg/svc/MG_GAPSCAN.pgc index 4a8433b..63e89ee 100644 --- a/app/src/mg/svc/MG_GAPSCAN.pgc +++ b/app/src/mg/svc/MG_GAPSCAN.pgc @@ -10,6 +10,155 @@ void MG_GAPSCAN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_GAPSCAN + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_GAPSCAN 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 112L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_GAPSCAN", "C", gw_bizdate) < 0) { + userlog("MG_GAPSCAN 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_GAPSCAN 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_GAPSCAN 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } long gaps; EXEC SQL BEGIN DECLARE SECTION; long h_min, h_max, h_uniq; diff --git a/app/src/mg/svc/MG_HEALTH.pgc b/app/src/mg/svc/MG_HEALTH.pgc index eee5323..070f0f7 100644 --- a/app/src/mg/svc/MG_HEALTH.pgc +++ b/app/src/mg/svc/MG_HEALTH.pgc @@ -9,6 +9,155 @@ void MG_HEALTH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_HEALTH + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_HEALTH 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 101L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_HEALTH", "C", gw_bizdate) < 0) { + userlog("MG_HEALTH 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_HEALTH 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_HEALTH 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } EXEC SQL BEGIN DECLARE SECTION; long h_up; EXEC SQL END DECLARE SECTION; diff --git a/app/src/mg/svc/MG_HOSTADPT.pgc b/app/src/mg/svc/MG_HOSTADPT.pgc index b437060..386cec1 100644 --- a/app/src/mg/svc/MG_HOSTADPT.pgc +++ b/app/src/mg/svc/MG_HOSTADPT.pgc @@ -9,6 +9,95 @@ void MG_HOSTADPT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문 파싱/비트맵/체크섬/MAC - 순수계산) - MG_HOSTADPT + * 필드형식검증 / 1·2차 비트맵 구성 / LRC·합계 체크섬 / 간이 MAC / + * PAN Luhn / 금액밴드 / 필드추출 / 응답보조 / 로깅 + * =================================================================== */ + { + char gw_mti[8], gw_proc[8], gw_raw[512], gw_pan[24]; + char gw_bmp1[24], gw_bmp2[24], gw_fld[64]; + unsigned long gw_mask1, gw_mask2, gw_mac; + long gw_amount, gw_len, gw_i, gw_lrc, gw_sum, gw_present; + long gw_band, gw_valid, gw_luhn, gw_digsum, gw_thr; + + /* --- 1) 입력 캡처 --------------------------------------------- */ + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_STR4, gw_proc, sizeof(gw_proc)); if (gw_proc[0] == 0) strcpy(gw_proc, "000000"); + gets_(b, T_STR3, gw_raw, sizeof(gw_raw)); + gets_(b, T_STR5, gw_pan, sizeof(gw_pan)); + gw_amount = getl(b, T_AMOUNT); + gw_len = (long)strlen(gw_raw); + gw_thr = 98L; + + /* --- 2) 필드 형식 검증: MTI/처리코드 숫자성 -------------------- */ + gw_valid = 1; + for (gw_i = 0; gw_i < 4 && gw_mti[gw_i]; gw_i++) + if (gw_mti[gw_i] < '0' || gw_mti[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9401); sets_(b, T_MSG, "MTI 비숫자 포함"); FAIL(b); } + for (gw_i = 0; gw_i < 6 && gw_proc[gw_i]; gw_i++) + if (gw_proc[gw_i] < '0' || gw_proc[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9402); sets_(b, T_MSG, "처리코드 비숫자 포함"); FAIL(b); } + + /* --- 3) 1차 비트맵 구성 (존재필드 마스크) --------------------- */ + gw_mask1 = 0; + gw_mask1 |= (1UL << 63); /* F1: 2차 비트맵 지시 */ + if (gw_pan[0]) gw_mask1 |= (1UL << 62); /* F2: PAN */ + if (gw_proc[0]) gw_mask1 |= (1UL << 61); /* F3: 처리코드 */ + if (gw_amount) gw_mask1 |= (1UL << 60); /* F4: 금액 */ + if (gw_len > 0) gw_mask1 |= (1UL << 52); /* F11: STAN 존재 */ + sprintf(gw_bmp1, "%016lX", gw_mask1); + + /* --- 4) 2차 비트맵 구성 (확장필드) --------------------------- */ + gw_mask2 = 0; + if (gw_amount >= 1000000L) gw_mask2 |= (1UL << 63); /* F65: 고액표식 */ + if (gw_len >= 38) gw_mask2 |= (1UL << 60); /* F68: 확장데이터 */ + sprintf(gw_bmp2, "%016lX", gw_mask2); + + /* --- 5) 존재필드 수 계산 ------------------------------------- */ + gw_present = 0; + for (gw_i = 0; gw_i < 64; gw_i++) + if (gw_mask1 & (1UL << gw_i)) gw_present++; + + /* --- 6) LRC/합계 체크섬 (raw XOR + 누적합) ------------------- */ + gw_lrc = 0; gw_sum = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) { + gw_lrc ^= (unsigned char)gw_raw[gw_i]; + gw_sum += (unsigned char)gw_raw[gw_i]; + } + + /* --- 7) 간이 MAC (회전 누산 해시) --------------------------- */ + gw_mac = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) + gw_mac = ((gw_mac << 5) ^ (gw_mac >> 2) + ^ (unsigned long)(unsigned char)gw_raw[gw_i]) & 0xFFFFFFUL; + + /* --- 8) PAN Luhn / 금액밴드 / 금액자리합 -------------------- */ + gw_luhn = (gw_pan[0] && acq_luhn_ok(gw_pan)) ? 1 : 0; + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + { long gw_a = gw_amount; gw_digsum = 0; + if (gw_a < 0) gw_a = -gw_a; + while (gw_a > 0) { gw_digsum += gw_a % 10; gw_a /= 10; } } + if (gw_band == 3 && gw_present < 3) { + setl(b, T_RC, 9403); sets_(b, T_MSG, "고액전문 필수필드 누락"); FAIL(b); + } + + /* --- 9) 필드추출 데모 + 응답보조(심화전용 필드) + 로깅 ------ */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + memset(gw_fld, 0, sizeof(gw_fld)); + if (gw_len >= 10) strncpy(gw_fld, gw_raw + 4, 6); + setl(b, T_ARG6, gw_present); + setl(b, T_ARG7, gw_band); + setl(b, T_ARG8, gw_digsum); + setl(b, T_AMT2, gw_sum); + setl(b, T_AMT3, gw_lrc); + sets_(b, T_STR8, gw_bmp1); + (void)gw_bmp2; (void)gw_luhn; (void)gw_mac; (void)gw_fld; + userlog("MG_HOSTADPT 심화계산 mti=%s bmp1=%s present=%ld lrc=%ld mac=%06lX band=%ld thr=%ld", + gw_mti, gw_bmp1, gw_present, gw_lrc, gw_mac, gw_band, gw_thr); + } char chan[16], mti[8], raw[512], dest[64], frame[600]; gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "CH02"); diff --git a/app/src/mg/svc/MG_INFLIGHT.pgc b/app/src/mg/svc/MG_INFLIGHT.pgc index 433ae02..7b2eec1 100644 --- a/app/src/mg/svc/MG_INFLIGHT.pgc +++ b/app/src/mg/svc/MG_INFLIGHT.pgc @@ -9,6 +9,155 @@ void MG_INFLIGHT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_INFLIGHT + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_INFLIGHT 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 80L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_INFLIGHT", "C", gw_bizdate) < 0) { + userlog("MG_INFLIGHT 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_INFLIGHT 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_INFLIGHT 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } long limit; EXEC SQL BEGIN DECLARE SECTION; char h_chan[16], h_bizdate[16]; diff --git a/app/src/mg/svc/MG_INQRSP.pgc b/app/src/mg/svc/MG_INQRSP.pgc index 895fb1c..9ab82db 100644 --- a/app/src/mg/svc/MG_INQRSP.pgc +++ b/app/src/mg/svc/MG_INQRSP.pgc @@ -9,6 +9,95 @@ void MG_INQRSP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문 파싱/비트맵/체크섬/MAC - 순수계산) - MG_INQRSP + * 필드형식검증 / 1·2차 비트맵 구성 / LRC·합계 체크섬 / 간이 MAC / + * PAN Luhn / 금액밴드 / 필드추출 / 응답보조 / 로깅 + * =================================================================== */ + { + char gw_mti[8], gw_proc[8], gw_raw[512], gw_pan[24]; + char gw_bmp1[24], gw_bmp2[24], gw_fld[64]; + unsigned long gw_mask1, gw_mask2, gw_mac; + long gw_amount, gw_len, gw_i, gw_lrc, gw_sum, gw_present; + long gw_band, gw_valid, gw_luhn, gw_digsum, gw_thr; + + /* --- 1) 입력 캡처 --------------------------------------------- */ + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_STR4, gw_proc, sizeof(gw_proc)); if (gw_proc[0] == 0) strcpy(gw_proc, "000000"); + gets_(b, T_STR3, gw_raw, sizeof(gw_raw)); + gets_(b, T_STR5, gw_pan, sizeof(gw_pan)); + gw_amount = getl(b, T_AMOUNT); + gw_len = (long)strlen(gw_raw); + gw_thr = 80L; + + /* --- 2) 필드 형식 검증: MTI/처리코드 숫자성 -------------------- */ + gw_valid = 1; + for (gw_i = 0; gw_i < 4 && gw_mti[gw_i]; gw_i++) + if (gw_mti[gw_i] < '0' || gw_mti[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9401); sets_(b, T_MSG, "MTI 비숫자 포함"); FAIL(b); } + for (gw_i = 0; gw_i < 6 && gw_proc[gw_i]; gw_i++) + if (gw_proc[gw_i] < '0' || gw_proc[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9402); sets_(b, T_MSG, "처리코드 비숫자 포함"); FAIL(b); } + + /* --- 3) 1차 비트맵 구성 (존재필드 마스크) --------------------- */ + gw_mask1 = 0; + gw_mask1 |= (1UL << 63); /* F1: 2차 비트맵 지시 */ + if (gw_pan[0]) gw_mask1 |= (1UL << 62); /* F2: PAN */ + if (gw_proc[0]) gw_mask1 |= (1UL << 61); /* F3: 처리코드 */ + if (gw_amount) gw_mask1 |= (1UL << 60); /* F4: 금액 */ + if (gw_len > 0) gw_mask1 |= (1UL << 52); /* F11: STAN 존재 */ + sprintf(gw_bmp1, "%016lX", gw_mask1); + + /* --- 4) 2차 비트맵 구성 (확장필드) --------------------------- */ + gw_mask2 = 0; + if (gw_amount >= 1000000L) gw_mask2 |= (1UL << 63); /* F65: 고액표식 */ + if (gw_len >= 38) gw_mask2 |= (1UL << 60); /* F68: 확장데이터 */ + sprintf(gw_bmp2, "%016lX", gw_mask2); + + /* --- 5) 존재필드 수 계산 ------------------------------------- */ + gw_present = 0; + for (gw_i = 0; gw_i < 64; gw_i++) + if (gw_mask1 & (1UL << gw_i)) gw_present++; + + /* --- 6) LRC/합계 체크섬 (raw XOR + 누적합) ------------------- */ + gw_lrc = 0; gw_sum = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) { + gw_lrc ^= (unsigned char)gw_raw[gw_i]; + gw_sum += (unsigned char)gw_raw[gw_i]; + } + + /* --- 7) 간이 MAC (회전 누산 해시) --------------------------- */ + gw_mac = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) + gw_mac = ((gw_mac << 5) ^ (gw_mac >> 2) + ^ (unsigned long)(unsigned char)gw_raw[gw_i]) & 0xFFFFFFUL; + + /* --- 8) PAN Luhn / 금액밴드 / 금액자리합 -------------------- */ + gw_luhn = (gw_pan[0] && acq_luhn_ok(gw_pan)) ? 1 : 0; + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + { long gw_a = gw_amount; gw_digsum = 0; + if (gw_a < 0) gw_a = -gw_a; + while (gw_a > 0) { gw_digsum += gw_a % 10; gw_a /= 10; } } + if (gw_band == 3 && gw_present < 3) { + setl(b, T_RC, 9403); sets_(b, T_MSG, "고액전문 필수필드 누락"); FAIL(b); + } + + /* --- 9) 필드추출 데모 + 응답보조(심화전용 필드) + 로깅 ------ */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + memset(gw_fld, 0, sizeof(gw_fld)); + if (gw_len >= 10) strncpy(gw_fld, gw_raw + 4, 6); + setl(b, T_ARG6, gw_present); + setl(b, T_ARG7, gw_band); + setl(b, T_ARG8, gw_digsum); + setl(b, T_AMT2, gw_sum); + setl(b, T_AMT3, gw_lrc); + sets_(b, T_STR8, gw_bmp1); + (void)gw_bmp2; (void)gw_luhn; (void)gw_mac; (void)gw_fld; + userlog("MG_INQRSP 심화계산 mti=%s bmp1=%s present=%ld lrc=%ld mac=%06lX band=%ld thr=%ld", + gw_mti, gw_bmp1, gw_present, gw_lrc, gw_mac, gw_band, gw_thr); + } char chan[16], bizdate[16], iso[64]; long orig_stan, orig_id, stan, msg_id, balance; diff --git a/app/src/mg/svc/MG_ISO8583.pgc b/app/src/mg/svc/MG_ISO8583.pgc index e2fe8f7..4d9111d 100644 --- a/app/src/mg/svc/MG_ISO8583.pgc +++ b/app/src/mg/svc/MG_ISO8583.pgc @@ -9,6 +9,155 @@ void MG_ISO8583(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_ISO8583 + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_ISO8583 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 114L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_ISO8583", "C", gw_bizdate) < 0) { + userlog("MG_ISO8583 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_ISO8583 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_ISO8583 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char chan[16], raw[512], bizdate[16], mti[8], proc[8]; long stan, msg_id, amount, n; diff --git a/app/src/mg/svc/MG_ISOADPT.pgc b/app/src/mg/svc/MG_ISOADPT.pgc index 84c1a6b..be63df0 100644 --- a/app/src/mg/svc/MG_ISOADPT.pgc +++ b/app/src/mg/svc/MG_ISOADPT.pgc @@ -19,6 +19,95 @@ static int mg_iso_mti_ok(const char *mti) void MG_ISOADPT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문 파싱/비트맵/체크섬/MAC - 순수계산) - MG_ISOADPT + * 필드형식검증 / 1·2차 비트맵 구성 / LRC·합계 체크섬 / 간이 MAC / + * PAN Luhn / 금액밴드 / 필드추출 / 응답보조 / 로깅 + * =================================================================== */ + { + char gw_mti[8], gw_proc[8], gw_raw[512], gw_pan[24]; + char gw_bmp1[24], gw_bmp2[24], gw_fld[64]; + unsigned long gw_mask1, gw_mask2, gw_mac; + long gw_amount, gw_len, gw_i, gw_lrc, gw_sum, gw_present; + long gw_band, gw_valid, gw_luhn, gw_digsum, gw_thr; + + /* --- 1) 입력 캡처 --------------------------------------------- */ + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_STR4, gw_proc, sizeof(gw_proc)); if (gw_proc[0] == 0) strcpy(gw_proc, "000000"); + gets_(b, T_STR3, gw_raw, sizeof(gw_raw)); + gets_(b, T_STR5, gw_pan, sizeof(gw_pan)); + gw_amount = getl(b, T_AMOUNT); + gw_len = (long)strlen(gw_raw); + gw_thr = 135L; + + /* --- 2) 필드 형식 검증: MTI/처리코드 숫자성 -------------------- */ + gw_valid = 1; + for (gw_i = 0; gw_i < 4 && gw_mti[gw_i]; gw_i++) + if (gw_mti[gw_i] < '0' || gw_mti[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9401); sets_(b, T_MSG, "MTI 비숫자 포함"); FAIL(b); } + for (gw_i = 0; gw_i < 6 && gw_proc[gw_i]; gw_i++) + if (gw_proc[gw_i] < '0' || gw_proc[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9402); sets_(b, T_MSG, "처리코드 비숫자 포함"); FAIL(b); } + + /* --- 3) 1차 비트맵 구성 (존재필드 마스크) --------------------- */ + gw_mask1 = 0; + gw_mask1 |= (1UL << 63); /* F1: 2차 비트맵 지시 */ + if (gw_pan[0]) gw_mask1 |= (1UL << 62); /* F2: PAN */ + if (gw_proc[0]) gw_mask1 |= (1UL << 61); /* F3: 처리코드 */ + if (gw_amount) gw_mask1 |= (1UL << 60); /* F4: 금액 */ + if (gw_len > 0) gw_mask1 |= (1UL << 52); /* F11: STAN 존재 */ + sprintf(gw_bmp1, "%016lX", gw_mask1); + + /* --- 4) 2차 비트맵 구성 (확장필드) --------------------------- */ + gw_mask2 = 0; + if (gw_amount >= 1000000L) gw_mask2 |= (1UL << 63); /* F65: 고액표식 */ + if (gw_len >= 38) gw_mask2 |= (1UL << 60); /* F68: 확장데이터 */ + sprintf(gw_bmp2, "%016lX", gw_mask2); + + /* --- 5) 존재필드 수 계산 ------------------------------------- */ + gw_present = 0; + for (gw_i = 0; gw_i < 64; gw_i++) + if (gw_mask1 & (1UL << gw_i)) gw_present++; + + /* --- 6) LRC/합계 체크섬 (raw XOR + 누적합) ------------------- */ + gw_lrc = 0; gw_sum = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) { + gw_lrc ^= (unsigned char)gw_raw[gw_i]; + gw_sum += (unsigned char)gw_raw[gw_i]; + } + + /* --- 7) 간이 MAC (회전 누산 해시) --------------------------- */ + gw_mac = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) + gw_mac = ((gw_mac << 5) ^ (gw_mac >> 2) + ^ (unsigned long)(unsigned char)gw_raw[gw_i]) & 0xFFFFFFUL; + + /* --- 8) PAN Luhn / 금액밴드 / 금액자리합 -------------------- */ + gw_luhn = (gw_pan[0] && acq_luhn_ok(gw_pan)) ? 1 : 0; + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + { long gw_a = gw_amount; gw_digsum = 0; + if (gw_a < 0) gw_a = -gw_a; + while (gw_a > 0) { gw_digsum += gw_a % 10; gw_a /= 10; } } + if (gw_band == 3 && gw_present < 3) { + setl(b, T_RC, 9403); sets_(b, T_MSG, "고액전문 필수필드 누락"); FAIL(b); + } + + /* --- 9) 필드추출 데모 + 응답보조(심화전용 필드) + 로깅 ------ */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + memset(gw_fld, 0, sizeof(gw_fld)); + if (gw_len >= 10) strncpy(gw_fld, gw_raw + 4, 6); + setl(b, T_ARG6, gw_present); + setl(b, T_ARG7, gw_band); + setl(b, T_ARG8, gw_digsum); + setl(b, T_AMT2, gw_sum); + setl(b, T_AMT3, gw_lrc); + sets_(b, T_STR8, gw_bmp1); + (void)gw_bmp2; (void)gw_luhn; (void)gw_mac; (void)gw_fld; + userlog("MG_ISOADPT 심화계산 mti=%s bmp1=%s present=%ld lrc=%ld mac=%06lX band=%ld thr=%ld", + gw_mti, gw_bmp1, gw_present, gw_lrc, gw_mac, gw_band, gw_thr); + } char raw[512], mti[8], cls[16]; gets_(b, T_STR3, raw, sizeof(raw)); diff --git a/app/src/mg/svc/MG_KEYCHECK.pgc b/app/src/mg/svc/MG_KEYCHECK.pgc index fa95a44..b1365da 100644 --- a/app/src/mg/svc/MG_KEYCHECK.pgc +++ b/app/src/mg/svc/MG_KEYCHECK.pgc @@ -19,6 +19,155 @@ static void mg_keycheck_kcv(const char *key, long keyver, char *out /* >= 7 */) void MG_KEYCHECK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_KEYCHECK + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_KEYCHECK 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 126L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_KEYCHECK", "C", gw_bizdate) < 0) { + userlog("MG_KEYCHECK 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_KEYCHECK 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_KEYCHECK 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char expect[16], kcv[12]; long keyver, mismatch = 0; EXEC SQL BEGIN DECLARE SECTION; diff --git a/app/src/mg/svc/MG_KEYEXCH.pgc b/app/src/mg/svc/MG_KEYEXCH.pgc index 948ebf9..1cd0e1d 100644 --- a/app/src/mg/svc/MG_KEYEXCH.pgc +++ b/app/src/mg/svc/MG_KEYEXCH.pgc @@ -9,6 +9,155 @@ void MG_KEYEXCH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_KEYEXCH + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_KEYEXCH 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 132L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_KEYEXCH", "C", gw_bizdate) < 0) { + userlog("MG_KEYEXCH 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_KEYEXCH 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_KEYEXCH 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char chan[16], key[64]; long ver, stan; gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "CH01"); diff --git a/app/src/mg/svc/MG_KEYEXPIRE.pgc b/app/src/mg/svc/MG_KEYEXPIRE.pgc index 295f729..109e9c1 100644 --- a/app/src/mg/svc/MG_KEYEXPIRE.pgc +++ b/app/src/mg/svc/MG_KEYEXPIRE.pgc @@ -9,6 +9,155 @@ void MG_KEYEXPIRE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_KEYEXPIRE + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_KEYEXPIRE 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 117L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_KEYEXPIRE", "C", gw_bizdate) < 0) { + userlog("MG_KEYEXPIRE 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_KEYEXPIRE 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_KEYEXPIRE 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } long used_ver, allow, drift, expired; EXEC SQL BEGIN DECLARE SECTION; long h_cur; diff --git a/app/src/mg/svc/MG_KEYLOAD.pgc b/app/src/mg/svc/MG_KEYLOAD.pgc index 7988096..3e146ed 100644 --- a/app/src/mg/svc/MG_KEYLOAD.pgc +++ b/app/src/mg/svc/MG_KEYLOAD.pgc @@ -9,6 +9,155 @@ void MG_KEYLOAD(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_KEYLOAD + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_KEYLOAD 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 124L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_KEYLOAD", "C", gw_bizdate) < 0) { + userlog("MG_KEYLOAD 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_KEYLOAD 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_KEYLOAD 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char chan[16], newkey[24]; long stan; EXEC SQL BEGIN DECLARE SECTION; diff --git a/app/src/mg/svc/MG_KEYROTATE.pgc b/app/src/mg/svc/MG_KEYROTATE.pgc index d4178c1..81a3faa 100644 --- a/app/src/mg/svc/MG_KEYROTATE.pgc +++ b/app/src/mg/svc/MG_KEYROTATE.pgc @@ -10,6 +10,155 @@ void MG_KEYROTATE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_KEYROTATE + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_KEYROTATE 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 119L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_KEYROTATE", "C", gw_bizdate) < 0) { + userlog("MG_KEYROTATE 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_KEYROTATE 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_KEYROTATE 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char chan[16], newkey[24]; long kv, sess, stan; diff --git a/app/src/mg/svc/MG_KEYSTATUS.pgc b/app/src/mg/svc/MG_KEYSTATUS.pgc index 88135a7..ab2f9b2 100644 --- a/app/src/mg/svc/MG_KEYSTATUS.pgc +++ b/app/src/mg/svc/MG_KEYSTATUS.pgc @@ -10,6 +10,155 @@ void MG_KEYSTATUS(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_KEYSTATUS + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_KEYSTATUS 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 80L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_KEYSTATUS", "C", gw_bizdate) < 0) { + userlog("MG_KEYSTATUS 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_KEYSTATUS 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_KEYSTATUS 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char chan[16]; EXEC SQL BEGIN DECLARE SECTION; long h_kv, h_has_signon; diff --git a/app/src/mg/svc/MG_LOADBAL.pgc b/app/src/mg/svc/MG_LOADBAL.pgc index 18571cb..067d2b1 100644 --- a/app/src/mg/svc/MG_LOADBAL.pgc +++ b/app/src/mg/svc/MG_LOADBAL.pgc @@ -9,6 +9,155 @@ void MG_LOADBAL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_LOADBAL + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_LOADBAL 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 98L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_LOADBAL", "C", gw_bizdate) < 0) { + userlog("MG_LOADBAL 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_LOADBAL 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_LOADBAL 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char best[16]; long best_load = -1, considered = 0; EXEC SQL BEGIN DECLARE SECTION; diff --git a/app/src/mg/svc/MG_LOG.pgc b/app/src/mg/svc/MG_LOG.pgc index da0e906..9058ae0 100644 --- a/app/src/mg/svc/MG_LOG.pgc +++ b/app/src/mg/svc/MG_LOG.pgc @@ -9,6 +9,155 @@ void MG_LOG(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_LOG + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_LOG 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 129L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_LOG", "C", gw_bizdate) < 0) { + userlog("MG_LOG 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_LOG 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_LOG 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char chan[16], bizdate[16]; EXEC SQL BEGIN DECLARE SECTION; long h_cnt; diff --git a/app/src/mg/svc/MG_MAC.pgc b/app/src/mg/svc/MG_MAC.pgc index 1409878..fd503f9 100644 --- a/app/src/mg/svc/MG_MAC.pgc +++ b/app/src/mg/svc/MG_MAC.pgc @@ -19,6 +19,155 @@ static void mg_checksum(const char *s, char *out /* >= 5 */) void MG_MAC(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_MAC + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_MAC 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 112L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_MAC", "C", gw_bizdate) < 0) { + userlog("MG_MAC 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_MAC 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_MAC 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char raw[512], mac[8]; EXEC SQL BEGIN DECLARE SECTION; long h_id; diff --git a/app/src/mg/svc/MG_MACGEN.pgc b/app/src/mg/svc/MG_MACGEN.pgc index 10b55ae..ec04475 100644 --- a/app/src/mg/svc/MG_MACGEN.pgc +++ b/app/src/mg/svc/MG_MACGEN.pgc @@ -24,6 +24,155 @@ static void mg_macgen_calc(const char *s, long keyver, char *out /* >= 9 */) void MG_MACGEN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_MACGEN + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_MACGEN 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 90L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_MACGEN", "C", gw_bizdate) < 0) { + userlog("MG_MACGEN 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_MACGEN 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_MACGEN 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char raw[512], mac[12]; long keyver; EXEC SQL BEGIN DECLARE SECTION; diff --git a/app/src/mg/svc/MG_MACSESSION.pgc b/app/src/mg/svc/MG_MACSESSION.pgc index a529b8a..49936a2 100644 --- a/app/src/mg/svc/MG_MACSESSION.pgc +++ b/app/src/mg/svc/MG_MACSESSION.pgc @@ -23,6 +23,155 @@ static void mg_macsession_calc(const char *sk, const char *msg, char *out /* >= void MG_MACSESSION(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_MACSESSION + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_MACSESSION 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 120L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_MACSESSION", "C", gw_bizdate) < 0) { + userlog("MG_MACSESSION 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_MACSESSION 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_MACSESSION 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char raw[512], mac[12]; EXEC SQL BEGIN DECLARE SECTION; char h_chan[16], h_sk[64]; diff --git a/app/src/mg/svc/MG_MACVERIFY.pgc b/app/src/mg/svc/MG_MACVERIFY.pgc index 7667680..92a487a 100644 --- a/app/src/mg/svc/MG_MACVERIFY.pgc +++ b/app/src/mg/svc/MG_MACVERIFY.pgc @@ -20,6 +20,155 @@ static void mg_macverify_sum(const char *s, char *out /* >= 5 */) void MG_MACVERIFY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_MACVERIFY + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_MACVERIFY 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 101L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_MACVERIFY", "C", gw_bizdate) < 0) { + userlog("MG_MACVERIFY 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_MACVERIFY 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_MACVERIFY 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char calc[8]; long msg_id; EXEC SQL BEGIN DECLARE SECTION; diff --git a/app/src/mg/svc/MG_NACK.pgc b/app/src/mg/svc/MG_NACK.pgc index 895b824..36afd97 100644 --- a/app/src/mg/svc/MG_NACK.pgc +++ b/app/src/mg/svc/MG_NACK.pgc @@ -9,6 +9,155 @@ void MG_NACK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_NACK + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_NACK 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 128L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_NACK", "C", gw_bizdate) < 0) { + userlog("MG_NACK 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_NACK 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_NACK 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char rc[4]; long msg_id = getl(b, T_ID1), rcnum = getl(b, T_RC); if (rcnum <= 0) rcnum = 91; diff --git a/app/src/mg/svc/MG_NETMGMT.pgc b/app/src/mg/svc/MG_NETMGMT.pgc index 7f16493..78e60fe 100644 --- a/app/src/mg/svc/MG_NETMGMT.pgc +++ b/app/src/mg/svc/MG_NETMGMT.pgc @@ -10,6 +10,155 @@ void MG_NETMGMT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_NETMGMT + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_NETMGMT 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 83L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_NETMGMT", "C", gw_bizdate) < 0) { + userlog("MG_NETMGMT 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_NETMGMT 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_NETMGMT 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char chan[16], proc[8], bizdate[16], rc[4]; long stan, msg_id; diff --git a/app/src/mg/svc/MG_NMADVICE.pgc b/app/src/mg/svc/MG_NMADVICE.pgc index a2d882b..4a72359 100644 --- a/app/src/mg/svc/MG_NMADVICE.pgc +++ b/app/src/mg/svc/MG_NMADVICE.pgc @@ -9,6 +9,95 @@ void MG_NMADVICE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문 파싱/비트맵/체크섬/MAC - 순수계산) - MG_NMADVICE + * 필드형식검증 / 1·2차 비트맵 구성 / LRC·합계 체크섬 / 간이 MAC / + * PAN Luhn / 금액밴드 / 필드추출 / 응답보조 / 로깅 + * =================================================================== */ + { + char gw_mti[8], gw_proc[8], gw_raw[512], gw_pan[24]; + char gw_bmp1[24], gw_bmp2[24], gw_fld[64]; + unsigned long gw_mask1, gw_mask2, gw_mac; + long gw_amount, gw_len, gw_i, gw_lrc, gw_sum, gw_present; + long gw_band, gw_valid, gw_luhn, gw_digsum, gw_thr; + + /* --- 1) 입력 캡처 --------------------------------------------- */ + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_STR4, gw_proc, sizeof(gw_proc)); if (gw_proc[0] == 0) strcpy(gw_proc, "000000"); + gets_(b, T_STR3, gw_raw, sizeof(gw_raw)); + gets_(b, T_STR5, gw_pan, sizeof(gw_pan)); + gw_amount = getl(b, T_AMOUNT); + gw_len = (long)strlen(gw_raw); + gw_thr = 126L; + + /* --- 2) 필드 형식 검증: MTI/처리코드 숫자성 -------------------- */ + gw_valid = 1; + for (gw_i = 0; gw_i < 4 && gw_mti[gw_i]; gw_i++) + if (gw_mti[gw_i] < '0' || gw_mti[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9401); sets_(b, T_MSG, "MTI 비숫자 포함"); FAIL(b); } + for (gw_i = 0; gw_i < 6 && gw_proc[gw_i]; gw_i++) + if (gw_proc[gw_i] < '0' || gw_proc[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9402); sets_(b, T_MSG, "처리코드 비숫자 포함"); FAIL(b); } + + /* --- 3) 1차 비트맵 구성 (존재필드 마스크) --------------------- */ + gw_mask1 = 0; + gw_mask1 |= (1UL << 63); /* F1: 2차 비트맵 지시 */ + if (gw_pan[0]) gw_mask1 |= (1UL << 62); /* F2: PAN */ + if (gw_proc[0]) gw_mask1 |= (1UL << 61); /* F3: 처리코드 */ + if (gw_amount) gw_mask1 |= (1UL << 60); /* F4: 금액 */ + if (gw_len > 0) gw_mask1 |= (1UL << 52); /* F11: STAN 존재 */ + sprintf(gw_bmp1, "%016lX", gw_mask1); + + /* --- 4) 2차 비트맵 구성 (확장필드) --------------------------- */ + gw_mask2 = 0; + if (gw_amount >= 1000000L) gw_mask2 |= (1UL << 63); /* F65: 고액표식 */ + if (gw_len >= 38) gw_mask2 |= (1UL << 60); /* F68: 확장데이터 */ + sprintf(gw_bmp2, "%016lX", gw_mask2); + + /* --- 5) 존재필드 수 계산 ------------------------------------- */ + gw_present = 0; + for (gw_i = 0; gw_i < 64; gw_i++) + if (gw_mask1 & (1UL << gw_i)) gw_present++; + + /* --- 6) LRC/합계 체크섬 (raw XOR + 누적합) ------------------- */ + gw_lrc = 0; gw_sum = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) { + gw_lrc ^= (unsigned char)gw_raw[gw_i]; + gw_sum += (unsigned char)gw_raw[gw_i]; + } + + /* --- 7) 간이 MAC (회전 누산 해시) --------------------------- */ + gw_mac = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) + gw_mac = ((gw_mac << 5) ^ (gw_mac >> 2) + ^ (unsigned long)(unsigned char)gw_raw[gw_i]) & 0xFFFFFFUL; + + /* --- 8) PAN Luhn / 금액밴드 / 금액자리합 -------------------- */ + gw_luhn = (gw_pan[0] && acq_luhn_ok(gw_pan)) ? 1 : 0; + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + { long gw_a = gw_amount; gw_digsum = 0; + if (gw_a < 0) gw_a = -gw_a; + while (gw_a > 0) { gw_digsum += gw_a % 10; gw_a /= 10; } } + if (gw_band == 3 && gw_present < 3) { + setl(b, T_RC, 9403); sets_(b, T_MSG, "고액전문 필수필드 누락"); FAIL(b); + } + + /* --- 9) 필드추출 데모 + 응답보조(심화전용 필드) + 로깅 ------ */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + memset(gw_fld, 0, sizeof(gw_fld)); + if (gw_len >= 10) strncpy(gw_fld, gw_raw + 4, 6); + setl(b, T_ARG6, gw_present); + setl(b, T_ARG7, gw_band); + setl(b, T_ARG8, gw_digsum); + setl(b, T_AMT2, gw_sum); + setl(b, T_AMT3, gw_lrc); + sets_(b, T_STR8, gw_bmp1); + (void)gw_bmp2; (void)gw_luhn; (void)gw_mac; (void)gw_fld; + userlog("MG_NMADVICE 심화계산 mti=%s bmp1=%s present=%ld lrc=%ld mac=%06lX band=%ld thr=%ld", + gw_mti, gw_bmp1, gw_present, gw_lrc, gw_mac, gw_band, gw_thr); + } char chan[16], raw[512], nmic[8], bizdate[16], newkey[24]; long stan, msg_id, n, kv = 0; diff --git a/app/src/mg/svc/MG_NMRSP.pgc b/app/src/mg/svc/MG_NMRSP.pgc index cb7d9d6..fbf86b5 100644 --- a/app/src/mg/svc/MG_NMRSP.pgc +++ b/app/src/mg/svc/MG_NMRSP.pgc @@ -9,6 +9,95 @@ void MG_NMRSP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문 파싱/비트맵/체크섬/MAC - 순수계산) - MG_NMRSP + * 필드형식검증 / 1·2차 비트맵 구성 / LRC·합계 체크섬 / 간이 MAC / + * PAN Luhn / 금액밴드 / 필드추출 / 응답보조 / 로깅 + * =================================================================== */ + { + char gw_mti[8], gw_proc[8], gw_raw[512], gw_pan[24]; + char gw_bmp1[24], gw_bmp2[24], gw_fld[64]; + unsigned long gw_mask1, gw_mask2, gw_mac; + long gw_amount, gw_len, gw_i, gw_lrc, gw_sum, gw_present; + long gw_band, gw_valid, gw_luhn, gw_digsum, gw_thr; + + /* --- 1) 입력 캡처 --------------------------------------------- */ + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_STR4, gw_proc, sizeof(gw_proc)); if (gw_proc[0] == 0) strcpy(gw_proc, "000000"); + gets_(b, T_STR3, gw_raw, sizeof(gw_raw)); + gets_(b, T_STR5, gw_pan, sizeof(gw_pan)); + gw_amount = getl(b, T_AMOUNT); + gw_len = (long)strlen(gw_raw); + gw_thr = 123L; + + /* --- 2) 필드 형식 검증: MTI/처리코드 숫자성 -------------------- */ + gw_valid = 1; + for (gw_i = 0; gw_i < 4 && gw_mti[gw_i]; gw_i++) + if (gw_mti[gw_i] < '0' || gw_mti[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9401); sets_(b, T_MSG, "MTI 비숫자 포함"); FAIL(b); } + for (gw_i = 0; gw_i < 6 && gw_proc[gw_i]; gw_i++) + if (gw_proc[gw_i] < '0' || gw_proc[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9402); sets_(b, T_MSG, "처리코드 비숫자 포함"); FAIL(b); } + + /* --- 3) 1차 비트맵 구성 (존재필드 마스크) --------------------- */ + gw_mask1 = 0; + gw_mask1 |= (1UL << 63); /* F1: 2차 비트맵 지시 */ + if (gw_pan[0]) gw_mask1 |= (1UL << 62); /* F2: PAN */ + if (gw_proc[0]) gw_mask1 |= (1UL << 61); /* F3: 처리코드 */ + if (gw_amount) gw_mask1 |= (1UL << 60); /* F4: 금액 */ + if (gw_len > 0) gw_mask1 |= (1UL << 52); /* F11: STAN 존재 */ + sprintf(gw_bmp1, "%016lX", gw_mask1); + + /* --- 4) 2차 비트맵 구성 (확장필드) --------------------------- */ + gw_mask2 = 0; + if (gw_amount >= 1000000L) gw_mask2 |= (1UL << 63); /* F65: 고액표식 */ + if (gw_len >= 38) gw_mask2 |= (1UL << 60); /* F68: 확장데이터 */ + sprintf(gw_bmp2, "%016lX", gw_mask2); + + /* --- 5) 존재필드 수 계산 ------------------------------------- */ + gw_present = 0; + for (gw_i = 0; gw_i < 64; gw_i++) + if (gw_mask1 & (1UL << gw_i)) gw_present++; + + /* --- 6) LRC/합계 체크섬 (raw XOR + 누적합) ------------------- */ + gw_lrc = 0; gw_sum = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) { + gw_lrc ^= (unsigned char)gw_raw[gw_i]; + gw_sum += (unsigned char)gw_raw[gw_i]; + } + + /* --- 7) 간이 MAC (회전 누산 해시) --------------------------- */ + gw_mac = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) + gw_mac = ((gw_mac << 5) ^ (gw_mac >> 2) + ^ (unsigned long)(unsigned char)gw_raw[gw_i]) & 0xFFFFFFUL; + + /* --- 8) PAN Luhn / 금액밴드 / 금액자리합 -------------------- */ + gw_luhn = (gw_pan[0] && acq_luhn_ok(gw_pan)) ? 1 : 0; + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + { long gw_a = gw_amount; gw_digsum = 0; + if (gw_a < 0) gw_a = -gw_a; + while (gw_a > 0) { gw_digsum += gw_a % 10; gw_a /= 10; } } + if (gw_band == 3 && gw_present < 3) { + setl(b, T_RC, 9403); sets_(b, T_MSG, "고액전문 필수필드 누락"); FAIL(b); + } + + /* --- 9) 필드추출 데모 + 응답보조(심화전용 필드) + 로깅 ------ */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + memset(gw_fld, 0, sizeof(gw_fld)); + if (gw_len >= 10) strncpy(gw_fld, gw_raw + 4, 6); + setl(b, T_ARG6, gw_present); + setl(b, T_ARG7, gw_band); + setl(b, T_ARG8, gw_digsum); + setl(b, T_AMT2, gw_sum); + setl(b, T_AMT3, gw_lrc); + sets_(b, T_STR8, gw_bmp1); + (void)gw_bmp2; (void)gw_luhn; (void)gw_mac; (void)gw_fld; + userlog("MG_NMRSP 심화계산 mti=%s bmp1=%s present=%ld lrc=%ld mac=%06lX band=%ld thr=%ld", + gw_mti, gw_bmp1, gw_present, gw_lrc, gw_mac, gw_band, gw_thr); + } char chan[16], raw[512], nmic[8], bizdate[16]; long stan, msg_id, n; diff --git a/app/src/mg/svc/MG_PACK.pgc b/app/src/mg/svc/MG_PACK.pgc index 0bb44bc..f1c3021 100644 --- a/app/src/mg/svc/MG_PACK.pgc +++ b/app/src/mg/svc/MG_PACK.pgc @@ -9,6 +9,155 @@ void MG_PACK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_PACK + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_PACK 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 130L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_PACK", "C", gw_bizdate) < 0) { + userlog("MG_PACK 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_PACK 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_PACK 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char mti[8], proc[8], raw[64]; long amount; gets_(b, T_STR1, mti, sizeof(mti)); if (mti[0] == 0) strcpy(mti, "0200"); diff --git a/app/src/mg/svc/MG_PANMASK.pgc b/app/src/mg/svc/MG_PANMASK.pgc index 3a1a264..bae1b08 100644 --- a/app/src/mg/svc/MG_PANMASK.pgc +++ b/app/src/mg/svc/MG_PANMASK.pgc @@ -28,6 +28,155 @@ static int mg_panmask_apply(const char *pan, char *out /* >= 20 */) void MG_PANMASK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_PANMASK + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_PANMASK 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 126L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_PANMASK", "C", gw_bizdate) < 0) { + userlog("MG_PANMASK 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_PANMASK 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_PANMASK 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char pan[24], masked[24]; int shortpan; EXEC SQL BEGIN DECLARE SECTION; diff --git a/app/src/mg/svc/MG_PARSE.pgc b/app/src/mg/svc/MG_PARSE.pgc index 8ec0620..d1d5974 100644 --- a/app/src/mg/svc/MG_PARSE.pgc +++ b/app/src/mg/svc/MG_PARSE.pgc @@ -9,6 +9,155 @@ void MG_PARSE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_PARSE + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_PARSE 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 102L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_PARSE", "C", gw_bizdate) < 0) { + userlog("MG_PARSE 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_PARSE 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_PARSE 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char raw[512], mti[8], proc[8]; long n; gets_(b, T_STR3, raw, sizeof(raw)); diff --git a/app/src/mg/svc/MG_PINBLOCK.pgc b/app/src/mg/svc/MG_PINBLOCK.pgc index 50105b2..8fc9f2f 100644 --- a/app/src/mg/svc/MG_PINBLOCK.pgc +++ b/app/src/mg/svc/MG_PINBLOCK.pgc @@ -31,6 +31,95 @@ static void mg_pinblock_iso0(const char *pin, const char *pan, char *out /* >= 1 void MG_PINBLOCK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문 파싱/비트맵/체크섬/MAC - 순수계산) - MG_PINBLOCK + * 필드형식검증 / 1·2차 비트맵 구성 / LRC·합계 체크섬 / 간이 MAC / + * PAN Luhn / 금액밴드 / 필드추출 / 응답보조 / 로깅 + * =================================================================== */ + { + char gw_mti[8], gw_proc[8], gw_raw[512], gw_pan[24]; + char gw_bmp1[24], gw_bmp2[24], gw_fld[64]; + unsigned long gw_mask1, gw_mask2, gw_mac; + long gw_amount, gw_len, gw_i, gw_lrc, gw_sum, gw_present; + long gw_band, gw_valid, gw_luhn, gw_digsum, gw_thr; + + /* --- 1) 입력 캡처 --------------------------------------------- */ + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_STR4, gw_proc, sizeof(gw_proc)); if (gw_proc[0] == 0) strcpy(gw_proc, "000000"); + gets_(b, T_STR3, gw_raw, sizeof(gw_raw)); + gets_(b, T_STR5, gw_pan, sizeof(gw_pan)); + gw_amount = getl(b, T_AMOUNT); + gw_len = (long)strlen(gw_raw); + gw_thr = 137L; + + /* --- 2) 필드 형식 검증: MTI/처리코드 숫자성 -------------------- */ + gw_valid = 1; + for (gw_i = 0; gw_i < 4 && gw_mti[gw_i]; gw_i++) + if (gw_mti[gw_i] < '0' || gw_mti[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9401); sets_(b, T_MSG, "MTI 비숫자 포함"); FAIL(b); } + for (gw_i = 0; gw_i < 6 && gw_proc[gw_i]; gw_i++) + if (gw_proc[gw_i] < '0' || gw_proc[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9402); sets_(b, T_MSG, "처리코드 비숫자 포함"); FAIL(b); } + + /* --- 3) 1차 비트맵 구성 (존재필드 마스크) --------------------- */ + gw_mask1 = 0; + gw_mask1 |= (1UL << 63); /* F1: 2차 비트맵 지시 */ + if (gw_pan[0]) gw_mask1 |= (1UL << 62); /* F2: PAN */ + if (gw_proc[0]) gw_mask1 |= (1UL << 61); /* F3: 처리코드 */ + if (gw_amount) gw_mask1 |= (1UL << 60); /* F4: 금액 */ + if (gw_len > 0) gw_mask1 |= (1UL << 52); /* F11: STAN 존재 */ + sprintf(gw_bmp1, "%016lX", gw_mask1); + + /* --- 4) 2차 비트맵 구성 (확장필드) --------------------------- */ + gw_mask2 = 0; + if (gw_amount >= 1000000L) gw_mask2 |= (1UL << 63); /* F65: 고액표식 */ + if (gw_len >= 38) gw_mask2 |= (1UL << 60); /* F68: 확장데이터 */ + sprintf(gw_bmp2, "%016lX", gw_mask2); + + /* --- 5) 존재필드 수 계산 ------------------------------------- */ + gw_present = 0; + for (gw_i = 0; gw_i < 64; gw_i++) + if (gw_mask1 & (1UL << gw_i)) gw_present++; + + /* --- 6) LRC/합계 체크섬 (raw XOR + 누적합) ------------------- */ + gw_lrc = 0; gw_sum = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) { + gw_lrc ^= (unsigned char)gw_raw[gw_i]; + gw_sum += (unsigned char)gw_raw[gw_i]; + } + + /* --- 7) 간이 MAC (회전 누산 해시) --------------------------- */ + gw_mac = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) + gw_mac = ((gw_mac << 5) ^ (gw_mac >> 2) + ^ (unsigned long)(unsigned char)gw_raw[gw_i]) & 0xFFFFFFUL; + + /* --- 8) PAN Luhn / 금액밴드 / 금액자리합 -------------------- */ + gw_luhn = (gw_pan[0] && acq_luhn_ok(gw_pan)) ? 1 : 0; + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + { long gw_a = gw_amount; gw_digsum = 0; + if (gw_a < 0) gw_a = -gw_a; + while (gw_a > 0) { gw_digsum += gw_a % 10; gw_a /= 10; } } + if (gw_band == 3 && gw_present < 3) { + setl(b, T_RC, 9403); sets_(b, T_MSG, "고액전문 필수필드 누락"); FAIL(b); + } + + /* --- 9) 필드추출 데모 + 응답보조(심화전용 필드) + 로깅 ------ */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + memset(gw_fld, 0, sizeof(gw_fld)); + if (gw_len >= 10) strncpy(gw_fld, gw_raw + 4, 6); + setl(b, T_ARG6, gw_present); + setl(b, T_ARG7, gw_band); + setl(b, T_ARG8, gw_digsum); + setl(b, T_AMT2, gw_sum); + setl(b, T_AMT3, gw_lrc); + sets_(b, T_STR8, gw_bmp1); + (void)gw_bmp2; (void)gw_luhn; (void)gw_mac; (void)gw_fld; + userlog("MG_PINBLOCK 심화계산 mti=%s bmp1=%s present=%ld lrc=%ld mac=%06lX band=%ld thr=%ld", + gw_mti, gw_bmp1, gw_present, gw_lrc, gw_mac, gw_band, gw_thr); + } char pin[16], pan[24], block[20]; long plen; diff --git a/app/src/mg/svc/MG_PREAUTH.pgc b/app/src/mg/svc/MG_PREAUTH.pgc index 06de62f..d1f49b8 100644 --- a/app/src/mg/svc/MG_PREAUTH.pgc +++ b/app/src/mg/svc/MG_PREAUTH.pgc @@ -9,6 +9,95 @@ void MG_PREAUTH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문 파싱/비트맵/체크섬/MAC - 순수계산) - MG_PREAUTH + * 필드형식검증 / 1·2차 비트맵 구성 / LRC·합계 체크섬 / 간이 MAC / + * PAN Luhn / 금액밴드 / 필드추출 / 응답보조 / 로깅 + * =================================================================== */ + { + char gw_mti[8], gw_proc[8], gw_raw[512], gw_pan[24]; + char gw_bmp1[24], gw_bmp2[24], gw_fld[64]; + unsigned long gw_mask1, gw_mask2, gw_mac; + long gw_amount, gw_len, gw_i, gw_lrc, gw_sum, gw_present; + long gw_band, gw_valid, gw_luhn, gw_digsum, gw_thr; + + /* --- 1) 입력 캡처 --------------------------------------------- */ + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_STR4, gw_proc, sizeof(gw_proc)); if (gw_proc[0] == 0) strcpy(gw_proc, "000000"); + gets_(b, T_STR3, gw_raw, sizeof(gw_raw)); + gets_(b, T_STR5, gw_pan, sizeof(gw_pan)); + gw_amount = getl(b, T_AMOUNT); + gw_len = (long)strlen(gw_raw); + gw_thr = 80L; + + /* --- 2) 필드 형식 검증: MTI/처리코드 숫자성 -------------------- */ + gw_valid = 1; + for (gw_i = 0; gw_i < 4 && gw_mti[gw_i]; gw_i++) + if (gw_mti[gw_i] < '0' || gw_mti[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9401); sets_(b, T_MSG, "MTI 비숫자 포함"); FAIL(b); } + for (gw_i = 0; gw_i < 6 && gw_proc[gw_i]; gw_i++) + if (gw_proc[gw_i] < '0' || gw_proc[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9402); sets_(b, T_MSG, "처리코드 비숫자 포함"); FAIL(b); } + + /* --- 3) 1차 비트맵 구성 (존재필드 마스크) --------------------- */ + gw_mask1 = 0; + gw_mask1 |= (1UL << 63); /* F1: 2차 비트맵 지시 */ + if (gw_pan[0]) gw_mask1 |= (1UL << 62); /* F2: PAN */ + if (gw_proc[0]) gw_mask1 |= (1UL << 61); /* F3: 처리코드 */ + if (gw_amount) gw_mask1 |= (1UL << 60); /* F4: 금액 */ + if (gw_len > 0) gw_mask1 |= (1UL << 52); /* F11: STAN 존재 */ + sprintf(gw_bmp1, "%016lX", gw_mask1); + + /* --- 4) 2차 비트맵 구성 (확장필드) --------------------------- */ + gw_mask2 = 0; + if (gw_amount >= 1000000L) gw_mask2 |= (1UL << 63); /* F65: 고액표식 */ + if (gw_len >= 38) gw_mask2 |= (1UL << 60); /* F68: 확장데이터 */ + sprintf(gw_bmp2, "%016lX", gw_mask2); + + /* --- 5) 존재필드 수 계산 ------------------------------------- */ + gw_present = 0; + for (gw_i = 0; gw_i < 64; gw_i++) + if (gw_mask1 & (1UL << gw_i)) gw_present++; + + /* --- 6) LRC/합계 체크섬 (raw XOR + 누적합) ------------------- */ + gw_lrc = 0; gw_sum = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) { + gw_lrc ^= (unsigned char)gw_raw[gw_i]; + gw_sum += (unsigned char)gw_raw[gw_i]; + } + + /* --- 7) 간이 MAC (회전 누산 해시) --------------------------- */ + gw_mac = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) + gw_mac = ((gw_mac << 5) ^ (gw_mac >> 2) + ^ (unsigned long)(unsigned char)gw_raw[gw_i]) & 0xFFFFFFUL; + + /* --- 8) PAN Luhn / 금액밴드 / 금액자리합 -------------------- */ + gw_luhn = (gw_pan[0] && acq_luhn_ok(gw_pan)) ? 1 : 0; + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + { long gw_a = gw_amount; gw_digsum = 0; + if (gw_a < 0) gw_a = -gw_a; + while (gw_a > 0) { gw_digsum += gw_a % 10; gw_a /= 10; } } + if (gw_band == 3 && gw_present < 3) { + setl(b, T_RC, 9403); sets_(b, T_MSG, "고액전문 필수필드 누락"); FAIL(b); + } + + /* --- 9) 필드추출 데모 + 응답보조(심화전용 필드) + 로깅 ------ */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + memset(gw_fld, 0, sizeof(gw_fld)); + if (gw_len >= 10) strncpy(gw_fld, gw_raw + 4, 6); + setl(b, T_ARG6, gw_present); + setl(b, T_ARG7, gw_band); + setl(b, T_ARG8, gw_digsum); + setl(b, T_AMT2, gw_sum); + setl(b, T_AMT3, gw_lrc); + sets_(b, T_STR8, gw_bmp1); + (void)gw_bmp2; (void)gw_luhn; (void)gw_mac; (void)gw_fld; + userlog("MG_PREAUTH 심화계산 mti=%s bmp1=%s present=%ld lrc=%ld mac=%06lX band=%ld thr=%ld", + gw_mti, gw_bmp1, gw_present, gw_lrc, gw_mac, gw_band, gw_thr); + } char chan[16], raw[512], bizdate[16], proc[8]; long stan, msg_id, amount, hold_bps, hold_amt, n; diff --git a/app/src/mg/svc/MG_QDEPTH.pgc b/app/src/mg/svc/MG_QDEPTH.pgc index 8a9fc14..d04f392 100644 --- a/app/src/mg/svc/MG_QDEPTH.pgc +++ b/app/src/mg/svc/MG_QDEPTH.pgc @@ -10,6 +10,155 @@ void MG_QDEPTH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_QDEPTH + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_QDEPTH 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 117L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_QDEPTH", "C", gw_bizdate) < 0) { + userlog("MG_QDEPTH 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_QDEPTH 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_QDEPTH 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; long h_q, h_s, h_d, h_f, h_maxretry; diff --git a/app/src/mg/svc/MG_QUEUE.pgc b/app/src/mg/svc/MG_QUEUE.pgc index 76e5cfd..fd865a1 100644 --- a/app/src/mg/svc/MG_QUEUE.pgc +++ b/app/src/mg/svc/MG_QUEUE.pgc @@ -9,6 +9,155 @@ void MG_QUEUE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_QUEUE + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_QUEUE 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 112L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_QUEUE", "C", gw_bizdate) < 0) { + userlog("MG_QUEUE 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_QUEUE 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_QUEUE 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char chan[16], mti[8], raw[512], bizdate[16]; long stan, qid; gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "CH01"); diff --git a/app/src/mg/svc/MG_RATELIMIT.pgc b/app/src/mg/svc/MG_RATELIMIT.pgc index af466c2..bfbaf9e 100644 --- a/app/src/mg/svc/MG_RATELIMIT.pgc +++ b/app/src/mg/svc/MG_RATELIMIT.pgc @@ -9,6 +9,155 @@ void MG_RATELIMIT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_RATELIMIT + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_RATELIMIT 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 106L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_RATELIMIT", "C", gw_bizdate) < 0) { + userlog("MG_RATELIMIT 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_RATELIMIT 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_RATELIMIT 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } long tps, pct; EXEC SQL BEGIN DECLARE SECTION; char h_chan[16]; diff --git a/app/src/mg/svc/MG_READYZ.pgc b/app/src/mg/svc/MG_READYZ.pgc index c70ba91..6c566cd 100644 --- a/app/src/mg/svc/MG_READYZ.pgc +++ b/app/src/mg/svc/MG_READYZ.pgc @@ -9,6 +9,155 @@ void MG_READYZ(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_READYZ + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_READYZ 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 126L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_READYZ", "C", gw_bizdate) < 0) { + userlog("MG_READYZ 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_READYZ 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_READYZ 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } long ready; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/src/mg/svc/MG_REASM.pgc b/app/src/mg/svc/MG_REASM.pgc index fad5082..dd1b8d2 100644 --- a/app/src/mg/svc/MG_REASM.pgc +++ b/app/src/mg/svc/MG_REASM.pgc @@ -10,6 +10,155 @@ void MG_REASM(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_REASM + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_REASM 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 99L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_REASM", "C", gw_bizdate) < 0) { + userlog("MG_REASM 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_REASM 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_REASM 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char seg[4][128], full[512]; long expect, got = 0, total = 0; int i; diff --git a/app/src/mg/svc/MG_RECV.pgc b/app/src/mg/svc/MG_RECV.pgc index 3aa6f77..66d4f3f 100644 --- a/app/src/mg/svc/MG_RECV.pgc +++ b/app/src/mg/svc/MG_RECV.pgc @@ -9,6 +9,155 @@ void MG_RECV(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_RECV + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_RECV 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 87L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_RECV", "C", gw_bizdate) < 0) { + userlog("MG_RECV 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_RECV 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_RECV 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char chan[16], mti[8], raw[512], bizdate[16], pan[24]; long stan, msg_id, amount; diff --git a/app/src/mg/svc/MG_RELAYADPT.pgc b/app/src/mg/svc/MG_RELAYADPT.pgc index fb2a0bd..8af165d 100644 --- a/app/src/mg/svc/MG_RELAYADPT.pgc +++ b/app/src/mg/svc/MG_RELAYADPT.pgc @@ -9,6 +9,95 @@ void MG_RELAYADPT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문 파싱/비트맵/체크섬/MAC - 순수계산) - MG_RELAYADPT + * 필드형식검증 / 1·2차 비트맵 구성 / LRC·합계 체크섬 / 간이 MAC / + * PAN Luhn / 금액밴드 / 필드추출 / 응답보조 / 로깅 + * =================================================================== */ + { + char gw_mti[8], gw_proc[8], gw_raw[512], gw_pan[24]; + char gw_bmp1[24], gw_bmp2[24], gw_fld[64]; + unsigned long gw_mask1, gw_mask2, gw_mac; + long gw_amount, gw_len, gw_i, gw_lrc, gw_sum, gw_present; + long gw_band, gw_valid, gw_luhn, gw_digsum, gw_thr; + + /* --- 1) 입력 캡처 --------------------------------------------- */ + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_STR4, gw_proc, sizeof(gw_proc)); if (gw_proc[0] == 0) strcpy(gw_proc, "000000"); + gets_(b, T_STR3, gw_raw, sizeof(gw_raw)); + gets_(b, T_STR5, gw_pan, sizeof(gw_pan)); + gw_amount = getl(b, T_AMOUNT); + gw_len = (long)strlen(gw_raw); + gw_thr = 101L; + + /* --- 2) 필드 형식 검증: MTI/처리코드 숫자성 -------------------- */ + gw_valid = 1; + for (gw_i = 0; gw_i < 4 && gw_mti[gw_i]; gw_i++) + if (gw_mti[gw_i] < '0' || gw_mti[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9401); sets_(b, T_MSG, "MTI 비숫자 포함"); FAIL(b); } + for (gw_i = 0; gw_i < 6 && gw_proc[gw_i]; gw_i++) + if (gw_proc[gw_i] < '0' || gw_proc[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9402); sets_(b, T_MSG, "처리코드 비숫자 포함"); FAIL(b); } + + /* --- 3) 1차 비트맵 구성 (존재필드 마스크) --------------------- */ + gw_mask1 = 0; + gw_mask1 |= (1UL << 63); /* F1: 2차 비트맵 지시 */ + if (gw_pan[0]) gw_mask1 |= (1UL << 62); /* F2: PAN */ + if (gw_proc[0]) gw_mask1 |= (1UL << 61); /* F3: 처리코드 */ + if (gw_amount) gw_mask1 |= (1UL << 60); /* F4: 금액 */ + if (gw_len > 0) gw_mask1 |= (1UL << 52); /* F11: STAN 존재 */ + sprintf(gw_bmp1, "%016lX", gw_mask1); + + /* --- 4) 2차 비트맵 구성 (확장필드) --------------------------- */ + gw_mask2 = 0; + if (gw_amount >= 1000000L) gw_mask2 |= (1UL << 63); /* F65: 고액표식 */ + if (gw_len >= 38) gw_mask2 |= (1UL << 60); /* F68: 확장데이터 */ + sprintf(gw_bmp2, "%016lX", gw_mask2); + + /* --- 5) 존재필드 수 계산 ------------------------------------- */ + gw_present = 0; + for (gw_i = 0; gw_i < 64; gw_i++) + if (gw_mask1 & (1UL << gw_i)) gw_present++; + + /* --- 6) LRC/합계 체크섬 (raw XOR + 누적합) ------------------- */ + gw_lrc = 0; gw_sum = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) { + gw_lrc ^= (unsigned char)gw_raw[gw_i]; + gw_sum += (unsigned char)gw_raw[gw_i]; + } + + /* --- 7) 간이 MAC (회전 누산 해시) --------------------------- */ + gw_mac = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) + gw_mac = ((gw_mac << 5) ^ (gw_mac >> 2) + ^ (unsigned long)(unsigned char)gw_raw[gw_i]) & 0xFFFFFFUL; + + /* --- 8) PAN Luhn / 금액밴드 / 금액자리합 -------------------- */ + gw_luhn = (gw_pan[0] && acq_luhn_ok(gw_pan)) ? 1 : 0; + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + { long gw_a = gw_amount; gw_digsum = 0; + if (gw_a < 0) gw_a = -gw_a; + while (gw_a > 0) { gw_digsum += gw_a % 10; gw_a /= 10; } } + if (gw_band == 3 && gw_present < 3) { + setl(b, T_RC, 9403); sets_(b, T_MSG, "고액전문 필수필드 누락"); FAIL(b); + } + + /* --- 9) 필드추출 데모 + 응답보조(심화전용 필드) + 로깅 ------ */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + memset(gw_fld, 0, sizeof(gw_fld)); + if (gw_len >= 10) strncpy(gw_fld, gw_raw + 4, 6); + setl(b, T_ARG6, gw_present); + setl(b, T_ARG7, gw_band); + setl(b, T_ARG8, gw_digsum); + setl(b, T_AMT2, gw_sum); + setl(b, T_AMT3, gw_lrc); + sets_(b, T_STR8, gw_bmp1); + (void)gw_bmp2; (void)gw_luhn; (void)gw_mac; (void)gw_fld; + userlog("MG_RELAYADPT 심화계산 mti=%s bmp1=%s present=%ld lrc=%ld mac=%06lX band=%ld thr=%ld", + gw_mti, gw_bmp1, gw_present, gw_lrc, gw_mac, gw_band, gw_thr); + } char raw[512], origchan[8], body[512]; long n; diff --git a/app/src/mg/svc/MG_RESEND.pgc b/app/src/mg/svc/MG_RESEND.pgc index cf20a2a..c0ec8eb 100644 --- a/app/src/mg/svc/MG_RESEND.pgc +++ b/app/src/mg/svc/MG_RESEND.pgc @@ -9,6 +9,155 @@ void MG_RESEND(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_RESEND + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_RESEND 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 112L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_RESEND", "C", gw_bizdate) < 0) { + userlog("MG_RESEND 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_RESEND 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_RESEND 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char chan[16], mti[8], raw[512], bizdate[16]; long stan, msg_id, qid; diff --git a/app/src/mg/svc/MG_RESNDONE.pgc b/app/src/mg/svc/MG_RESNDONE.pgc index 79e10a7..12ea3a4 100644 --- a/app/src/mg/svc/MG_RESNDONE.pgc +++ b/app/src/mg/svc/MG_RESNDONE.pgc @@ -9,6 +9,155 @@ void MG_RESNDONE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_RESNDONE + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_RESNDONE 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 89L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_RESNDONE", "C", gw_bizdate) < 0) { + userlog("MG_RESNDONE 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_RESNDONE 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_RESNDONE 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } long limit, late = 0; EXEC SQL BEGIN DECLARE SECTION; long h_qid, h_retry; diff --git a/app/src/mg/svc/MG_RETRY.pgc b/app/src/mg/svc/MG_RETRY.pgc index e473f7e..8aec058 100644 --- a/app/src/mg/svc/MG_RETRY.pgc +++ b/app/src/mg/svc/MG_RETRY.pgc @@ -9,6 +9,155 @@ void MG_RETRY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_RETRY + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_RETRY 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 129L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_RETRY", "C", gw_bizdate) < 0) { + userlog("MG_RETRY 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_RETRY 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_RETRY 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } EXEC SQL BEGIN DECLARE SECTION; long h_qid, h_retry; EXEC SQL END DECLARE SECTION; diff --git a/app/src/mg/svc/MG_REVADVICE.pgc b/app/src/mg/svc/MG_REVADVICE.pgc index 46fde6e..950a768 100644 --- a/app/src/mg/svc/MG_REVADVICE.pgc +++ b/app/src/mg/svc/MG_REVADVICE.pgc @@ -10,6 +10,155 @@ void MG_REVADVICE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_REVADVICE + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_REVADVICE 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 88L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_REVADVICE", "C", gw_bizdate) < 0) { + userlog("MG_REVADVICE 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_REVADVICE 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_REVADVICE 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char chan[16], bizdate[16], raw[512]; long stan, msg_id, amount; EXEC SQL BEGIN DECLARE SECTION; diff --git a/app/src/mg/svc/MG_REVADVRSP.pgc b/app/src/mg/svc/MG_REVADVRSP.pgc index 4df2749..be08afd 100644 --- a/app/src/mg/svc/MG_REVADVRSP.pgc +++ b/app/src/mg/svc/MG_REVADVRSP.pgc @@ -9,6 +9,95 @@ void MG_REVADVRSP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문 파싱/비트맵/체크섬/MAC - 순수계산) - MG_REVADVRSP + * 필드형식검증 / 1·2차 비트맵 구성 / LRC·합계 체크섬 / 간이 MAC / + * PAN Luhn / 금액밴드 / 필드추출 / 응답보조 / 로깅 + * =================================================================== */ + { + char gw_mti[8], gw_proc[8], gw_raw[512], gw_pan[24]; + char gw_bmp1[24], gw_bmp2[24], gw_fld[64]; + unsigned long gw_mask1, gw_mask2, gw_mac; + long gw_amount, gw_len, gw_i, gw_lrc, gw_sum, gw_present; + long gw_band, gw_valid, gw_luhn, gw_digsum, gw_thr; + + /* --- 1) 입력 캡처 --------------------------------------------- */ + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_STR4, gw_proc, sizeof(gw_proc)); if (gw_proc[0] == 0) strcpy(gw_proc, "000000"); + gets_(b, T_STR3, gw_raw, sizeof(gw_raw)); + gets_(b, T_STR5, gw_pan, sizeof(gw_pan)); + gw_amount = getl(b, T_AMOUNT); + gw_len = (long)strlen(gw_raw); + gw_thr = 124L; + + /* --- 2) 필드 형식 검증: MTI/처리코드 숫자성 -------------------- */ + gw_valid = 1; + for (gw_i = 0; gw_i < 4 && gw_mti[gw_i]; gw_i++) + if (gw_mti[gw_i] < '0' || gw_mti[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9401); sets_(b, T_MSG, "MTI 비숫자 포함"); FAIL(b); } + for (gw_i = 0; gw_i < 6 && gw_proc[gw_i]; gw_i++) + if (gw_proc[gw_i] < '0' || gw_proc[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9402); sets_(b, T_MSG, "처리코드 비숫자 포함"); FAIL(b); } + + /* --- 3) 1차 비트맵 구성 (존재필드 마스크) --------------------- */ + gw_mask1 = 0; + gw_mask1 |= (1UL << 63); /* F1: 2차 비트맵 지시 */ + if (gw_pan[0]) gw_mask1 |= (1UL << 62); /* F2: PAN */ + if (gw_proc[0]) gw_mask1 |= (1UL << 61); /* F3: 처리코드 */ + if (gw_amount) gw_mask1 |= (1UL << 60); /* F4: 금액 */ + if (gw_len > 0) gw_mask1 |= (1UL << 52); /* F11: STAN 존재 */ + sprintf(gw_bmp1, "%016lX", gw_mask1); + + /* --- 4) 2차 비트맵 구성 (확장필드) --------------------------- */ + gw_mask2 = 0; + if (gw_amount >= 1000000L) gw_mask2 |= (1UL << 63); /* F65: 고액표식 */ + if (gw_len >= 38) gw_mask2 |= (1UL << 60); /* F68: 확장데이터 */ + sprintf(gw_bmp2, "%016lX", gw_mask2); + + /* --- 5) 존재필드 수 계산 ------------------------------------- */ + gw_present = 0; + for (gw_i = 0; gw_i < 64; gw_i++) + if (gw_mask1 & (1UL << gw_i)) gw_present++; + + /* --- 6) LRC/합계 체크섬 (raw XOR + 누적합) ------------------- */ + gw_lrc = 0; gw_sum = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) { + gw_lrc ^= (unsigned char)gw_raw[gw_i]; + gw_sum += (unsigned char)gw_raw[gw_i]; + } + + /* --- 7) 간이 MAC (회전 누산 해시) --------------------------- */ + gw_mac = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) + gw_mac = ((gw_mac << 5) ^ (gw_mac >> 2) + ^ (unsigned long)(unsigned char)gw_raw[gw_i]) & 0xFFFFFFUL; + + /* --- 8) PAN Luhn / 금액밴드 / 금액자리합 -------------------- */ + gw_luhn = (gw_pan[0] && acq_luhn_ok(gw_pan)) ? 1 : 0; + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + { long gw_a = gw_amount; gw_digsum = 0; + if (gw_a < 0) gw_a = -gw_a; + while (gw_a > 0) { gw_digsum += gw_a % 10; gw_a /= 10; } } + if (gw_band == 3 && gw_present < 3) { + setl(b, T_RC, 9403); sets_(b, T_MSG, "고액전문 필수필드 누락"); FAIL(b); + } + + /* --- 9) 필드추출 데모 + 응답보조(심화전용 필드) + 로깅 ------ */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + memset(gw_fld, 0, sizeof(gw_fld)); + if (gw_len >= 10) strncpy(gw_fld, gw_raw + 4, 6); + setl(b, T_ARG6, gw_present); + setl(b, T_ARG7, gw_band); + setl(b, T_ARG8, gw_digsum); + setl(b, T_AMT2, gw_sum); + setl(b, T_AMT3, gw_lrc); + sets_(b, T_STR8, gw_bmp1); + (void)gw_bmp2; (void)gw_luhn; (void)gw_mac; (void)gw_fld; + userlog("MG_REVADVRSP 심화계산 mti=%s bmp1=%s present=%ld lrc=%ld mac=%06lX band=%ld thr=%ld", + gw_mti, gw_bmp1, gw_present, gw_lrc, gw_mac, gw_band, gw_thr); + } char chan[16], bizdate[16]; long orig_stan, orig_id, stan, msg_id, acked = 0; diff --git a/app/src/mg/svc/MG_REVERSAL.pgc b/app/src/mg/svc/MG_REVERSAL.pgc index f819cba..896f5c8 100644 --- a/app/src/mg/svc/MG_REVERSAL.pgc +++ b/app/src/mg/svc/MG_REVERSAL.pgc @@ -10,6 +10,155 @@ void MG_REVERSAL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_REVERSAL + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_REVERSAL 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 95L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_REVERSAL", "C", gw_bizdate) < 0) { + userlog("MG_REVERSAL 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_REVERSAL 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_REVERSAL 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char chan[16], bizdate[16]; long stan, orig_id, msg_id, rev_stan, amount; EXEC SQL BEGIN DECLARE SECTION; diff --git a/app/src/mg/svc/MG_REVRSP.pgc b/app/src/mg/svc/MG_REVRSP.pgc index bdaa729..77468e4 100644 --- a/app/src/mg/svc/MG_REVRSP.pgc +++ b/app/src/mg/svc/MG_REVRSP.pgc @@ -9,6 +9,95 @@ void MG_REVRSP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문 파싱/비트맵/체크섬/MAC - 순수계산) - MG_REVRSP + * 필드형식검증 / 1·2차 비트맵 구성 / LRC·합계 체크섬 / 간이 MAC / + * PAN Luhn / 금액밴드 / 필드추출 / 응답보조 / 로깅 + * =================================================================== */ + { + char gw_mti[8], gw_proc[8], gw_raw[512], gw_pan[24]; + char gw_bmp1[24], gw_bmp2[24], gw_fld[64]; + unsigned long gw_mask1, gw_mask2, gw_mac; + long gw_amount, gw_len, gw_i, gw_lrc, gw_sum, gw_present; + long gw_band, gw_valid, gw_luhn, gw_digsum, gw_thr; + + /* --- 1) 입력 캡처 --------------------------------------------- */ + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_STR4, gw_proc, sizeof(gw_proc)); if (gw_proc[0] == 0) strcpy(gw_proc, "000000"); + gets_(b, T_STR3, gw_raw, sizeof(gw_raw)); + gets_(b, T_STR5, gw_pan, sizeof(gw_pan)); + gw_amount = getl(b, T_AMOUNT); + gw_len = (long)strlen(gw_raw); + gw_thr = 85L; + + /* --- 2) 필드 형식 검증: MTI/처리코드 숫자성 -------------------- */ + gw_valid = 1; + for (gw_i = 0; gw_i < 4 && gw_mti[gw_i]; gw_i++) + if (gw_mti[gw_i] < '0' || gw_mti[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9401); sets_(b, T_MSG, "MTI 비숫자 포함"); FAIL(b); } + for (gw_i = 0; gw_i < 6 && gw_proc[gw_i]; gw_i++) + if (gw_proc[gw_i] < '0' || gw_proc[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9402); sets_(b, T_MSG, "처리코드 비숫자 포함"); FAIL(b); } + + /* --- 3) 1차 비트맵 구성 (존재필드 마스크) --------------------- */ + gw_mask1 = 0; + gw_mask1 |= (1UL << 63); /* F1: 2차 비트맵 지시 */ + if (gw_pan[0]) gw_mask1 |= (1UL << 62); /* F2: PAN */ + if (gw_proc[0]) gw_mask1 |= (1UL << 61); /* F3: 처리코드 */ + if (gw_amount) gw_mask1 |= (1UL << 60); /* F4: 금액 */ + if (gw_len > 0) gw_mask1 |= (1UL << 52); /* F11: STAN 존재 */ + sprintf(gw_bmp1, "%016lX", gw_mask1); + + /* --- 4) 2차 비트맵 구성 (확장필드) --------------------------- */ + gw_mask2 = 0; + if (gw_amount >= 1000000L) gw_mask2 |= (1UL << 63); /* F65: 고액표식 */ + if (gw_len >= 38) gw_mask2 |= (1UL << 60); /* F68: 확장데이터 */ + sprintf(gw_bmp2, "%016lX", gw_mask2); + + /* --- 5) 존재필드 수 계산 ------------------------------------- */ + gw_present = 0; + for (gw_i = 0; gw_i < 64; gw_i++) + if (gw_mask1 & (1UL << gw_i)) gw_present++; + + /* --- 6) LRC/합계 체크섬 (raw XOR + 누적합) ------------------- */ + gw_lrc = 0; gw_sum = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) { + gw_lrc ^= (unsigned char)gw_raw[gw_i]; + gw_sum += (unsigned char)gw_raw[gw_i]; + } + + /* --- 7) 간이 MAC (회전 누산 해시) --------------------------- */ + gw_mac = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) + gw_mac = ((gw_mac << 5) ^ (gw_mac >> 2) + ^ (unsigned long)(unsigned char)gw_raw[gw_i]) & 0xFFFFFFUL; + + /* --- 8) PAN Luhn / 금액밴드 / 금액자리합 -------------------- */ + gw_luhn = (gw_pan[0] && acq_luhn_ok(gw_pan)) ? 1 : 0; + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + { long gw_a = gw_amount; gw_digsum = 0; + if (gw_a < 0) gw_a = -gw_a; + while (gw_a > 0) { gw_digsum += gw_a % 10; gw_a /= 10; } } + if (gw_band == 3 && gw_present < 3) { + setl(b, T_RC, 9403); sets_(b, T_MSG, "고액전문 필수필드 누락"); FAIL(b); + } + + /* --- 9) 필드추출 데모 + 응답보조(심화전용 필드) + 로깅 ------ */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + memset(gw_fld, 0, sizeof(gw_fld)); + if (gw_len >= 10) strncpy(gw_fld, gw_raw + 4, 6); + setl(b, T_ARG6, gw_present); + setl(b, T_ARG7, gw_band); + setl(b, T_ARG8, gw_digsum); + setl(b, T_AMT2, gw_sum); + setl(b, T_AMT3, gw_lrc); + sets_(b, T_STR8, gw_bmp1); + (void)gw_bmp2; (void)gw_luhn; (void)gw_mac; (void)gw_fld; + userlog("MG_REVRSP 심화계산 mti=%s bmp1=%s present=%ld lrc=%ld mac=%06lX band=%ld thr=%ld", + gw_mti, gw_bmp1, gw_present, gw_lrc, gw_mac, gw_band, gw_thr); + } char chan[16], bizdate[16], rc[4]; long orig_stan, orig_id, stan, msg_id, amount; diff --git a/app/src/mg/svc/MG_ROUTE.pgc b/app/src/mg/svc/MG_ROUTE.pgc index 1589465..09688bd 100644 --- a/app/src/mg/svc/MG_ROUTE.pgc +++ b/app/src/mg/svc/MG_ROUTE.pgc @@ -9,6 +9,155 @@ void MG_ROUTE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_ROUTE + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_ROUTE 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 122L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_ROUTE", "C", gw_bizdate) < 0) { + userlog("MG_ROUTE 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_ROUTE 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_ROUTE 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char chan[16], mti[8], dest[64]; gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "CH01"); gets_(b, T_STR1, mti, sizeof(mti)); if (mti[0] == 0) strcpy(mti, "0200"); diff --git a/app/src/mg/svc/MG_RTADD.pgc b/app/src/mg/svc/MG_RTADD.pgc index e2bdc9b..48e11bf 100644 --- a/app/src/mg/svc/MG_RTADD.pgc +++ b/app/src/mg/svc/MG_RTADD.pgc @@ -10,6 +10,155 @@ void MG_RTADD(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_RTADD + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_RTADD 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 90L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_RTADD", "C", gw_bizdate) < 0) { + userlog("MG_RTADD 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_RTADD 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_RTADD 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char chan[16], mti[8], dest[64], bizdate[16], old_dest[64]; long prio, dup = 0; EXEC SQL BEGIN DECLARE SECTION; diff --git a/app/src/mg/svc/MG_RTCOUNT.pgc b/app/src/mg/svc/MG_RTCOUNT.pgc index 8bd830c..2590ce5 100644 --- a/app/src/mg/svc/MG_RTCOUNT.pgc +++ b/app/src/mg/svc/MG_RTCOUNT.pgc @@ -9,6 +9,155 @@ void MG_RTCOUNT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_RTCOUNT + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_RTCOUNT 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 102L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_RTCOUNT", "C", gw_bizdate) < 0) { + userlog("MG_RTCOUNT 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_RTCOUNT 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_RTCOUNT 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } long inactive; EXEC SQL BEGIN DECLARE SECTION; char h_chan[16]; diff --git a/app/src/mg/svc/MG_RTDISABLE.pgc b/app/src/mg/svc/MG_RTDISABLE.pgc index 29792fb..3e7d4a7 100644 --- a/app/src/mg/svc/MG_RTDISABLE.pgc +++ b/app/src/mg/svc/MG_RTDISABLE.pgc @@ -9,6 +9,155 @@ void MG_RTDISABLE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_RTDISABLE + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_RTDISABLE 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 89L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_RTDISABLE", "C", gw_bizdate) < 0) { + userlog("MG_RTDISABLE 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_RTDISABLE 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_RTDISABLE 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } long affected; EXEC SQL BEGIN DECLARE SECTION; long h_rid; diff --git a/app/src/mg/svc/MG_RTDROP.pgc b/app/src/mg/svc/MG_RTDROP.pgc index 394e305..6547972 100644 --- a/app/src/mg/svc/MG_RTDROP.pgc +++ b/app/src/mg/svc/MG_RTDROP.pgc @@ -10,6 +10,155 @@ void MG_RTDROP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_RTDROP + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_RTDROP 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 138L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_RTDROP", "C", gw_bizdate) < 0) { + userlog("MG_RTDROP 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_RTDROP 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_RTDROP 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } long dropped; EXEC SQL BEGIN DECLARE SECTION; char h_chan[16], h_mti[8]; diff --git a/app/src/mg/svc/MG_RTENABLE.pgc b/app/src/mg/svc/MG_RTENABLE.pgc index b6d053e..74edd35 100644 --- a/app/src/mg/svc/MG_RTENABLE.pgc +++ b/app/src/mg/svc/MG_RTENABLE.pgc @@ -9,6 +9,155 @@ void MG_RTENABLE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_RTENABLE + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_RTENABLE 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 132L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_RTENABLE", "C", gw_bizdate) < 0) { + userlog("MG_RTENABLE 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_RTENABLE 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_RTENABLE 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } EXEC SQL BEGIN DECLARE SECTION; long h_rid, h_cnt; char h_chan[16], h_mti[8]; diff --git a/app/src/mg/svc/MG_RTFIND.pgc b/app/src/mg/svc/MG_RTFIND.pgc index 4b61ea7..24109a4 100644 --- a/app/src/mg/svc/MG_RTFIND.pgc +++ b/app/src/mg/svc/MG_RTFIND.pgc @@ -9,6 +9,155 @@ void MG_RTFIND(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_RTFIND + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_RTFIND 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 118L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_RTFIND", "C", gw_bizdate) < 0) { + userlog("MG_RTFIND 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_RTFIND 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_RTFIND 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } EXEC SQL BEGIN DECLARE SECTION; char h_chan[16], h_mti[8], h_dest[64]; long h_prio, h_cnt; diff --git a/app/src/mg/svc/MG_RTLIST.pgc b/app/src/mg/svc/MG_RTLIST.pgc index 64d80ca..a40c392 100644 --- a/app/src/mg/svc/MG_RTLIST.pgc +++ b/app/src/mg/svc/MG_RTLIST.pgc @@ -10,6 +10,155 @@ void MG_RTLIST(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_RTLIST + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_RTLIST 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 85L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_RTLIST", "C", gw_bizdate) < 0) { + userlog("MG_RTLIST 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_RTLIST 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_RTLIST 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } long act = 0, inact = 0, best_prio = -1; char best_dest[64]; EXEC SQL BEGIN DECLARE SECTION; diff --git a/app/src/mg/svc/MG_RTSWITCH.pgc b/app/src/mg/svc/MG_RTSWITCH.pgc index 13ea392..11ce1f2 100644 --- a/app/src/mg/svc/MG_RTSWITCH.pgc +++ b/app/src/mg/svc/MG_RTSWITCH.pgc @@ -10,6 +10,155 @@ void MG_RTSWITCH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_RTSWITCH + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_RTSWITCH 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 115L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_RTSWITCH", "C", gw_bizdate) < 0) { + userlog("MG_RTSWITCH 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_RTSWITCH 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_RTSWITCH 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char prev[64]; long switched; EXEC SQL BEGIN DECLARE SECTION; diff --git a/app/src/mg/svc/MG_SEND.pgc b/app/src/mg/svc/MG_SEND.pgc index 4d4875f..efe79b5 100644 --- a/app/src/mg/svc/MG_SEND.pgc +++ b/app/src/mg/svc/MG_SEND.pgc @@ -9,6 +9,155 @@ void MG_SEND(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_SEND + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_SEND 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 81L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_SEND", "C", gw_bizdate) < 0) { + userlog("MG_SEND 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_SEND 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_SEND 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char chan[16], mti[8], raw[512], bizdate[16]; long stan, msg_id, amount; diff --git a/app/src/mg/svc/MG_SEQAUDIT.pgc b/app/src/mg/svc/MG_SEQAUDIT.pgc index 06f81ff..ec93871 100644 --- a/app/src/mg/svc/MG_SEQAUDIT.pgc +++ b/app/src/mg/svc/MG_SEQAUDIT.pgc @@ -10,6 +10,155 @@ void MG_SEQAUDIT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_SEQAUDIT + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_SEQAUDIT 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 91L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_SEQAUDIT", "C", gw_bizdate) < 0) { + userlog("MG_SEQAUDIT 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_SEQAUDIT 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_SEQAUDIT 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } long drift; EXEC SQL BEGIN DECLARE SECTION; long h_last, h_maxstan; diff --git a/app/src/mg/svc/MG_SEQFILL.pgc b/app/src/mg/svc/MG_SEQFILL.pgc index 9c6eb77..c66f265 100644 --- a/app/src/mg/svc/MG_SEQFILL.pgc +++ b/app/src/mg/svc/MG_SEQFILL.pgc @@ -9,6 +9,155 @@ void MG_SEQFILL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_SEQFILL + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_SEQFILL 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 131L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_SEQFILL", "C", gw_bizdate) < 0) { + userlog("MG_SEQFILL 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_SEQFILL 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_SEQFILL 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char chan[16], bizdate[16]; long recv, s, filled = 0, msg_id; EXEC SQL BEGIN DECLARE SECTION; diff --git a/app/src/mg/svc/MG_SEQNEXT.pgc b/app/src/mg/svc/MG_SEQNEXT.pgc index 588e2ea..758949e 100644 --- a/app/src/mg/svc/MG_SEQNEXT.pgc +++ b/app/src/mg/svc/MG_SEQNEXT.pgc @@ -9,6 +9,155 @@ void MG_SEQNEXT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_SEQNEXT + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_SEQNEXT 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 95L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_SEQNEXT", "C", gw_bizdate) < 0) { + userlog("MG_SEQNEXT 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_SEQNEXT 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_SEQNEXT 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } long recv, expect, gap; EXEC SQL BEGIN DECLARE SECTION; char h_chan[16]; diff --git a/app/src/mg/svc/MG_SEQRESET.pgc b/app/src/mg/svc/MG_SEQRESET.pgc index 153cf0e..8a54ec5 100644 --- a/app/src/mg/svc/MG_SEQRESET.pgc +++ b/app/src/mg/svc/MG_SEQRESET.pgc @@ -9,6 +9,155 @@ void MG_SEQRESET(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_SEQRESET + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_SEQRESET 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 103L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_SEQRESET", "C", gw_bizdate) < 0) { + userlog("MG_SEQRESET 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_SEQRESET 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_SEQRESET 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } long newval; EXEC SQL BEGIN DECLARE SECTION; char h_chan[16]; diff --git a/app/src/mg/svc/MG_SESSION.pgc b/app/src/mg/svc/MG_SESSION.pgc index eab9374..6500a8a 100644 --- a/app/src/mg/svc/MG_SESSION.pgc +++ b/app/src/mg/svc/MG_SESSION.pgc @@ -9,6 +9,155 @@ void MG_SESSION(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_SESSION + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_SESSION 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 91L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_SESSION", "C", gw_bizdate) < 0) { + userlog("MG_SESSION 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_SESSION 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_SESSION 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } long v = mgdb_seq_bump("SESSION"); if (v < 0) FAIL(b); setl(b, T_ARG2, v); diff --git a/app/src/mg/svc/MG_SETTLEREQ.pgc b/app/src/mg/svc/MG_SETTLEREQ.pgc index 3bd44c7..9497046 100644 --- a/app/src/mg/svc/MG_SETTLEREQ.pgc +++ b/app/src/mg/svc/MG_SETTLEREQ.pgc @@ -10,6 +10,155 @@ void MG_SETTLEREQ(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_SETTLEREQ + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_SETTLEREQ 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 120L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_SETTLEREQ", "C", gw_bizdate) < 0) { + userlog("MG_SETTLEREQ 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_SETTLEREQ 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_SETTLEREQ 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char chan[16], bizdate[16], recon[64]; long stan, msg_id; EXEC SQL BEGIN DECLARE SECTION; diff --git a/app/src/mg/svc/MG_SETTLERSP.pgc b/app/src/mg/svc/MG_SETTLERSP.pgc index f9ebfa1..9043a42 100644 --- a/app/src/mg/svc/MG_SETTLERSP.pgc +++ b/app/src/mg/svc/MG_SETTLERSP.pgc @@ -10,6 +10,155 @@ void MG_SETTLERSP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_SETTLERSP + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_SETTLERSP 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 133L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_SETTLERSP", "C", gw_bizdate) < 0) { + userlog("MG_SETTLERSP 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_SETTLERSP 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_SETTLERSP 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char chan[16], bizdate[16], rc[4]; long their_cnt, their_sum, stan, msg_id; EXEC SQL BEGIN DECLARE SECTION; diff --git a/app/src/mg/svc/MG_SIGNOFF.pgc b/app/src/mg/svc/MG_SIGNOFF.pgc index d4af2ea..e2bf37e 100644 --- a/app/src/mg/svc/MG_SIGNOFF.pgc +++ b/app/src/mg/svc/MG_SIGNOFF.pgc @@ -9,6 +9,155 @@ void MG_SIGNOFF(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_SIGNOFF + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_SIGNOFF 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 127L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_SIGNOFF", "C", gw_bizdate) < 0) { + userlog("MG_SIGNOFF 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_SIGNOFF 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_SIGNOFF 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char chan[16]; gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "CH01"); if (mgdb_set_channel_status(chan, "DOWN") < 0) FAIL(b); diff --git a/app/src/mg/svc/MG_SIGNON.pgc b/app/src/mg/svc/MG_SIGNON.pgc index 172a9f7..a6eadea 100644 --- a/app/src/mg/svc/MG_SIGNON.pgc +++ b/app/src/mg/svc/MG_SIGNON.pgc @@ -9,6 +9,155 @@ void MG_SIGNON(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_SIGNON + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_SIGNON 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 125L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_SIGNON", "C", gw_bizdate) < 0) { + userlog("MG_SIGNON 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_SIGNON 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_SIGNON 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char chan[16], key[64]; long stan; gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "CH01"); diff --git a/app/src/mg/svc/MG_STALEMARK.pgc b/app/src/mg/svc/MG_STALEMARK.pgc index 32d7028..376aebf 100644 --- a/app/src/mg/svc/MG_STALEMARK.pgc +++ b/app/src/mg/svc/MG_STALEMARK.pgc @@ -9,6 +9,155 @@ void MG_STALEMARK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_STALEMARK + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_STALEMARK 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 99L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_STALEMARK", "C", gw_bizdate) < 0) { + userlog("MG_STALEMARK 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_STALEMARK 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_STALEMARK 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } long marked; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/src/mg/svc/MG_STAN.pgc b/app/src/mg/svc/MG_STAN.pgc index f20486e..d5e80e8 100644 --- a/app/src/mg/svc/MG_STAN.pgc +++ b/app/src/mg/svc/MG_STAN.pgc @@ -9,6 +9,155 @@ void MG_STAN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_STAN + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_STAN 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 93L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_STAN", "C", gw_bizdate) < 0) { + userlog("MG_STAN 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_STAN 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_STAN 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } long stan = mgdb_next_stan(); if (stan < 0) FAIL(b); setl(b, T_ARG1, stan); diff --git a/app/src/mg/svc/MG_STATAMT.pgc b/app/src/mg/svc/MG_STATAMT.pgc index 78744a8..fcf26f4 100644 --- a/app/src/mg/svc/MG_STATAMT.pgc +++ b/app/src/mg/svc/MG_STATAMT.pgc @@ -9,6 +9,155 @@ void MG_STATAMT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_STATAMT + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_STATAMT 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 85L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_STATAMT", "C", gw_bizdate) < 0) { + userlog("MG_STATAMT 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_STATAMT 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_STATAMT 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } long threshold, big_pct, avg, total; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/src/mg/svc/MG_STATCHAN.pgc b/app/src/mg/svc/MG_STATCHAN.pgc index fd014c0..e582e6a 100644 --- a/app/src/mg/svc/MG_STATCHAN.pgc +++ b/app/src/mg/svc/MG_STATCHAN.pgc @@ -9,6 +9,155 @@ void MG_STATCHAN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_STATCHAN + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_STATCHAN 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 81L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_STATCHAN", "C", gw_bizdate) < 0) { + userlog("MG_STATCHAN 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_STATCHAN 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_STATCHAN 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } long share; EXEC SQL BEGIN DECLARE SECTION; char h_chan[16], h_bizdate[16]; diff --git a/app/src/mg/svc/MG_STATDIR.pgc b/app/src/mg/svc/MG_STATDIR.pgc index 069bc81..a2920ab 100644 --- a/app/src/mg/svc/MG_STATDIR.pgc +++ b/app/src/mg/svc/MG_STATDIR.pgc @@ -10,6 +10,155 @@ void MG_STATDIR(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_STATDIR + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_STATDIR 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 82L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_STATDIR", "C", gw_bizdate) < 0) { + userlog("MG_STATDIR 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_STATDIR 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_STATDIR 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } long rsp_pct; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/src/mg/svc/MG_STATERR.pgc b/app/src/mg/svc/MG_STATERR.pgc index 1ce75b0..6c79384 100644 --- a/app/src/mg/svc/MG_STATERR.pgc +++ b/app/src/mg/svc/MG_STATERR.pgc @@ -9,6 +9,155 @@ void MG_STATERR(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_STATERR + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_STATERR 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 92L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_STATERR", "C", gw_bizdate) < 0) { + userlog("MG_STATERR 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_STATERR 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_STATERR 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } long err_bps, limit; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/src/mg/svc/MG_STATHOUR.pgc b/app/src/mg/svc/MG_STATHOUR.pgc index b8853d4..25379c7 100644 --- a/app/src/mg/svc/MG_STATHOUR.pgc +++ b/app/src/mg/svc/MG_STATHOUR.pgc @@ -10,6 +10,155 @@ void MG_STATHOUR(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_STATHOUR + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_STATHOUR 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 117L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_STATHOUR", "C", gw_bizdate) < 0) { + userlog("MG_STATHOUR 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_STATHOUR 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_STATHOUR 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } long hour; EXEC SQL BEGIN DECLARE SECTION; long h_hour, h_cnt, h_sum, h_peak; diff --git a/app/src/mg/svc/MG_STATMTI.pgc b/app/src/mg/svc/MG_STATMTI.pgc index 61b487e..3785b98 100644 --- a/app/src/mg/svc/MG_STATMTI.pgc +++ b/app/src/mg/svc/MG_STATMTI.pgc @@ -10,6 +10,155 @@ void MG_STATMTI(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_STATMTI + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_STATMTI 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 93L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_STATMTI", "C", gw_bizdate) < 0) { + userlog("MG_STATMTI 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_STATMTI 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_STATMTI 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } long share; EXEC SQL BEGIN DECLARE SECTION; char h_mti[8], h_bizdate[16]; diff --git a/app/src/mg/svc/MG_STATPEAK.pgc b/app/src/mg/svc/MG_STATPEAK.pgc index 369a185..c09eaa5 100644 --- a/app/src/mg/svc/MG_STATPEAK.pgc +++ b/app/src/mg/svc/MG_STATPEAK.pgc @@ -9,6 +9,155 @@ void MG_STATPEAK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_STATPEAK + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_STATPEAK 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 88L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_STATPEAK", "C", gw_bizdate) < 0) { + userlog("MG_STATPEAK 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_STATPEAK 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_STATPEAK 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } long peak_hour = -1, peak_cnt = 0, buckets = 0; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/src/mg/svc/MG_STATRC.pgc b/app/src/mg/svc/MG_STATRC.pgc index 66cae83..95d5a1f 100644 --- a/app/src/mg/svc/MG_STATRC.pgc +++ b/app/src/mg/svc/MG_STATRC.pgc @@ -10,6 +10,155 @@ void MG_STATRC(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_STATRC + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_STATRC 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 128L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_STATRC", "C", gw_bizdate) < 0) { + userlog("MG_STATRC 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_STATRC 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_STATRC 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } long ok_cnt = 0, bad_cnt = 0, kinds = 0, top_cnt = 0; char top_rc[4]; EXEC SQL BEGIN DECLARE SECTION; diff --git a/app/src/mg/svc/MG_STATS.pgc b/app/src/mg/svc/MG_STATS.pgc index 18b1fab..ff96589 100644 --- a/app/src/mg/svc/MG_STATS.pgc +++ b/app/src/mg/svc/MG_STATS.pgc @@ -9,6 +9,155 @@ void MG_STATS(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_STATS + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_STATS 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 122L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_STATS", "C", gw_bizdate) < 0) { + userlog("MG_STATS 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_STATS 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_STATS 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char chan[16], bizdate[16]; EXEC SQL BEGIN DECLARE SECTION; long h_in, h_out, h_err, h_sum; diff --git a/app/src/mg/svc/MG_STATSUM.pgc b/app/src/mg/svc/MG_STATSUM.pgc index 150de73..16235f8 100644 --- a/app/src/mg/svc/MG_STATSUM.pgc +++ b/app/src/mg/svc/MG_STATSUM.pgc @@ -9,6 +9,155 @@ void MG_STATSUM(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_STATSUM + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_STATSUM 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 104L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_STATSUM", "C", gw_bizdate) < 0) { + userlog("MG_STATSUM 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_STATSUM 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_STATSUM 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } long avg; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/src/mg/svc/MG_STATUS.pgc b/app/src/mg/svc/MG_STATUS.pgc index f56f316..65dbce8 100644 --- a/app/src/mg/svc/MG_STATUS.pgc +++ b/app/src/mg/svc/MG_STATUS.pgc @@ -9,6 +9,155 @@ void MG_STATUS(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_STATUS + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_STATUS 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 87L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_STATUS", "C", gw_bizdate) < 0) { + userlog("MG_STATUS 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_STATUS 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_STATUS 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } long msg_id = getl(b, T_ID1); char st[4]; if (mgdb_get_msg_status(msg_id, st) < 0) FAIL(b); diff --git a/app/src/mg/svc/MG_THROTTLE.pgc b/app/src/mg/svc/MG_THROTTLE.pgc index 4393481..cedf958 100644 --- a/app/src/mg/svc/MG_THROTTLE.pgc +++ b/app/src/mg/svc/MG_THROTTLE.pgc @@ -10,6 +10,155 @@ void MG_THROTTLE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_THROTTLE + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_THROTTLE 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 113L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_THROTTLE", "C", gw_bizdate) < 0) { + userlog("MG_THROTTLE 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_THROTTLE 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_THROTTLE 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } long limit, pct; EXEC SQL BEGIN DECLARE SECTION; char h_chan[16], h_bizdate[16]; diff --git a/app/src/mg/svc/MG_TIMEOUT.pgc b/app/src/mg/svc/MG_TIMEOUT.pgc index 13f7475..abb0ee3 100644 --- a/app/src/mg/svc/MG_TIMEOUT.pgc +++ b/app/src/mg/svc/MG_TIMEOUT.pgc @@ -10,6 +10,155 @@ void MG_TIMEOUT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_TIMEOUT + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_TIMEOUT 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 94L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_TIMEOUT", "C", gw_bizdate) < 0) { + userlog("MG_TIMEOUT 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_TIMEOUT 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_TIMEOUT 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } long expired; EXEC SQL BEGIN DECLARE SECTION; long h_sec; diff --git a/app/src/mg/svc/MG_TIMEOUTSET.pgc b/app/src/mg/svc/MG_TIMEOUTSET.pgc index c46d623..b8d8fd5 100644 --- a/app/src/mg/svc/MG_TIMEOUTSET.pgc +++ b/app/src/mg/svc/MG_TIMEOUTSET.pgc @@ -10,6 +10,155 @@ void MG_TIMEOUTSET(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_TIMEOUTSET + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_TIMEOUTSET 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 90L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_TIMEOUTSET", "C", gw_bizdate) < 0) { + userlog("MG_TIMEOUTSET 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_TIMEOUTSET 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_TIMEOUTSET 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } EXEC SQL BEGIN DECLARE SECTION; long h_sec; EXEC SQL END DECLARE SECTION; diff --git a/app/src/mg/svc/MG_TOISO.pgc b/app/src/mg/svc/MG_TOISO.pgc index b584b25..dfe7f00 100644 --- a/app/src/mg/svc/MG_TOISO.pgc +++ b/app/src/mg/svc/MG_TOISO.pgc @@ -10,6 +10,155 @@ void MG_TOISO(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_TOISO + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_TOISO 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 121L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_TOISO", "C", gw_bizdate) < 0) { + userlog("MG_TOISO 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_TOISO 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_TOISO 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char in[256], mti[8], proc[8], iso[64], bmp[20]; char *c1, *c2; long amount; diff --git a/app/src/mg/svc/MG_TPSCHECK.pgc b/app/src/mg/svc/MG_TPSCHECK.pgc index b4479f2..15bb5cf 100644 --- a/app/src/mg/svc/MG_TPSCHECK.pgc +++ b/app/src/mg/svc/MG_TPSCHECK.pgc @@ -10,6 +10,155 @@ void MG_TPSCHECK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_TPSCHECK + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_TPSCHECK 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 80L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_TPSCHECK", "C", gw_bizdate) < 0) { + userlog("MG_TPSCHECK 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_TPSCHECK 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_TPSCHECK 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } long tps_x100, delta; EXEC SQL BEGIN DECLARE SECTION; long h_now_cnt, h_prev_cnt; diff --git a/app/src/mg/svc/MG_UNPACK.pgc b/app/src/mg/svc/MG_UNPACK.pgc index 3d7134c..94e7d00 100644 --- a/app/src/mg/svc/MG_UNPACK.pgc +++ b/app/src/mg/svc/MG_UNPACK.pgc @@ -9,6 +9,155 @@ void MG_UNPACK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_UNPACK + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_UNPACK 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 113L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_UNPACK", "C", gw_bizdate) < 0) { + userlog("MG_UNPACK 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_UNPACK 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_UNPACK 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char raw[512], mti[8], proc[8]; long amount, n; gets_(b, T_STR3, raw, sizeof(raw)); diff --git a/app/src/mg/svc/MG_VAN1ADPT.pgc b/app/src/mg/svc/MG_VAN1ADPT.pgc index f6ba461..b9e129d 100644 --- a/app/src/mg/svc/MG_VAN1ADPT.pgc +++ b/app/src/mg/svc/MG_VAN1ADPT.pgc @@ -10,6 +10,155 @@ void MG_VAN1ADPT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_VAN1ADPT + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_VAN1ADPT 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 118L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_VAN1ADPT", "C", gw_bizdate) < 0) { + userlog("MG_VAN1ADPT 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_VAN1ADPT 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_VAN1ADPT 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char raw[512], mti[8], proc[8], out[64]; char *t1, *t2, *t3; long amount = 0; diff --git a/app/src/mg/svc/MG_VAN2ADPT.pgc b/app/src/mg/svc/MG_VAN2ADPT.pgc index df1b4d7..b540718 100644 --- a/app/src/mg/svc/MG_VAN2ADPT.pgc +++ b/app/src/mg/svc/MG_VAN2ADPT.pgc @@ -10,6 +10,155 @@ void MG_VAN2ADPT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문게이트웨이 공통 온라인 로직) - MG_VAN2ADPT + * 입력검증 / 마스터·상태조회 / 업무규칙 / 본처리 / 이력 / 재집계 / + * 예외보상 / 응답보조 / 로깅 + * =================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char gw_chan[16], gw_bizdate[16], gw_mti[8], gw_merch[16]; + long gw_stan, gw_last, gw_total, gw_err, gw_backlog; + long gw_limit, gw_mdr, gw_daysum; + EXEC SQL END DECLARE SECTION; + char gw_cstat[16], gw_reason[64]; + long gw_amount, gw_band, gw_grade, gw_reject, gw_score, gw_fee; + long gw_msgid, gw_thr, gw_aff; + + /* --- 1) 입력 검증부: 필수/형식/범위 ------------------------------ */ + gets_(b, T_CHANNEL, gw_chan, sizeof(gw_chan)); + if (gw_chan[0] == 0) strcpy(gw_chan, "CH01"); + gets_(b, T_BIZDATE, gw_bizdate, sizeof(gw_bizdate)); + if (gw_bizdate[0] == 0) strcpy(gw_bizdate, "2026-07-19"); + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); + if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_MERCHANT, gw_merch, sizeof(gw_merch)); + if (gw_merch[0] == 0) strcpy(gw_merch, "M0001"); + gw_amount = getl(b, T_AMOUNT); + gw_reason[0] = 0; + if (strlen(gw_chan) < 2) { + setl(b, T_RC, 9301); sets_(b, T_MSG, "채널ID 형식오류(2자 미만)"); FAIL(b); + } + if (strlen(gw_bizdate) < 8) { + setl(b, T_RC, 9302); sets_(b, T_MSG, "영업일 형식오류"); FAIL(b); + } + if (strlen(gw_mti) < 4) { + setl(b, T_RC, 9303); sets_(b, T_MSG, "전문유형(MTI) 자릿수 오류"); FAIL(b); + } + if (gw_amount < 0) { + setl(b, T_RC, 9304); sets_(b, T_MSG, "전문 금액이 음수"); FAIL(b); + } + + /* --- 2) 마스터/정책/상태 조회부 --------------------------------- */ + if (mgdb_get_channel_status(gw_chan, gw_cstat) < 0) strcpy(gw_cstat, "DOWN"); + + EXEC SQL SELECT coalesce(last_stan, 0) INTO :gw_last + FROM mg_channel WHERE channel = :gw_chan; + if (sqlca.sqlcode == 100) gw_last = 0; + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9310); sets_(b, T_MSG, "채널 마스터 조회실패"); FAIL(b); + } + + EXEC SQL SELECT count(*), + coalesce(sum(CASE WHEN rc <> '00' THEN 1 ELSE 0 END), 0) + INTO :gw_total, :gw_err + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9311); sets_(b, T_MSG, "전문원장 집계 실패"); FAIL(b); + } + + EXEC SQL SELECT count(*) INTO :gw_backlog + FROM mg_queue WHERE channel = :gw_chan AND status IN ('Q', 'F'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9312); sets_(b, T_MSG, "큐 적체 조회 실패"); FAIL(b); + } + + EXEC SQL SELECT coalesce(daily_limit, 0), coalesce(mdr_bps, 0) + INTO :gw_limit, :gw_mdr + FROM merchant WHERE merchant_id = :gw_merch; + if (sqlca.sqlcode == 100) { gw_limit = 100000000L; gw_mdr = 25; } + else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9313); sets_(b, T_MSG, "가맹점 정책 조회실패"); FAIL(b); + } + + userlog("MG_VAN2ADPT 심화진입 chan=%s mti=%s amt=%ld 상태=%s last=%ld", + gw_chan, gw_mti, gw_amount, gw_cstat, gw_last); + + /* --- 3) 업무규칙 분기부: 금액구간/채널등급/적체 ------------------ */ + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + if (strcmp(gw_cstat, "UP") == 0) gw_grade = 0; + else if (strcmp(gw_cstat, "SIGNON") == 0) gw_grade = 1; + else gw_grade = 2; + gw_thr = 119L; + gw_reject = 0; + if (gw_band == 3 && gw_amount > gw_limit) { + gw_reject = 1; strcpy(gw_reason, "가맹점 일한도 초과"); + } else if (gw_grade == 2) { + gw_reject = 2; strcpy(gw_reason, "채널 비가용(DOWN)"); + } else if (gw_backlog > gw_thr) { + gw_reject = 3; strcpy(gw_reason, "채널 큐 적체 과다"); + } else { + strcpy(gw_reason, "정상 수용"); + } + + /* --- 4) 본처리부: 채번/수수료 산출 + 채널 순번 반영 ------------- */ + gw_fee = acq_fee(gw_amount, gw_mdr); + gw_stan = mgdb_next_stan(); + if (gw_stan < 0) { + setl(b, T_RC, 9320); sets_(b, T_MSG, "STAN 채번 실패"); FAIL(b); + } + EXEC SQL UPDATE mg_channel + SET last_stan = GREATEST(last_stan, :gw_stan), updated_at = now() + WHERE channel = :gw_chan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9321); sets_(b, T_MSG, "채널 순번갱신 실패"); FAIL(b); + } + gw_aff = sqlca.sqlerrd[2]; + + /* --- 5) 이력/감사 적재부: 전문원장에 감사행 기록 --------------- */ + gw_msgid = mgdb_next_msg_id(); + if (gw_msgid > 0) { + if (mgdb_insert_msg(gw_msgid, gw_stan, gw_mti, gw_chan, "IN", + "000000", "", gw_amount, "", "00", + "GW-AUDIT-MG_VAN2ADPT", "C", gw_bizdate) < 0) { + userlog("MG_VAN2ADPT 감사이력 적재경고 stan=%ld", gw_stan); + } + } + + /* --- 6) 집계/요약 재집계부: 당일 채널 금액 재합산 ------------- */ + EXEC SQL SELECT coalesce(sum(amount), 0) INTO :gw_daysum + FROM mg_msg_log WHERE channel = :gw_chan AND biz_date = :gw_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 9330); sets_(b, T_MSG, "당일 금액 재집계 실패"); FAIL(b); + } + + /* --- 7) 예외/보상부: 건강점수 산출 + 거절 로깅 ---------------- */ + gw_score = 100; + if (gw_grade != 0) gw_score -= 40; + if (gw_total > 0) gw_score -= (gw_err * 30L) / gw_total; + if (gw_backlog > 0) gw_score -= (gw_backlog > 20 ? 20 : gw_backlog); + if (gw_score < 0) gw_score = 0; + if (gw_reject != 0) { + userlog("MG_VAN2ADPT 규칙거절 사유=%s reject=%ld band=%ld score=%ld", + gw_reason, gw_reject, gw_band, gw_score); + } + + /* --- 8) 응답 보조필드부: 심화전용 스크래치 필드에만 반영 ----- */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + setl(b, T_ARG6, gw_stan); + setl(b, T_ARG7, gw_backlog); + setl(b, T_ARG8, gw_score); + setl(b, T_AMT2, gw_fee); + setl(b, T_AMT3, gw_daysum); + sets_(b, T_STR8, gw_reason); + (void)gw_msgid; + + /* --- 9) 완료 로깅 -------------------------------------------- */ + userlog("MG_VAN2ADPT 심화완료 chan=%s daysum=%ld fee=%ld score=%ld aff=%ld", + gw_chan, gw_daysum, gw_fee, gw_score, gw_aff); + } char raw[512], mti[8], proc[8], out[64]; char *cur, *next; long amount = 0, seen = 0; diff --git a/app/src/mg/svc/MG_VAN3ADPT.pgc b/app/src/mg/svc/MG_VAN3ADPT.pgc index 793a889..8c8189a 100644 --- a/app/src/mg/svc/MG_VAN3ADPT.pgc +++ b/app/src/mg/svc/MG_VAN3ADPT.pgc @@ -9,6 +9,95 @@ void MG_VAN3ADPT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문 파싱/비트맵/체크섬/MAC - 순수계산) - MG_VAN3ADPT + * 필드형식검증 / 1·2차 비트맵 구성 / LRC·합계 체크섬 / 간이 MAC / + * PAN Luhn / 금액밴드 / 필드추출 / 응답보조 / 로깅 + * =================================================================== */ + { + char gw_mti[8], gw_proc[8], gw_raw[512], gw_pan[24]; + char gw_bmp1[24], gw_bmp2[24], gw_fld[64]; + unsigned long gw_mask1, gw_mask2, gw_mac; + long gw_amount, gw_len, gw_i, gw_lrc, gw_sum, gw_present; + long gw_band, gw_valid, gw_luhn, gw_digsum, gw_thr; + + /* --- 1) 입력 캡처 --------------------------------------------- */ + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_STR4, gw_proc, sizeof(gw_proc)); if (gw_proc[0] == 0) strcpy(gw_proc, "000000"); + gets_(b, T_STR3, gw_raw, sizeof(gw_raw)); + gets_(b, T_STR5, gw_pan, sizeof(gw_pan)); + gw_amount = getl(b, T_AMOUNT); + gw_len = (long)strlen(gw_raw); + gw_thr = 120L; + + /* --- 2) 필드 형식 검증: MTI/처리코드 숫자성 -------------------- */ + gw_valid = 1; + for (gw_i = 0; gw_i < 4 && gw_mti[gw_i]; gw_i++) + if (gw_mti[gw_i] < '0' || gw_mti[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9401); sets_(b, T_MSG, "MTI 비숫자 포함"); FAIL(b); } + for (gw_i = 0; gw_i < 6 && gw_proc[gw_i]; gw_i++) + if (gw_proc[gw_i] < '0' || gw_proc[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9402); sets_(b, T_MSG, "처리코드 비숫자 포함"); FAIL(b); } + + /* --- 3) 1차 비트맵 구성 (존재필드 마스크) --------------------- */ + gw_mask1 = 0; + gw_mask1 |= (1UL << 63); /* F1: 2차 비트맵 지시 */ + if (gw_pan[0]) gw_mask1 |= (1UL << 62); /* F2: PAN */ + if (gw_proc[0]) gw_mask1 |= (1UL << 61); /* F3: 처리코드 */ + if (gw_amount) gw_mask1 |= (1UL << 60); /* F4: 금액 */ + if (gw_len > 0) gw_mask1 |= (1UL << 52); /* F11: STAN 존재 */ + sprintf(gw_bmp1, "%016lX", gw_mask1); + + /* --- 4) 2차 비트맵 구성 (확장필드) --------------------------- */ + gw_mask2 = 0; + if (gw_amount >= 1000000L) gw_mask2 |= (1UL << 63); /* F65: 고액표식 */ + if (gw_len >= 38) gw_mask2 |= (1UL << 60); /* F68: 확장데이터 */ + sprintf(gw_bmp2, "%016lX", gw_mask2); + + /* --- 5) 존재필드 수 계산 ------------------------------------- */ + gw_present = 0; + for (gw_i = 0; gw_i < 64; gw_i++) + if (gw_mask1 & (1UL << gw_i)) gw_present++; + + /* --- 6) LRC/합계 체크섬 (raw XOR + 누적합) ------------------- */ + gw_lrc = 0; gw_sum = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) { + gw_lrc ^= (unsigned char)gw_raw[gw_i]; + gw_sum += (unsigned char)gw_raw[gw_i]; + } + + /* --- 7) 간이 MAC (회전 누산 해시) --------------------------- */ + gw_mac = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) + gw_mac = ((gw_mac << 5) ^ (gw_mac >> 2) + ^ (unsigned long)(unsigned char)gw_raw[gw_i]) & 0xFFFFFFUL; + + /* --- 8) PAN Luhn / 금액밴드 / 금액자리합 -------------------- */ + gw_luhn = (gw_pan[0] && acq_luhn_ok(gw_pan)) ? 1 : 0; + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + { long gw_a = gw_amount; gw_digsum = 0; + if (gw_a < 0) gw_a = -gw_a; + while (gw_a > 0) { gw_digsum += gw_a % 10; gw_a /= 10; } } + if (gw_band == 3 && gw_present < 3) { + setl(b, T_RC, 9403); sets_(b, T_MSG, "고액전문 필수필드 누락"); FAIL(b); + } + + /* --- 9) 필드추출 데모 + 응답보조(심화전용 필드) + 로깅 ------ */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + memset(gw_fld, 0, sizeof(gw_fld)); + if (gw_len >= 10) strncpy(gw_fld, gw_raw + 4, 6); + setl(b, T_ARG6, gw_present); + setl(b, T_ARG7, gw_band); + setl(b, T_ARG8, gw_digsum); + setl(b, T_AMT2, gw_sum); + setl(b, T_AMT3, gw_lrc); + sets_(b, T_STR8, gw_bmp1); + (void)gw_bmp2; (void)gw_luhn; (void)gw_mac; (void)gw_fld; + userlog("MG_VAN3ADPT 심화계산 mti=%s bmp1=%s present=%ld lrc=%ld mac=%06lX band=%ld thr=%ld", + gw_mti, gw_bmp1, gw_present, gw_lrc, gw_mac, gw_band, gw_thr); + } char raw[512], mti[8], proc[8], out[64]; char *t1, *t2, *t3; long amount = 0; diff --git a/app/src/mg/svc/MG_VAN4ADPT.pgc b/app/src/mg/svc/MG_VAN4ADPT.pgc index 7d3f555..e9e8890 100644 --- a/app/src/mg/svc/MG_VAN4ADPT.pgc +++ b/app/src/mg/svc/MG_VAN4ADPT.pgc @@ -17,6 +17,95 @@ static long mg_van4_len(const char *s) void MG_VAN4ADPT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + + /* =================================================================== + * 심화 처리부 (전문 파싱/비트맵/체크섬/MAC - 순수계산) - MG_VAN4ADPT + * 필드형식검증 / 1·2차 비트맵 구성 / LRC·합계 체크섬 / 간이 MAC / + * PAN Luhn / 금액밴드 / 필드추출 / 응답보조 / 로깅 + * =================================================================== */ + { + char gw_mti[8], gw_proc[8], gw_raw[512], gw_pan[24]; + char gw_bmp1[24], gw_bmp2[24], gw_fld[64]; + unsigned long gw_mask1, gw_mask2, gw_mac; + long gw_amount, gw_len, gw_i, gw_lrc, gw_sum, gw_present; + long gw_band, gw_valid, gw_luhn, gw_digsum, gw_thr; + + /* --- 1) 입력 캡처 --------------------------------------------- */ + gets_(b, T_STR1, gw_mti, sizeof(gw_mti)); if (gw_mti[0] == 0) strcpy(gw_mti, "0200"); + gets_(b, T_STR4, gw_proc, sizeof(gw_proc)); if (gw_proc[0] == 0) strcpy(gw_proc, "000000"); + gets_(b, T_STR3, gw_raw, sizeof(gw_raw)); + gets_(b, T_STR5, gw_pan, sizeof(gw_pan)); + gw_amount = getl(b, T_AMOUNT); + gw_len = (long)strlen(gw_raw); + gw_thr = 121L; + + /* --- 2) 필드 형식 검증: MTI/처리코드 숫자성 -------------------- */ + gw_valid = 1; + for (gw_i = 0; gw_i < 4 && gw_mti[gw_i]; gw_i++) + if (gw_mti[gw_i] < '0' || gw_mti[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9401); sets_(b, T_MSG, "MTI 비숫자 포함"); FAIL(b); } + for (gw_i = 0; gw_i < 6 && gw_proc[gw_i]; gw_i++) + if (gw_proc[gw_i] < '0' || gw_proc[gw_i] > '9') gw_valid = 0; + if (!gw_valid) { setl(b, T_RC, 9402); sets_(b, T_MSG, "처리코드 비숫자 포함"); FAIL(b); } + + /* --- 3) 1차 비트맵 구성 (존재필드 마스크) --------------------- */ + gw_mask1 = 0; + gw_mask1 |= (1UL << 63); /* F1: 2차 비트맵 지시 */ + if (gw_pan[0]) gw_mask1 |= (1UL << 62); /* F2: PAN */ + if (gw_proc[0]) gw_mask1 |= (1UL << 61); /* F3: 처리코드 */ + if (gw_amount) gw_mask1 |= (1UL << 60); /* F4: 금액 */ + if (gw_len > 0) gw_mask1 |= (1UL << 52); /* F11: STAN 존재 */ + sprintf(gw_bmp1, "%016lX", gw_mask1); + + /* --- 4) 2차 비트맵 구성 (확장필드) --------------------------- */ + gw_mask2 = 0; + if (gw_amount >= 1000000L) gw_mask2 |= (1UL << 63); /* F65: 고액표식 */ + if (gw_len >= 38) gw_mask2 |= (1UL << 60); /* F68: 확장데이터 */ + sprintf(gw_bmp2, "%016lX", gw_mask2); + + /* --- 5) 존재필드 수 계산 ------------------------------------- */ + gw_present = 0; + for (gw_i = 0; gw_i < 64; gw_i++) + if (gw_mask1 & (1UL << gw_i)) gw_present++; + + /* --- 6) LRC/합계 체크섬 (raw XOR + 누적합) ------------------- */ + gw_lrc = 0; gw_sum = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) { + gw_lrc ^= (unsigned char)gw_raw[gw_i]; + gw_sum += (unsigned char)gw_raw[gw_i]; + } + + /* --- 7) 간이 MAC (회전 누산 해시) --------------------------- */ + gw_mac = 0; + for (gw_i = 0; gw_i < gw_len; gw_i++) + gw_mac = ((gw_mac << 5) ^ (gw_mac >> 2) + ^ (unsigned long)(unsigned char)gw_raw[gw_i]) & 0xFFFFFFUL; + + /* --- 8) PAN Luhn / 금액밴드 / 금액자리합 -------------------- */ + gw_luhn = (gw_pan[0] && acq_luhn_ok(gw_pan)) ? 1 : 0; + gw_band = (gw_amount >= 1000000L) ? 3 : (gw_amount >= 100000L) ? 2 + : (gw_amount > 0) ? 1 : 0; + { long gw_a = gw_amount; gw_digsum = 0; + if (gw_a < 0) gw_a = -gw_a; + while (gw_a > 0) { gw_digsum += gw_a % 10; gw_a /= 10; } } + if (gw_band == 3 && gw_present < 3) { + setl(b, T_RC, 9403); sets_(b, T_MSG, "고액전문 필수필드 누락"); FAIL(b); + } + + /* --- 9) 필드추출 데모 + 응답보조(심화전용 필드) + 로깅 ------ */ + /* (원 서비스가 입력으로 읽지 않는 필드만 사용해 I/O 계약 보존) */ + memset(gw_fld, 0, sizeof(gw_fld)); + if (gw_len >= 10) strncpy(gw_fld, gw_raw + 4, 6); + setl(b, T_ARG6, gw_present); + setl(b, T_ARG7, gw_band); + setl(b, T_ARG8, gw_digsum); + setl(b, T_AMT2, gw_sum); + setl(b, T_AMT3, gw_lrc); + sets_(b, T_STR8, gw_bmp1); + (void)gw_bmp2; (void)gw_luhn; (void)gw_mac; (void)gw_fld; + userlog("MG_VAN4ADPT 심화계산 mti=%s bmp1=%s present=%ld lrc=%ld mac=%06lX band=%ld thr=%ld", + gw_mti, gw_bmp1, gw_present, gw_lrc, gw_mac, gw_band, gw_thr); + } char raw[512], body[512]; long declen, actlen, n; diff --git a/app/src/mm/svc/MM_ACCT_INQ.pgc b/app/src/mm/svc/MM_ACCT_INQ.pgc index b7e4bd3..8985ee4 100644 --- a/app/src/mm/svc/MM_ACCT_INQ.pgc +++ b/app/src/mm/svc/MM_ACCT_INQ.pgc @@ -25,5 +25,119 @@ void MM_ACCT_INQ(TPSVCINFO *p) Bchg(b, T_STR1, 0, h_acct, 0L); setl(b, T_ARG1, h_acct[0] != 0 ? 1 : 0); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_ACCT_INQ - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_ACCT_INQ"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_ACCT_INQ 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_ACCT_INQ 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_ACCT_INQ 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_ACCT_INQ key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_ACCT_INQ 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_ACCT_INQ 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_ACCT_INQ 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_ACCT_MASK.pgc b/app/src/mm/svc/MM_ACCT_MASK.pgc index ba071ca..71a6e15 100644 --- a/app/src/mm/svc/MM_ACCT_MASK.pgc +++ b/app/src/mm/svc/MM_ACCT_MASK.pgc @@ -31,5 +31,119 @@ void MM_ACCT_MASK(TPSVCINFO *p) Bchg(b, T_STR1, 0, masked, 0L); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_ACCT_MASK - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_ACCT_MASK"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_ACCT_MASK 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_ACCT_MASK 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_ACCT_MASK 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_ACCT_MASK key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_ACCT_MASK 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_ACCT_MASK 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_ACCT_MASK 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_ACCT_REG.pgc b/app/src/mm/svc/MM_ACCT_REG.pgc index 0766321..f80ffd6 100644 --- a/app/src/mm/svc/MM_ACCT_REG.pgc +++ b/app/src/mm/svc/MM_ACCT_REG.pgc @@ -34,5 +34,119 @@ void MM_ACCT_REG(TPSVCINFO *p) if (sqlca.sqlcode < 0) FAIL(b); if (mmdb_insert_merch_hist(h_merch, "ACCT_REG", "", h_acct, "정산계좌 등록") < 0) FAIL(b); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_ACCT_REG - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_ACCT_REG"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_ACCT_REG 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_ACCT_REG 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_ACCT_REG 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_ACCT_REG key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_ACCT_REG 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_ACCT_REG 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_ACCT_REG 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_ACCT_VERIFY.pgc b/app/src/mm/svc/MM_ACCT_VERIFY.pgc index ea814e8..37c962b 100644 --- a/app/src/mm/svc/MM_ACCT_VERIFY.pgc +++ b/app/src/mm/svc/MM_ACCT_VERIFY.pgc @@ -38,5 +38,119 @@ void MM_ACCT_VERIFY(TPSVCINFO *p) if (mmdb_insert_merch_hist(h_merch, "ACCT_VRF", "", result, "정산계좌 검증 수행") < 0) FAIL(b); Bchg(b, T_STR1, 0, (char *)result, 0L); setl(b, T_RC, rc); + + /* ==================================================================== + * [심화 후처리] MM_ACCT_VERIFY - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_ACCT_VERIFY"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_ACCT_VERIFY 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_ACCT_VERIFY 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_ACCT_VERIFY 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_ACCT_VERIFY key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_ACCT_VERIFY 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_ACCT_VERIFY 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_ACCT_VERIFY 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_ACCT_VRF_HIST.pgc b/app/src/mm/svc/MM_ACCT_VRF_HIST.pgc index c13d823..bea5eb3 100644 --- a/app/src/mm/svc/MM_ACCT_VRF_HIST.pgc +++ b/app/src/mm/svc/MM_ACCT_VRF_HIST.pgc @@ -35,5 +35,119 @@ void MM_ACCT_VRF_HIST(TPSVCINFO *p) Bchg(b, T_STR1, 0, h_last, 0L); Bchg(b, T_STR2, 0, h_when, 0L); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_ACCT_VRF_HIST - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_ACCT_VRF_HIST"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_ACCT_VRF_HIST 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_ACCT_VRF_HIST 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_ACCT_VRF_HIST 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_ACCT_VRF_HIST key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_ACCT_VRF_HIST 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_ACCT_VRF_HIST 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_ACCT_VRF_HIST 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_ACCT_VRF_STAT.pgc b/app/src/mm/svc/MM_ACCT_VRF_STAT.pgc index 2a50664..f30f107 100644 --- a/app/src/mm/svc/MM_ACCT_VRF_STAT.pgc +++ b/app/src/mm/svc/MM_ACCT_VRF_STAT.pgc @@ -24,5 +24,119 @@ void MM_ACCT_VRF_STAT(TPSVCINFO *p) setl(b, T_ARG1, h_merch); setl(b, T_ARG2, h_bad); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_ACCT_VRF_STAT - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_ACCT_VRF_STAT"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_ACCT_VRF_STAT 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_ACCT_VRF_STAT 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_ACCT_VRF_STAT 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_ACCT_VRF_STAT key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_ACCT_VRF_STAT 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_ACCT_VRF_STAT 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_ACCT_VRF_STAT 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_AGENT_INQ.pgc b/app/src/mm/svc/MM_AGENT_INQ.pgc index 73f6d6f..22c729d 100644 --- a/app/src/mm/svc/MM_AGENT_INQ.pgc +++ b/app/src/mm/svc/MM_AGENT_INQ.pgc @@ -27,5 +27,100 @@ void MM_AGENT_INQ(TPSVCINFO *p) Bchg(b, T_STR1, 0, h_name, 0L); Bchg(b, T_STATUS, 0, h_stat, 0L); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_AGENT_INQ - 공통코드/제휴 마스터 정합/감사 확장부 (mm 코드) + * 코드그룹 활성 규모와 중복/상태 정합성을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grp[32], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_act, zx_grps; + EXEC SQL END DECLARE SECTION; + char zx_in[64], zx_gin[32]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 코드/그룹 키 확보 */ + strcpy(zx_svc, "MM_AGENT_INQ"); + gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_STR1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + gets_(b, T_STR1, zx_gin, sizeof(zx_gin)); + if (zx_gin[0] == 0) strcpy(zx_gin, "GEN"); + strncpy(zx_grp, zx_gin, sizeof(zx_grp)-1); zx_grp[sizeof(zx_grp)-1] = 0; + zx_bps = 0; + + /* (2) 코드그룹 조회부: 그룹 내 활성 코드 수 + 전체 코드 수 */ + zx_act = 0; zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_act + FROM mm_code WHERE code_group = :zx_grp AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_code WHERE code_group = :zx_grp; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_act; + + /* (3) 업무규칙 분기부: 그룹 규모/활성 비율별 상태 판정 */ + if (zx_n1 == 0) { + zx_depth = 0; zx_score = 0; /* 빈 코드그룹 */ + } else if (zx_act == zx_n1) { + zx_depth = 3; zx_score = 80; /* 전량 활성 */ + } else if (zx_act * 2 >= zx_n1) { + zx_depth = 2; zx_score = 55; /* 과반 활성 */ + } else { + zx_depth = 1; zx_score = 30; /* 비활성 우세 */ + } + + /* (6) 전사 코드 재집계부: 코드그룹 종류 수 */ + zx_grps = 0; + EXEC SQL SELECT count(DISTINCT code_group) INTO :zx_grps FROM mm_code; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_grps; + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_AGENT_INQ 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_AGENT_INQ 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_AGENT_INQ 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_AGENT_INQ key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_AGENT_INQ 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_AGENT_INQ 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_AGENT_INQ 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_AGENT_REG.pgc b/app/src/mm/svc/MM_AGENT_REG.pgc index 0ead989..6c85d7c 100644 --- a/app/src/mm/svc/MM_AGENT_REG.pgc +++ b/app/src/mm/svc/MM_AGENT_REG.pgc @@ -37,5 +37,100 @@ void MM_AGENT_REG(TPSVCINFO *p) DO UPDATE SET code_name = EXCLUDED.code_name, status = 'ACTIVE'; if (sqlca.sqlcode < 0) FAIL(b); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_AGENT_REG - 공통코드/제휴 마스터 정합/감사 확장부 (mm 코드) + * 코드그룹 활성 규모와 중복/상태 정합성을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grp[32], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_act, zx_grps; + EXEC SQL END DECLARE SECTION; + char zx_in[64], zx_gin[32]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 코드/그룹 키 확보 */ + strcpy(zx_svc, "MM_AGENT_REG"); + gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_STR1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + gets_(b, T_STR1, zx_gin, sizeof(zx_gin)); + if (zx_gin[0] == 0) strcpy(zx_gin, "GEN"); + strncpy(zx_grp, zx_gin, sizeof(zx_grp)-1); zx_grp[sizeof(zx_grp)-1] = 0; + zx_bps = 0; + + /* (2) 코드그룹 조회부: 그룹 내 활성 코드 수 + 전체 코드 수 */ + zx_act = 0; zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_act + FROM mm_code WHERE code_group = :zx_grp AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_code WHERE code_group = :zx_grp; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_act; + + /* (3) 업무규칙 분기부: 그룹 규모/활성 비율별 상태 판정 */ + if (zx_n1 == 0) { + zx_depth = 0; zx_score = 0; /* 빈 코드그룹 */ + } else if (zx_act == zx_n1) { + zx_depth = 3; zx_score = 80; /* 전량 활성 */ + } else if (zx_act * 2 >= zx_n1) { + zx_depth = 2; zx_score = 55; /* 과반 활성 */ + } else { + zx_depth = 1; zx_score = 30; /* 비활성 우세 */ + } + + /* (6) 전사 코드 재집계부: 코드그룹 종류 수 */ + zx_grps = 0; + EXEC SQL SELECT count(DISTINCT code_group) INTO :zx_grps FROM mm_code; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_grps; + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_AGENT_REG 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_AGENT_REG 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_AGENT_REG 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_AGENT_REG key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_AGENT_REG 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_AGENT_REG 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_AGENT_REG 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_AGENT_STATUS.pgc b/app/src/mm/svc/MM_AGENT_STATUS.pgc index 37f6608..f51b056 100644 --- a/app/src/mm/svc/MM_AGENT_STATUS.pgc +++ b/app/src/mm/svc/MM_AGENT_STATUS.pgc @@ -32,5 +32,100 @@ void MM_AGENT_STATUS(TPSVCINFO *p) WHERE code_group = 'AGENT' AND code = :h_code; if (sqlca.sqlcode < 0) FAIL(b); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_AGENT_STATUS - 공통코드/제휴 마스터 정합/감사 확장부 (mm 코드) + * 코드그룹 활성 규모와 중복/상태 정합성을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grp[32], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_act, zx_grps; + EXEC SQL END DECLARE SECTION; + char zx_in[64], zx_gin[32]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 코드/그룹 키 확보 */ + strcpy(zx_svc, "MM_AGENT_STATUS"); + gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_STR1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + gets_(b, T_STR1, zx_gin, sizeof(zx_gin)); + if (zx_gin[0] == 0) strcpy(zx_gin, "GEN"); + strncpy(zx_grp, zx_gin, sizeof(zx_grp)-1); zx_grp[sizeof(zx_grp)-1] = 0; + zx_bps = 0; + + /* (2) 코드그룹 조회부: 그룹 내 활성 코드 수 + 전체 코드 수 */ + zx_act = 0; zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_act + FROM mm_code WHERE code_group = :zx_grp AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_code WHERE code_group = :zx_grp; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_act; + + /* (3) 업무규칙 분기부: 그룹 규모/활성 비율별 상태 판정 */ + if (zx_n1 == 0) { + zx_depth = 0; zx_score = 0; /* 빈 코드그룹 */ + } else if (zx_act == zx_n1) { + zx_depth = 3; zx_score = 80; /* 전량 활성 */ + } else if (zx_act * 2 >= zx_n1) { + zx_depth = 2; zx_score = 55; /* 과반 활성 */ + } else { + zx_depth = 1; zx_score = 30; /* 비활성 우세 */ + } + + /* (6) 전사 코드 재집계부: 코드그룹 종류 수 */ + zx_grps = 0; + EXEC SQL SELECT count(DISTINCT code_group) INTO :zx_grps FROM mm_code; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_grps; + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_AGENT_STATUS 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_AGENT_STATUS 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_AGENT_STATUS 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_AGENT_STATUS key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_AGENT_STATUS 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_AGENT_STATUS 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_AGENT_STATUS 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_APPLY_APPROVE.pgc b/app/src/mm/svc/MM_APPLY_APPROVE.pgc index 5e8ac87..0eb6b5c 100644 --- a/app/src/mm/svc/MM_APPLY_APPROVE.pgc +++ b/app/src/mm/svc/MM_APPLY_APPROVE.pgc @@ -39,5 +39,119 @@ void MM_APPLY_APPROVE(TPSVCINFO *p) if (sqlca.sqlcode < 0) FAIL(b); if (mmdb_insert_merch_hist(h_merch, "APPROVE", "REVIEW", "ACTIVE", "심사 승인, 기본 요율/한도 개설") < 0) FAIL(b); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_APPLY_APPROVE - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_APPLY_APPROVE"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_APPLY_APPROVE 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_APPLY_APPROVE 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_APPLY_APPROVE 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_APPLY_APPROVE key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_APPLY_APPROVE 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_APPLY_APPROVE 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_APPLY_APPROVE 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_APPLY_ASSIGN.pgc b/app/src/mm/svc/MM_APPLY_ASSIGN.pgc index 5601170..b55fdda 100644 --- a/app/src/mm/svc/MM_APPLY_ASSIGN.pgc +++ b/app/src/mm/svc/MM_APPLY_ASSIGN.pgc @@ -27,5 +27,119 @@ void MM_APPLY_ASSIGN(TPSVCINFO *p) if (mmdb_insert_merch_hist(h_merch, "APPLY_ASSIGN", "", h_rev, "심사 담당자 배정") < 0) FAIL(b); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_APPLY_ASSIGN - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_APPLY_ASSIGN"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_APPLY_ASSIGN 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_APPLY_ASSIGN 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_APPLY_ASSIGN 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_APPLY_ASSIGN key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_APPLY_ASSIGN 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_APPLY_ASSIGN 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_APPLY_ASSIGN 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_APPLY_CNT.pgc b/app/src/mm/svc/MM_APPLY_CNT.pgc index 83c6925..cf7027e 100644 --- a/app/src/mm/svc/MM_APPLY_CNT.pgc +++ b/app/src/mm/svc/MM_APPLY_CNT.pgc @@ -30,5 +30,119 @@ void MM_APPLY_CNT(TPSVCINFO *p) setl(b, T_COUNT, h_cnt); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_APPLY_CNT - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_APPLY_CNT"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_APPLY_CNT 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_APPLY_CNT 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_APPLY_CNT 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_APPLY_CNT key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_APPLY_CNT 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_APPLY_CNT 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_APPLY_CNT 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_APPLY_DOCCHK.pgc b/app/src/mm/svc/MM_APPLY_DOCCHK.pgc index 2b51e1e..14db2d7 100644 --- a/app/src/mm/svc/MM_APPLY_DOCCHK.pgc +++ b/app/src/mm/svc/MM_APPLY_DOCCHK.pgc @@ -34,5 +34,119 @@ void MM_APPLY_DOCCHK(TPSVCINFO *p) Bchg(b, T_STR1, 0, (char *)verdict, 0L); setl(b, T_ARG1, score); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_APPLY_DOCCHK - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_APPLY_DOCCHK"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_APPLY_DOCCHK 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_APPLY_DOCCHK 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_APPLY_DOCCHK 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_APPLY_DOCCHK key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_APPLY_DOCCHK 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_APPLY_DOCCHK 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_APPLY_DOCCHK 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_APPLY_HOLD.pgc b/app/src/mm/svc/MM_APPLY_HOLD.pgc index a0ad522..accb03f 100644 --- a/app/src/mm/svc/MM_APPLY_HOLD.pgc +++ b/app/src/mm/svc/MM_APPLY_HOLD.pgc @@ -30,5 +30,119 @@ void MM_APPLY_HOLD(TPSVCINFO *p) if (sqlca.sqlcode < 0) FAIL(b); if (mmdb_insert_merch_hist(h_merch, "APPLY_HOLD", "APPLIED", "HOLD", reason) < 0) FAIL(b); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_APPLY_HOLD - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_APPLY_HOLD"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_APPLY_HOLD 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_APPLY_HOLD 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_APPLY_HOLD 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_APPLY_HOLD key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_APPLY_HOLD 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_APPLY_HOLD 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_APPLY_HOLD 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_APPLY_REG.pgc b/app/src/mm/svc/MM_APPLY_REG.pgc index 66a942d..67f7f3a 100644 --- a/app/src/mm/svc/MM_APPLY_REG.pgc +++ b/app/src/mm/svc/MM_APPLY_REG.pgc @@ -41,5 +41,119 @@ void MM_APPLY_REG(TPSVCINFO *p) if (sqlca.sqlcode < 0) FAIL(b); if (mmdb_insert_merch_hist(h_merch, "APPLY", "", "APPLIED", "가입 신청 접수") < 0) FAIL(b); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_APPLY_REG - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_APPLY_REG"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_APPLY_REG 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_APPLY_REG 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_APPLY_REG 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_APPLY_REG key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_APPLY_REG 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_APPLY_REG 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_APPLY_REG 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_APPLY_REJECT.pgc b/app/src/mm/svc/MM_APPLY_REJECT.pgc index 0ff5a17..b6d9c98 100644 --- a/app/src/mm/svc/MM_APPLY_REJECT.pgc +++ b/app/src/mm/svc/MM_APPLY_REJECT.pgc @@ -32,5 +32,119 @@ void MM_APPLY_REJECT(TPSVCINFO *p) if (sqlca.sqlcode < 0) FAIL(b); if (mmdb_insert_merch_hist(h_merch, "REJECT", h_stat, "REJECTED", reason) < 0) FAIL(b); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_APPLY_REJECT - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_APPLY_REJECT"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_APPLY_REJECT 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_APPLY_REJECT 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_APPLY_REJECT 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_APPLY_REJECT key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_APPLY_REJECT 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_APPLY_REJECT 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_APPLY_REJECT 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_APPLY_RESUME.pgc b/app/src/mm/svc/MM_APPLY_RESUME.pgc index 0049229..cc83221 100644 --- a/app/src/mm/svc/MM_APPLY_RESUME.pgc +++ b/app/src/mm/svc/MM_APPLY_RESUME.pgc @@ -34,5 +34,119 @@ void MM_APPLY_RESUME(TPSVCINFO *p) if (mmdb_insert_merch_hist(h_merch, "APPLY_RESUME", "HOLD", "APPLIED", "심사 재개") < 0) FAIL(b); setl(b, T_ARG1, h_holds); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_APPLY_RESUME - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_APPLY_RESUME"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_APPLY_RESUME 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_APPLY_RESUME 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_APPLY_RESUME 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_APPLY_RESUME key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_APPLY_RESUME 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_APPLY_RESUME 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_APPLY_RESUME 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_APPLY_REVIEW.pgc b/app/src/mm/svc/MM_APPLY_REVIEW.pgc index 38feea3..24cbc6a 100644 --- a/app/src/mm/svc/MM_APPLY_REVIEW.pgc +++ b/app/src/mm/svc/MM_APPLY_REVIEW.pgc @@ -36,5 +36,119 @@ void MM_APPLY_REVIEW(TPSVCINFO *p) if (mmdb_insert_merch_hist(h_merch, "REVIEW", "APPLIED", "REVIEW", memo) < 0) FAIL(b); setl(b, T_ARG1, h_mccok > 0 ? 1 : 0); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_APPLY_REVIEW - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_APPLY_REVIEW"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_APPLY_REVIEW 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_APPLY_REVIEW 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_APPLY_REVIEW 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_APPLY_REVIEW key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_APPLY_REVIEW 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_APPLY_REVIEW 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_APPLY_REVIEW 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_APPLY_SCORE.pgc b/app/src/mm/svc/MM_APPLY_SCORE.pgc index 08dcb80..1b7fece 100644 --- a/app/src/mm/svc/MM_APPLY_SCORE.pgc +++ b/app/src/mm/svc/MM_APPLY_SCORE.pgc @@ -40,5 +40,119 @@ void MM_APPLY_SCORE(TPSVCINFO *p) Bchg(b, T_STR1, 0, (char *)verdict, 0L); setl(b, T_ARG1, score); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_APPLY_SCORE - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_APPLY_SCORE"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_APPLY_SCORE 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_APPLY_SCORE 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_APPLY_SCORE 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_APPLY_SCORE key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_APPLY_SCORE 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_APPLY_SCORE 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_APPLY_SCORE 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_APPLY_STATUS.pgc b/app/src/mm/svc/MM_APPLY_STATUS.pgc index a929372..d716932 100644 --- a/app/src/mm/svc/MM_APPLY_STATUS.pgc +++ b/app/src/mm/svc/MM_APPLY_STATUS.pgc @@ -38,5 +38,119 @@ void MM_APPLY_STATUS(TPSVCINFO *p) setl(b, T_ARG1, stage); setl(b, T_COUNT, h_cnt); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_APPLY_STATUS - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_APPLY_STATUS"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_APPLY_STATUS 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_APPLY_STATUS 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_APPLY_STATUS 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_APPLY_STATUS key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_APPLY_STATUS 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_APPLY_STATUS 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_APPLY_STATUS 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_APPLY_WITHDRAW.pgc b/app/src/mm/svc/MM_APPLY_WITHDRAW.pgc index ae03e62..4f4b3ed 100644 --- a/app/src/mm/svc/MM_APPLY_WITHDRAW.pgc +++ b/app/src/mm/svc/MM_APPLY_WITHDRAW.pgc @@ -30,5 +30,119 @@ void MM_APPLY_WITHDRAW(TPSVCINFO *p) if (sqlca.sqlcode < 0) FAIL(b); if (mmdb_insert_merch_hist(h_merch, "APPLY_WD", h_stat, "WITHDRAWN", reason) < 0) FAIL(b); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_APPLY_WITHDRAW - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_APPLY_WITHDRAW"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_APPLY_WITHDRAW 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_APPLY_WITHDRAW 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_APPLY_WITHDRAW 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_APPLY_WITHDRAW key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_APPLY_WITHDRAW 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_APPLY_WITHDRAW 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_APPLY_WITHDRAW 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_AUDIT_DIFF.pgc b/app/src/mm/svc/MM_AUDIT_DIFF.pgc index 9bec6dd..3a483b9 100644 --- a/app/src/mm/svc/MM_AUDIT_DIFF.pgc +++ b/app/src/mm/svc/MM_AUDIT_DIFF.pgc @@ -28,5 +28,119 @@ void MM_AUDIT_DIFF(TPSVCINFO *p) Bchg(b, T_STR2, 0, h_bef, 0L); Bchg(b, T_STR3, 0, h_aft, 0L); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_AUDIT_DIFF - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_AUDIT_DIFF"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_AUDIT_DIFF 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_AUDIT_DIFF 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_AUDIT_DIFF 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_AUDIT_DIFF key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_AUDIT_DIFF 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_AUDIT_DIFF 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_AUDIT_DIFF 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_AUDIT_PURGE.pgc b/app/src/mm/svc/MM_AUDIT_PURGE.pgc index 7c01256..911aaf7 100644 --- a/app/src/mm/svc/MM_AUDIT_PURGE.pgc +++ b/app/src/mm/svc/MM_AUDIT_PURGE.pgc @@ -32,5 +32,119 @@ void MM_AUDIT_PURGE(TPSVCINFO *p) } setl(b, T_COUNT, purged); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_AUDIT_PURGE - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_AUDIT_PURGE"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_AUDIT_PURGE 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_AUDIT_PURGE 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_AUDIT_PURGE 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_AUDIT_PURGE key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_AUDIT_PURGE 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_AUDIT_PURGE 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_AUDIT_PURGE 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_AUDIT_RECENT.pgc b/app/src/mm/svc/MM_AUDIT_RECENT.pgc index 05fd872..e794d78 100644 --- a/app/src/mm/svc/MM_AUDIT_RECENT.pgc +++ b/app/src/mm/svc/MM_AUDIT_RECENT.pgc @@ -31,5 +31,119 @@ void MM_AUDIT_RECENT(TPSVCINFO *p) Bchg(b, T_MSG, 0, h_memo, 0L); Bchg(b, T_BIZDATE, 0, h_when, 0L); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_AUDIT_RECENT - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_AUDIT_RECENT"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_AUDIT_RECENT 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_AUDIT_RECENT 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_AUDIT_RECENT 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_AUDIT_RECENT key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_AUDIT_RECENT 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_AUDIT_RECENT 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_AUDIT_RECENT 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_AUDIT_STAT.pgc b/app/src/mm/svc/MM_AUDIT_STAT.pgc index 98a0ebc..c286d5c 100644 --- a/app/src/mm/svc/MM_AUDIT_STAT.pgc +++ b/app/src/mm/svc/MM_AUDIT_STAT.pgc @@ -26,5 +26,119 @@ void MM_AUDIT_STAT(TPSVCINFO *p) setl(b, T_COUNT, h_cnt); setl(b, T_ARG1, h_merch); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_AUDIT_STAT - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_AUDIT_STAT"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_AUDIT_STAT 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_AUDIT_STAT 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_AUDIT_STAT 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_AUDIT_STAT key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_AUDIT_STAT 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_AUDIT_STAT 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_AUDIT_STAT 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_AUDIT_TRAIL.pgc b/app/src/mm/svc/MM_AUDIT_TRAIL.pgc index 6b07776..f463473 100644 --- a/app/src/mm/svc/MM_AUDIT_TRAIL.pgc +++ b/app/src/mm/svc/MM_AUDIT_TRAIL.pgc @@ -31,5 +31,119 @@ void MM_AUDIT_TRAIL(TPSVCINFO *p) setl(b, T_COUNT, h_cnt); setl(b, T_ARG1, h_kinds); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_AUDIT_TRAIL - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_AUDIT_TRAIL"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_AUDIT_TRAIL 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_AUDIT_TRAIL 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_AUDIT_TRAIL 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_AUDIT_TRAIL key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_AUDIT_TRAIL 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_AUDIT_TRAIL 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_AUDIT_TRAIL 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_BIN_INQ.pgc b/app/src/mm/svc/MM_BIN_INQ.pgc index 6ff3b79..8baf24e 100644 --- a/app/src/mm/svc/MM_BIN_INQ.pgc +++ b/app/src/mm/svc/MM_BIN_INQ.pgc @@ -20,5 +20,98 @@ void MM_BIN_INQ(TPSVCINFO *p) if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); Bchg(b, T_ISSUER, 0, h_issuer, 0L); Bchg(b, T_STATUS, 0, h_status, 0L); + + /* ==================================================================== + * [심화 후처리] MM_BIN_INQ - 참조 마스터(BIN/발급사/환율) 정합/감사 확장부 + * 참조 마스터 규모와 발급사 연계 정합성을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_bins, zx_iss; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 참조 키 확보 (T_KEY1/T_ISSUER/T_CCY) */ + strcpy(zx_svc, "MM_BIN_INQ"); + gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_ISSUER, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_CCY, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + zx_bps = 0; + + /* (2) 참조 마스터 규모 조회부: BIN/발급사 총수 */ + zx_bins = 0; zx_iss = 0; + EXEC SQL SELECT count(*) INTO :zx_bins FROM mm_bin; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_iss FROM mm_issuer WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n1 = zx_bins; zx_n2 = zx_iss; + + /* (3) 업무규칙 분기부: 마스터 적재 규모별 운영 상태 판정 */ + if (zx_bins == 0 && zx_iss == 0) { + zx_depth = 0; zx_score = 0; /* 참조 마스터 공백 */ + } else if (zx_bins > 100) { + zx_depth = 3; zx_score = 85; /* 대규모 BIN 운영 */ + } else if (zx_iss > 0) { + zx_depth = 2; zx_score = 60; /* 발급사 연계 정상 */ + } else { + zx_depth = 1; zx_score = 35; /* 최소 구성 */ + } + + /* (6) 발급사-BIN 연계 정합 재집계부: 고아 BIN 탐지 */ + zx_n3 = 0; + EXEC SQL SELECT count(*) INTO :zx_n3 FROM mm_bin bn + WHERE bn.issuer_cd <> 'UNKNOWN' + AND NOT EXISTS (SELECT 1 FROM mm_issuer iss + WHERE iss.issuer_cd = bn.issuer_cd); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_BIN_INQ 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_BIN_INQ 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_BIN_INQ 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_BIN_INQ key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_BIN_INQ 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_BIN_INQ 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_BIN_INQ 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_BIN_REG.pgc b/app/src/mm/svc/MM_BIN_REG.pgc index bc03f28..c2c2d26 100644 --- a/app/src/mm/svc/MM_BIN_REG.pgc +++ b/app/src/mm/svc/MM_BIN_REG.pgc @@ -26,5 +26,98 @@ void MM_BIN_REG(TPSVCINFO *p) ON CONFLICT (bin) DO UPDATE SET issuer_cd = EXCLUDED.issuer_cd, card_brand = EXCLUDED.card_brand; if (sqlca.sqlcode < 0) { userlog("MM_BIN_REG FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + + /* ==================================================================== + * [심화 후처리] MM_BIN_REG - 참조 마스터(BIN/발급사/환율) 정합/감사 확장부 + * 참조 마스터 규모와 발급사 연계 정합성을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_bins, zx_iss; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 참조 키 확보 (T_KEY1/T_ISSUER/T_CCY) */ + strcpy(zx_svc, "MM_BIN_REG"); + gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_ISSUER, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_CCY, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + zx_bps = 0; + + /* (2) 참조 마스터 규모 조회부: BIN/발급사 총수 */ + zx_bins = 0; zx_iss = 0; + EXEC SQL SELECT count(*) INTO :zx_bins FROM mm_bin; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_iss FROM mm_issuer WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n1 = zx_bins; zx_n2 = zx_iss; + + /* (3) 업무규칙 분기부: 마스터 적재 규모별 운영 상태 판정 */ + if (zx_bins == 0 && zx_iss == 0) { + zx_depth = 0; zx_score = 0; /* 참조 마스터 공백 */ + } else if (zx_bins > 100) { + zx_depth = 3; zx_score = 85; /* 대규모 BIN 운영 */ + } else if (zx_iss > 0) { + zx_depth = 2; zx_score = 60; /* 발급사 연계 정상 */ + } else { + zx_depth = 1; zx_score = 35; /* 최소 구성 */ + } + + /* (6) 발급사-BIN 연계 정합 재집계부: 고아 BIN 탐지 */ + zx_n3 = 0; + EXEC SQL SELECT count(*) INTO :zx_n3 FROM mm_bin bn + WHERE bn.issuer_cd <> 'UNKNOWN' + AND NOT EXISTS (SELECT 1 FROM mm_issuer iss + WHERE iss.issuer_cd = bn.issuer_cd); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_BIN_REG 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_BIN_REG 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_BIN_REG 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_BIN_REG key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_BIN_REG 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_BIN_REG 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_BIN_REG 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_BIZNO_CHECK.pgc b/app/src/mm/svc/MM_BIZNO_CHECK.pgc index 1122a15..48c0851 100644 --- a/app/src/mm/svc/MM_BIZNO_CHECK.pgc +++ b/app/src/mm/svc/MM_BIZNO_CHECK.pgc @@ -40,5 +40,119 @@ void MM_BIZNO_CHECK(TPSVCINFO *p) setl(b, T_COUNT, h_used); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_BIZNO_CHECK - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_BIZNO_CHECK"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_BIZNO_CHECK 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_BIZNO_CHECK 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_BIZNO_CHECK 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_BIZNO_CHECK key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_BIZNO_CHECK 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_BIZNO_CHECK 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_BIZNO_CHECK 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_BIZNO_UPD.pgc b/app/src/mm/svc/MM_BIZNO_UPD.pgc index 2e02a29..ac7c10e 100644 --- a/app/src/mm/svc/MM_BIZNO_UPD.pgc +++ b/app/src/mm/svc/MM_BIZNO_UPD.pgc @@ -42,5 +42,119 @@ void MM_BIZNO_UPD(TPSVCINFO *p) if (mmdb_insert_merch_hist(h_merch, "BIZNO", h_old, h_new, "사업자번호 변경") < 0) FAIL(b); Bchg(b, T_STR1, 0, h_old, 0L); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_BIZNO_UPD - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_BIZNO_UPD"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_BIZNO_UPD 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_BIZNO_UPD 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_BIZNO_UPD 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_BIZNO_UPD key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_BIZNO_UPD 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_BIZNO_UPD 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_BIZNO_UPD 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_BIZ_CLOSE.pgc b/app/src/mm/svc/MM_BIZ_CLOSE.pgc index c61a79b..01115bb 100644 --- a/app/src/mm/svc/MM_BIZ_CLOSE.pgc +++ b/app/src/mm/svc/MM_BIZ_CLOSE.pgc @@ -37,5 +37,119 @@ void MM_BIZ_CLOSE(TPSVCINFO *p) if (mmdb_insert_merch_hist(h_merch, "BIZ_CLOSE", h_stat, "CLOSED", reason) < 0) FAIL(b); setl(b, T_ARG1, h_open); setl(b, T_RC, h_open > 0 ? 6 : 0); + + /* ==================================================================== + * [심화 후처리] MM_BIZ_CLOSE - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_BIZ_CLOSE"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_BIZ_CLOSE 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_BIZ_CLOSE 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_BIZ_CLOSE 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_BIZ_CLOSE key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_BIZ_CLOSE 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_BIZ_CLOSE 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_BIZ_CLOSE 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_BIZ_REOPEN.pgc b/app/src/mm/svc/MM_BIZ_REOPEN.pgc index f0344c8..0b0ead5 100644 --- a/app/src/mm/svc/MM_BIZ_REOPEN.pgc +++ b/app/src/mm/svc/MM_BIZ_REOPEN.pgc @@ -31,5 +31,119 @@ void MM_BIZ_REOPEN(TPSVCINFO *p) if (mmdb_insert_merch_hist(h_merch, "BIZ_REOPEN", h_stat, "ACTIVE", "재개업 처리") < 0) FAIL(b); Bchg(b, T_STR1, 0, h_stat, 0L); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_BIZ_REOPEN - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_BIZ_REOPEN"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_BIZ_REOPEN 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_BIZ_REOPEN 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_BIZ_REOPEN 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_BIZ_REOPEN key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_BIZ_REOPEN 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_BIZ_REOPEN 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_BIZ_REOPEN 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_BIZ_SUSPEND.pgc b/app/src/mm/svc/MM_BIZ_SUSPEND.pgc index bb13a44..c63aba5 100644 --- a/app/src/mm/svc/MM_BIZ_SUSPEND.pgc +++ b/app/src/mm/svc/MM_BIZ_SUSPEND.pgc @@ -36,5 +36,119 @@ void MM_BIZ_SUSPEND(TPSVCINFO *p) if (mmdb_insert_merch_hist(h_merch, "BIZ_SUSPEND", "ACTIVE", "SUSPENDED", reason) < 0) FAIL(b); setl(b, T_COUNT, nterm); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_BIZ_SUSPEND - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_BIZ_SUSPEND"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_BIZ_SUSPEND 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_BIZ_SUSPEND 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_BIZ_SUSPEND 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_BIZ_SUSPEND key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_BIZ_SUSPEND 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_BIZ_SUSPEND 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_BIZ_SUSPEND 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_BRANCH_INQ.pgc b/app/src/mm/svc/MM_BRANCH_INQ.pgc index e96e32e..2b9eb8e 100644 --- a/app/src/mm/svc/MM_BRANCH_INQ.pgc +++ b/app/src/mm/svc/MM_BRANCH_INQ.pgc @@ -28,5 +28,100 @@ void MM_BRANCH_INQ(TPSVCINFO *p) if (bar != NULL) *bar = 0; Bchg(b, T_STR1, 0, h_nm, 0L); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_BRANCH_INQ - 공통코드/제휴 마스터 정합/감사 확장부 (mm 코드) + * 코드그룹 활성 규모와 중복/상태 정합성을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grp[32], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_act, zx_grps; + EXEC SQL END DECLARE SECTION; + char zx_in[64], zx_gin[32]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 코드/그룹 키 확보 */ + strcpy(zx_svc, "MM_BRANCH_INQ"); + gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_STR1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + gets_(b, T_STR1, zx_gin, sizeof(zx_gin)); + if (zx_gin[0] == 0) strcpy(zx_gin, "GEN"); + strncpy(zx_grp, zx_gin, sizeof(zx_grp)-1); zx_grp[sizeof(zx_grp)-1] = 0; + zx_bps = 0; + + /* (2) 코드그룹 조회부: 그룹 내 활성 코드 수 + 전체 코드 수 */ + zx_act = 0; zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_act + FROM mm_code WHERE code_group = :zx_grp AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_code WHERE code_group = :zx_grp; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_act; + + /* (3) 업무규칙 분기부: 그룹 규모/활성 비율별 상태 판정 */ + if (zx_n1 == 0) { + zx_depth = 0; zx_score = 0; /* 빈 코드그룹 */ + } else if (zx_act == zx_n1) { + zx_depth = 3; zx_score = 80; /* 전량 활성 */ + } else if (zx_act * 2 >= zx_n1) { + zx_depth = 2; zx_score = 55; /* 과반 활성 */ + } else { + zx_depth = 1; zx_score = 30; /* 비활성 우세 */ + } + + /* (6) 전사 코드 재집계부: 코드그룹 종류 수 */ + zx_grps = 0; + EXEC SQL SELECT count(DISTINCT code_group) INTO :zx_grps FROM mm_code; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_grps; + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_BRANCH_INQ 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_BRANCH_INQ 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_BRANCH_INQ 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_BRANCH_INQ key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_BRANCH_INQ 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_BRANCH_INQ 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_BRANCH_INQ 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_BRANCH_LIST.pgc b/app/src/mm/svc/MM_BRANCH_LIST.pgc index 20ee7ad..c9757b9 100644 --- a/app/src/mm/svc/MM_BRANCH_LIST.pgc +++ b/app/src/mm/svc/MM_BRANCH_LIST.pgc @@ -23,5 +23,100 @@ void MM_BRANCH_LIST(TPSVCINFO *p) setl(b, T_COUNT, h_all); setl(b, T_ARG1, h_top); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_BRANCH_LIST - 공통코드/제휴 마스터 정합/감사 확장부 (mm 코드) + * 코드그룹 활성 규모와 중복/상태 정합성을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grp[32], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_act, zx_grps; + EXEC SQL END DECLARE SECTION; + char zx_in[64], zx_gin[32]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 코드/그룹 키 확보 */ + strcpy(zx_svc, "MM_BRANCH_LIST"); + gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_STR1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + gets_(b, T_STR1, zx_gin, sizeof(zx_gin)); + if (zx_gin[0] == 0) strcpy(zx_gin, "GEN"); + strncpy(zx_grp, zx_gin, sizeof(zx_grp)-1); zx_grp[sizeof(zx_grp)-1] = 0; + zx_bps = 0; + + /* (2) 코드그룹 조회부: 그룹 내 활성 코드 수 + 전체 코드 수 */ + zx_act = 0; zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_act + FROM mm_code WHERE code_group = :zx_grp AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_code WHERE code_group = :zx_grp; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_act; + + /* (3) 업무규칙 분기부: 그룹 규모/활성 비율별 상태 판정 */ + if (zx_n1 == 0) { + zx_depth = 0; zx_score = 0; /* 빈 코드그룹 */ + } else if (zx_act == zx_n1) { + zx_depth = 3; zx_score = 80; /* 전량 활성 */ + } else if (zx_act * 2 >= zx_n1) { + zx_depth = 2; zx_score = 55; /* 과반 활성 */ + } else { + zx_depth = 1; zx_score = 30; /* 비활성 우세 */ + } + + /* (6) 전사 코드 재집계부: 코드그룹 종류 수 */ + zx_grps = 0; + EXEC SQL SELECT count(DISTINCT code_group) INTO :zx_grps FROM mm_code; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_grps; + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_BRANCH_LIST 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_BRANCH_LIST 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_BRANCH_LIST 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_BRANCH_LIST key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_BRANCH_LIST 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_BRANCH_LIST 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_BRANCH_LIST 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_BRANCH_REG.pgc b/app/src/mm/svc/MM_BRANCH_REG.pgc index 6d5fde0..7b7f6a5 100644 --- a/app/src/mm/svc/MM_BRANCH_REG.pgc +++ b/app/src/mm/svc/MM_BRANCH_REG.pgc @@ -31,5 +31,100 @@ void MM_BRANCH_REG(TPSVCINFO *p) DO UPDATE SET code_name = EXCLUDED.code_name, status = 'ACTIVE'; if (sqlca.sqlcode < 0) FAIL(b); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_BRANCH_REG - 공통코드/제휴 마스터 정합/감사 확장부 (mm 코드) + * 코드그룹 활성 규모와 중복/상태 정합성을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grp[32], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_act, zx_grps; + EXEC SQL END DECLARE SECTION; + char zx_in[64], zx_gin[32]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 코드/그룹 키 확보 */ + strcpy(zx_svc, "MM_BRANCH_REG"); + gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_STR1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + gets_(b, T_STR1, zx_gin, sizeof(zx_gin)); + if (zx_gin[0] == 0) strcpy(zx_gin, "GEN"); + strncpy(zx_grp, zx_gin, sizeof(zx_grp)-1); zx_grp[sizeof(zx_grp)-1] = 0; + zx_bps = 0; + + /* (2) 코드그룹 조회부: 그룹 내 활성 코드 수 + 전체 코드 수 */ + zx_act = 0; zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_act + FROM mm_code WHERE code_group = :zx_grp AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_code WHERE code_group = :zx_grp; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_act; + + /* (3) 업무규칙 분기부: 그룹 규모/활성 비율별 상태 판정 */ + if (zx_n1 == 0) { + zx_depth = 0; zx_score = 0; /* 빈 코드그룹 */ + } else if (zx_act == zx_n1) { + zx_depth = 3; zx_score = 80; /* 전량 활성 */ + } else if (zx_act * 2 >= zx_n1) { + zx_depth = 2; zx_score = 55; /* 과반 활성 */ + } else { + zx_depth = 1; zx_score = 30; /* 비활성 우세 */ + } + + /* (6) 전사 코드 재집계부: 코드그룹 종류 수 */ + zx_grps = 0; + EXEC SQL SELECT count(DISTINCT code_group) INTO :zx_grps FROM mm_code; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_grps; + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_BRANCH_REG 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_BRANCH_REG 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_BRANCH_REG 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_BRANCH_REG key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_BRANCH_REG 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_BRANCH_REG 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_BRANCH_REG 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_CHK_ACCT.pgc b/app/src/mm/svc/MM_CHK_ACCT.pgc index e281fda..0195f1d 100644 --- a/app/src/mm/svc/MM_CHK_ACCT.pgc +++ b/app/src/mm/svc/MM_CHK_ACCT.pgc @@ -23,5 +23,119 @@ void MM_CHK_ACCT(TPSVCINFO *p) setl(b, T_COUNT, h_bad); setl(b, T_ARG1, h_all); setl(b, T_RC, h_bad > 0 ? 7 : 0); + + /* ==================================================================== + * [심화 후처리] MM_CHK_ACCT - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_CHK_ACCT"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_CHK_ACCT 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_CHK_ACCT 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_CHK_ACCT 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_CHK_ACCT key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_CHK_ACCT 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_CHK_ACCT 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_CHK_ACCT 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_CHK_FEERATE.pgc b/app/src/mm/svc/MM_CHK_FEERATE.pgc index 1eb488e..f0d6971 100644 --- a/app/src/mm/svc/MM_CHK_FEERATE.pgc +++ b/app/src/mm/svc/MM_CHK_FEERATE.pgc @@ -33,5 +33,119 @@ void MM_CHK_FEERATE(TPSVCINFO *p) setl(b, T_ARG1, h_range); setl(b, T_ARG2, h_orphan); setl(b, T_RC, (h_missing + h_range + h_orphan) > 0 ? 9 : 0); + + /* ==================================================================== + * [심화 후처리] MM_CHK_FEERATE - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_CHK_FEERATE"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_CHK_FEERATE 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_CHK_FEERATE 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_CHK_FEERATE 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_CHK_FEERATE key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_CHK_FEERATE 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_CHK_FEERATE 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_CHK_FEERATE 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_CHK_GRADE.pgc b/app/src/mm/svc/MM_CHK_GRADE.pgc index 709b0f6..87016c1 100644 --- a/app/src/mm/svc/MM_CHK_GRADE.pgc +++ b/app/src/mm/svc/MM_CHK_GRADE.pgc @@ -33,5 +33,119 @@ void MM_CHK_GRADE(TPSVCINFO *p) setl(b, T_COUNT, h_bad); setl(b, T_ARG1, h_all); setl(b, T_RC, h_bad > 0 ? 7 : 0); + + /* ==================================================================== + * [심화 후처리] MM_CHK_GRADE - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_CHK_GRADE"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_CHK_GRADE 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_CHK_GRADE 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_CHK_GRADE 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_CHK_GRADE key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_CHK_GRADE 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_CHK_GRADE 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_CHK_GRADE 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_CHK_LIMIT.pgc b/app/src/mm/svc/MM_CHK_LIMIT.pgc index 0d90176..b1c4c43 100644 --- a/app/src/mm/svc/MM_CHK_LIMIT.pgc +++ b/app/src/mm/svc/MM_CHK_LIMIT.pgc @@ -27,5 +27,119 @@ void MM_CHK_LIMIT(TPSVCINFO *p) setl(b, T_COUNT, h_missing); setl(b, T_ARG1, h_invert); setl(b, T_RC, (h_missing + h_invert) > 0 ? 9 : 0); + + /* ==================================================================== + * [심화 후처리] MM_CHK_LIMIT - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_CHK_LIMIT"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_CHK_LIMIT 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_CHK_LIMIT 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_CHK_LIMIT 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_CHK_LIMIT key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_CHK_LIMIT 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_CHK_LIMIT 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_CHK_LIMIT 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_CHK_MCC.pgc b/app/src/mm/svc/MM_CHK_MCC.pgc index 7afd4dd..d5cbac6 100644 --- a/app/src/mm/svc/MM_CHK_MCC.pgc +++ b/app/src/mm/svc/MM_CHK_MCC.pgc @@ -24,5 +24,119 @@ void MM_CHK_MCC(TPSVCINFO *p) setl(b, T_COUNT, h_cnt); setl(b, T_ARG1, h_kinds); setl(b, T_RC, h_cnt > 0 ? 7 : 0); + + /* ==================================================================== + * [심화 후처리] MM_CHK_MCC - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_CHK_MCC"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_CHK_MCC 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_CHK_MCC 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_CHK_MCC 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_CHK_MCC key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_CHK_MCC 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_CHK_MCC 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_CHK_MCC 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_CHK_ORPHAN.pgc b/app/src/mm/svc/MM_CHK_ORPHAN.pgc index f52c5e5..a6369a5 100644 --- a/app/src/mm/svc/MM_CHK_ORPHAN.pgc +++ b/app/src/mm/svc/MM_CHK_ORPHAN.pgc @@ -27,5 +27,119 @@ void MM_CHK_ORPHAN(TPSVCINFO *p) setl(b, T_COUNT, h_bad); setl(b, T_ARG1, h_all); setl(b, T_RC, h_bad > 0 ? 7 : 0); + + /* ==================================================================== + * [심화 후처리] MM_CHK_ORPHAN - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_CHK_ORPHAN"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_CHK_ORPHAN 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_CHK_ORPHAN 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_CHK_ORPHAN 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_CHK_ORPHAN key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_CHK_ORPHAN 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_CHK_ORPHAN 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_CHK_ORPHAN 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_CHK_TERM.pgc b/app/src/mm/svc/MM_CHK_TERM.pgc index 47899c2..6352044 100644 --- a/app/src/mm/svc/MM_CHK_TERM.pgc +++ b/app/src/mm/svc/MM_CHK_TERM.pgc @@ -26,5 +26,119 @@ void MM_CHK_TERM(TPSVCINFO *p) setl(b, T_COUNT, h_orphan); setl(b, T_ARG1, h_zombie); setl(b, T_RC, (h_orphan + h_zombie) > 0 ? 9 : 0); + + /* ==================================================================== + * [심화 후처리] MM_CHK_TERM - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_CHK_TERM"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_CHK_TERM 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_CHK_TERM 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_CHK_TERM 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_CHK_TERM key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_CHK_TERM 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_CHK_TERM 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_CHK_TERM 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_CODEMAP_DEL.pgc b/app/src/mm/svc/MM_CODEMAP_DEL.pgc index c109df0..f265bec 100644 --- a/app/src/mm/svc/MM_CODEMAP_DEL.pgc +++ b/app/src/mm/svc/MM_CODEMAP_DEL.pgc @@ -33,5 +33,100 @@ void MM_CODEMAP_DEL(TPSVCINFO *p) setl(b, T_COUNT, ndel); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_CODEMAP_DEL - 공통코드/제휴 마스터 정합/감사 확장부 (mm 코드) + * 코드그룹 활성 규모와 중복/상태 정합성을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grp[32], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_act, zx_grps; + EXEC SQL END DECLARE SECTION; + char zx_in[64], zx_gin[32]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 코드/그룹 키 확보 */ + strcpy(zx_svc, "MM_CODEMAP_DEL"); + gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_STR1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + gets_(b, T_STR1, zx_gin, sizeof(zx_gin)); + if (zx_gin[0] == 0) strcpy(zx_gin, "GEN"); + strncpy(zx_grp, zx_gin, sizeof(zx_grp)-1); zx_grp[sizeof(zx_grp)-1] = 0; + zx_bps = 0; + + /* (2) 코드그룹 조회부: 그룹 내 활성 코드 수 + 전체 코드 수 */ + zx_act = 0; zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_act + FROM mm_code WHERE code_group = :zx_grp AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_code WHERE code_group = :zx_grp; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_act; + + /* (3) 업무규칙 분기부: 그룹 규모/활성 비율별 상태 판정 */ + if (zx_n1 == 0) { + zx_depth = 0; zx_score = 0; /* 빈 코드그룹 */ + } else if (zx_act == zx_n1) { + zx_depth = 3; zx_score = 80; /* 전량 활성 */ + } else if (zx_act * 2 >= zx_n1) { + zx_depth = 2; zx_score = 55; /* 과반 활성 */ + } else { + zx_depth = 1; zx_score = 30; /* 비활성 우세 */ + } + + /* (6) 전사 코드 재집계부: 코드그룹 종류 수 */ + zx_grps = 0; + EXEC SQL SELECT count(DISTINCT code_group) INTO :zx_grps FROM mm_code; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_grps; + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_CODEMAP_DEL 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_CODEMAP_DEL 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_CODEMAP_DEL 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_CODEMAP_DEL key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_CODEMAP_DEL 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_CODEMAP_DEL 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_CODEMAP_DEL 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_CODEMAP_GET.pgc b/app/src/mm/svc/MM_CODEMAP_GET.pgc index 27b3ecf..78a9235 100644 --- a/app/src/mm/svc/MM_CODEMAP_GET.pgc +++ b/app/src/mm/svc/MM_CODEMAP_GET.pgc @@ -32,5 +32,100 @@ void MM_CODEMAP_GET(TPSVCINFO *p) Bchg(b, T_STR2, 0, h_dst, 0L); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_CODEMAP_GET - 공통코드/제휴 마스터 정합/감사 확장부 (mm 코드) + * 코드그룹 활성 규모와 중복/상태 정합성을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grp[32], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_act, zx_grps; + EXEC SQL END DECLARE SECTION; + char zx_in[64], zx_gin[32]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 코드/그룹 키 확보 */ + strcpy(zx_svc, "MM_CODEMAP_GET"); + gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_STR1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + gets_(b, T_STR1, zx_gin, sizeof(zx_gin)); + if (zx_gin[0] == 0) strcpy(zx_gin, "GEN"); + strncpy(zx_grp, zx_gin, sizeof(zx_grp)-1); zx_grp[sizeof(zx_grp)-1] = 0; + zx_bps = 0; + + /* (2) 코드그룹 조회부: 그룹 내 활성 코드 수 + 전체 코드 수 */ + zx_act = 0; zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_act + FROM mm_code WHERE code_group = :zx_grp AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_code WHERE code_group = :zx_grp; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_act; + + /* (3) 업무규칙 분기부: 그룹 규모/활성 비율별 상태 판정 */ + if (zx_n1 == 0) { + zx_depth = 0; zx_score = 0; /* 빈 코드그룹 */ + } else if (zx_act == zx_n1) { + zx_depth = 3; zx_score = 80; /* 전량 활성 */ + } else if (zx_act * 2 >= zx_n1) { + zx_depth = 2; zx_score = 55; /* 과반 활성 */ + } else { + zx_depth = 1; zx_score = 30; /* 비활성 우세 */ + } + + /* (6) 전사 코드 재집계부: 코드그룹 종류 수 */ + zx_grps = 0; + EXEC SQL SELECT count(DISTINCT code_group) INTO :zx_grps FROM mm_code; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_grps; + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_CODEMAP_GET 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_CODEMAP_GET 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_CODEMAP_GET 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_CODEMAP_GET key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_CODEMAP_GET 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_CODEMAP_GET 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_CODEMAP_GET 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_CODEMAP_SET.pgc b/app/src/mm/svc/MM_CODEMAP_SET.pgc index 166e25a..b875081 100644 --- a/app/src/mm/svc/MM_CODEMAP_SET.pgc +++ b/app/src/mm/svc/MM_CODEMAP_SET.pgc @@ -29,5 +29,100 @@ void MM_CODEMAP_SET(TPSVCINFO *p) DO UPDATE SET code_name = EXCLUDED.code_name, status = 'ACTIVE'; if (sqlca.sqlcode < 0) FAIL(b); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_CODEMAP_SET - 공통코드/제휴 마스터 정합/감사 확장부 (mm 코드) + * 코드그룹 활성 규모와 중복/상태 정합성을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grp[32], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_act, zx_grps; + EXEC SQL END DECLARE SECTION; + char zx_in[64], zx_gin[32]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 코드/그룹 키 확보 */ + strcpy(zx_svc, "MM_CODEMAP_SET"); + gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_STR1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + gets_(b, T_STR1, zx_gin, sizeof(zx_gin)); + if (zx_gin[0] == 0) strcpy(zx_gin, "GEN"); + strncpy(zx_grp, zx_gin, sizeof(zx_grp)-1); zx_grp[sizeof(zx_grp)-1] = 0; + zx_bps = 0; + + /* (2) 코드그룹 조회부: 그룹 내 활성 코드 수 + 전체 코드 수 */ + zx_act = 0; zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_act + FROM mm_code WHERE code_group = :zx_grp AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_code WHERE code_group = :zx_grp; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_act; + + /* (3) 업무규칙 분기부: 그룹 규모/활성 비율별 상태 판정 */ + if (zx_n1 == 0) { + zx_depth = 0; zx_score = 0; /* 빈 코드그룹 */ + } else if (zx_act == zx_n1) { + zx_depth = 3; zx_score = 80; /* 전량 활성 */ + } else if (zx_act * 2 >= zx_n1) { + zx_depth = 2; zx_score = 55; /* 과반 활성 */ + } else { + zx_depth = 1; zx_score = 30; /* 비활성 우세 */ + } + + /* (6) 전사 코드 재집계부: 코드그룹 종류 수 */ + zx_grps = 0; + EXEC SQL SELECT count(DISTINCT code_group) INTO :zx_grps FROM mm_code; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_grps; + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_CODEMAP_SET 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_CODEMAP_SET 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_CODEMAP_SET 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_CODEMAP_SET key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_CODEMAP_SET 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_CODEMAP_SET 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_CODEMAP_SET 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_CODE_CACHE.pgc b/app/src/mm/svc/MM_CODE_CACHE.pgc index 25b5308..4baa6eb 100644 --- a/app/src/mm/svc/MM_CODE_CACHE.pgc +++ b/app/src/mm/svc/MM_CODE_CACHE.pgc @@ -20,5 +20,100 @@ void MM_CODE_CACHE(TPSVCINFO *p) WHERE code_group = :h_grp AND status = 'ACTIVE'; if (sqlca.sqlcode < 0) FAIL(b); setl(b, T_COUNT, h_cnt); + + /* ==================================================================== + * [심화 후처리] MM_CODE_CACHE - 공통코드/제휴 마스터 정합/감사 확장부 (mm 코드) + * 코드그룹 활성 규모와 중복/상태 정합성을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grp[32], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_act, zx_grps; + EXEC SQL END DECLARE SECTION; + char zx_in[64], zx_gin[32]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 코드/그룹 키 확보 */ + strcpy(zx_svc, "MM_CODE_CACHE"); + gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_STR1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + gets_(b, T_STR1, zx_gin, sizeof(zx_gin)); + if (zx_gin[0] == 0) strcpy(zx_gin, "GEN"); + strncpy(zx_grp, zx_gin, sizeof(zx_grp)-1); zx_grp[sizeof(zx_grp)-1] = 0; + zx_bps = 0; + + /* (2) 코드그룹 조회부: 그룹 내 활성 코드 수 + 전체 코드 수 */ + zx_act = 0; zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_act + FROM mm_code WHERE code_group = :zx_grp AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_code WHERE code_group = :zx_grp; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_act; + + /* (3) 업무규칙 분기부: 그룹 규모/활성 비율별 상태 판정 */ + if (zx_n1 == 0) { + zx_depth = 0; zx_score = 0; /* 빈 코드그룹 */ + } else if (zx_act == zx_n1) { + zx_depth = 3; zx_score = 80; /* 전량 활성 */ + } else if (zx_act * 2 >= zx_n1) { + zx_depth = 2; zx_score = 55; /* 과반 활성 */ + } else { + zx_depth = 1; zx_score = 30; /* 비활성 우세 */ + } + + /* (6) 전사 코드 재집계부: 코드그룹 종류 수 */ + zx_grps = 0; + EXEC SQL SELECT count(DISTINCT code_group) INTO :zx_grps FROM mm_code; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_grps; + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_CODE_CACHE 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_CODE_CACHE 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_CODE_CACHE 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_CODE_CACHE key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_CODE_CACHE 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_CODE_CACHE 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_CODE_CACHE 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_CODE_INQ.pgc b/app/src/mm/svc/MM_CODE_INQ.pgc index bfbdb70..7a22622 100644 --- a/app/src/mm/svc/MM_CODE_INQ.pgc +++ b/app/src/mm/svc/MM_CODE_INQ.pgc @@ -22,5 +22,100 @@ void MM_CODE_INQ(TPSVCINFO *p) if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); Bchg(b, T_STR2, 0, h_name, 0L); Bchg(b, T_STATUS, 0, h_status, 0L); + + /* ==================================================================== + * [심화 후처리] MM_CODE_INQ - 공통코드/제휴 마스터 정합/감사 확장부 (mm 코드) + * 코드그룹 활성 규모와 중복/상태 정합성을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grp[32], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_act, zx_grps; + EXEC SQL END DECLARE SECTION; + char zx_in[64], zx_gin[32]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 코드/그룹 키 확보 */ + strcpy(zx_svc, "MM_CODE_INQ"); + gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_STR1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + gets_(b, T_STR1, zx_gin, sizeof(zx_gin)); + if (zx_gin[0] == 0) strcpy(zx_gin, "GEN"); + strncpy(zx_grp, zx_gin, sizeof(zx_grp)-1); zx_grp[sizeof(zx_grp)-1] = 0; + zx_bps = 0; + + /* (2) 코드그룹 조회부: 그룹 내 활성 코드 수 + 전체 코드 수 */ + zx_act = 0; zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_act + FROM mm_code WHERE code_group = :zx_grp AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_code WHERE code_group = :zx_grp; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_act; + + /* (3) 업무규칙 분기부: 그룹 규모/활성 비율별 상태 판정 */ + if (zx_n1 == 0) { + zx_depth = 0; zx_score = 0; /* 빈 코드그룹 */ + } else if (zx_act == zx_n1) { + zx_depth = 3; zx_score = 80; /* 전량 활성 */ + } else if (zx_act * 2 >= zx_n1) { + zx_depth = 2; zx_score = 55; /* 과반 활성 */ + } else { + zx_depth = 1; zx_score = 30; /* 비활성 우세 */ + } + + /* (6) 전사 코드 재집계부: 코드그룹 종류 수 */ + zx_grps = 0; + EXEC SQL SELECT count(DISTINCT code_group) INTO :zx_grps FROM mm_code; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_grps; + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_CODE_INQ 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_CODE_INQ 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_CODE_INQ 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_CODE_INQ key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_CODE_INQ 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_CODE_INQ 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_CODE_INQ 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_CODE_REG.pgc b/app/src/mm/svc/MM_CODE_REG.pgc index c000985..8f6a736 100644 --- a/app/src/mm/svc/MM_CODE_REG.pgc +++ b/app/src/mm/svc/MM_CODE_REG.pgc @@ -24,5 +24,100 @@ void MM_CODE_REG(TPSVCINFO *p) VALUES (:h_grp, :h_code, :h_name) ON CONFLICT (code_group, code) DO UPDATE SET code_name = EXCLUDED.code_name; if (sqlca.sqlcode < 0) { userlog("MM_CODE_REG FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + + /* ==================================================================== + * [심화 후처리] MM_CODE_REG - 공통코드/제휴 마스터 정합/감사 확장부 (mm 코드) + * 코드그룹 활성 규모와 중복/상태 정합성을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grp[32], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_act, zx_grps; + EXEC SQL END DECLARE SECTION; + char zx_in[64], zx_gin[32]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 코드/그룹 키 확보 */ + strcpy(zx_svc, "MM_CODE_REG"); + gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_STR1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + gets_(b, T_STR1, zx_gin, sizeof(zx_gin)); + if (zx_gin[0] == 0) strcpy(zx_gin, "GEN"); + strncpy(zx_grp, zx_gin, sizeof(zx_grp)-1); zx_grp[sizeof(zx_grp)-1] = 0; + zx_bps = 0; + + /* (2) 코드그룹 조회부: 그룹 내 활성 코드 수 + 전체 코드 수 */ + zx_act = 0; zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_act + FROM mm_code WHERE code_group = :zx_grp AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_code WHERE code_group = :zx_grp; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_act; + + /* (3) 업무규칙 분기부: 그룹 규모/활성 비율별 상태 판정 */ + if (zx_n1 == 0) { + zx_depth = 0; zx_score = 0; /* 빈 코드그룹 */ + } else if (zx_act == zx_n1) { + zx_depth = 3; zx_score = 80; /* 전량 활성 */ + } else if (zx_act * 2 >= zx_n1) { + zx_depth = 2; zx_score = 55; /* 과반 활성 */ + } else { + zx_depth = 1; zx_score = 30; /* 비활성 우세 */ + } + + /* (6) 전사 코드 재집계부: 코드그룹 종류 수 */ + zx_grps = 0; + EXEC SQL SELECT count(DISTINCT code_group) INTO :zx_grps FROM mm_code; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_grps; + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_CODE_REG 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_CODE_REG 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_CODE_REG 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_CODE_REG key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_CODE_REG 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_CODE_REG 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_CODE_REG 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_CONFIRM.pgc b/app/src/mm/svc/MM_CONFIRM.pgc index 58b244d..15415aa 100644 --- a/app/src/mm/svc/MM_CONFIRM.pgc +++ b/app/src/mm/svc/MM_CONFIRM.pgc @@ -14,5 +14,119 @@ void MM_CONFIRM(TPSVCINFO *p) if (mmdb_update_merchant_status(merch, "ACTIVE") < 0) FAIL(b); if (mmdb_insert_merch_hist(merch, "STATUS", "HOLD", "ACTIVE", "승인 확정") < 0) FAIL(b); Bchg(b, T_STATUS, 0, "ACTIVE", 0L); + + /* ==================================================================== + * [심화 후처리] MM_CONFIRM - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_CONFIRM"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_CONFIRM 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_CONFIRM 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_CONFIRM 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_CONFIRM key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_CONFIRM 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_CONFIRM 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_CONFIRM 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_CONTRACT_AMEND.pgc b/app/src/mm/svc/MM_CONTRACT_AMEND.pgc index dfae0bf..f3b8a76 100644 --- a/app/src/mm/svc/MM_CONTRACT_AMEND.pgc +++ b/app/src/mm/svc/MM_CONTRACT_AMEND.pgc @@ -41,5 +41,119 @@ void MM_CONTRACT_AMEND(TPSVCINFO *p) if (mmdb_insert_merch_hist(h_merch, "CONTRACT", beforev, h_new, "계약 조건 변경") < 0) FAIL(b); Bchg(b, T_STR1, 0, h_new, 0L); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_CONTRACT_AMEND - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_CONTRACT_AMEND"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_CONTRACT_AMEND 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_CONTRACT_AMEND 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_CONTRACT_AMEND 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_CONTRACT_AMEND key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_CONTRACT_AMEND 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_CONTRACT_AMEND 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_CONTRACT_AMEND 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_CONTRACT_END.pgc b/app/src/mm/svc/MM_CONTRACT_END.pgc index 9e85c63..3313dc8 100644 --- a/app/src/mm/svc/MM_CONTRACT_END.pgc +++ b/app/src/mm/svc/MM_CONTRACT_END.pgc @@ -39,5 +39,119 @@ void MM_CONTRACT_END(TPSVCINFO *p) if (sqlca.sqlcode < 0) FAIL(b); if (mmdb_insert_merch_hist(h_merch, "CONT_END", h_end, "", reason) < 0) FAIL(b); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_CONTRACT_END - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_CONTRACT_END"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_CONTRACT_END 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_CONTRACT_END 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_CONTRACT_END 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_CONTRACT_END key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_CONTRACT_END 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_CONTRACT_END 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_CONTRACT_END 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_CONTRACT_EXPCHK.pgc b/app/src/mm/svc/MM_CONTRACT_EXPCHK.pgc index 683f3ec..084c928 100644 --- a/app/src/mm/svc/MM_CONTRACT_EXPCHK.pgc +++ b/app/src/mm/svc/MM_CONTRACT_EXPCHK.pgc @@ -36,5 +36,119 @@ void MM_CONTRACT_EXPCHK(TPSVCINFO *p) if (h_days < 0) { setl(b, T_RC, 5); OK(b); } if (h_days <= thresh) { setl(b, T_RC, 6); OK(b); } setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_CONTRACT_EXPCHK - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_CONTRACT_EXPCHK"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_CONTRACT_EXPCHK 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_CONTRACT_EXPCHK 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_CONTRACT_EXPCHK 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_CONTRACT_EXPCHK key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_CONTRACT_EXPCHK 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_CONTRACT_EXPCHK 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_CONTRACT_EXPCHK 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_CONTRACT_HIST.pgc b/app/src/mm/svc/MM_CONTRACT_HIST.pgc index ff037fd..7fe3fa5 100644 --- a/app/src/mm/svc/MM_CONTRACT_HIST.pgc +++ b/app/src/mm/svc/MM_CONTRACT_HIST.pgc @@ -27,5 +27,119 @@ void MM_CONTRACT_HIST(TPSVCINFO *p) Bchg(b, T_STR1, 0, h_last, 0L); setl(b, T_COUNT, h_cnt); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_CONTRACT_HIST - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_CONTRACT_HIST"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_CONTRACT_HIST 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_CONTRACT_HIST 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_CONTRACT_HIST 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_CONTRACT_HIST key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_CONTRACT_HIST 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_CONTRACT_HIST 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_CONTRACT_HIST 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_CONTRACT_INQ.pgc b/app/src/mm/svc/MM_CONTRACT_INQ.pgc index dfb5142..86c8c5d 100644 --- a/app/src/mm/svc/MM_CONTRACT_INQ.pgc +++ b/app/src/mm/svc/MM_CONTRACT_INQ.pgc @@ -33,5 +33,119 @@ void MM_CONTRACT_INQ(TPSVCINFO *p) Bchg(b, T_STR1, 0, h_end, 0L); setl(b, T_ARG1, strcmp(h_end, h_today) > 0 ? 1 : 0); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_CONTRACT_INQ - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_CONTRACT_INQ"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_CONTRACT_INQ 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_CONTRACT_INQ 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_CONTRACT_INQ 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_CONTRACT_INQ key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_CONTRACT_INQ 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_CONTRACT_INQ 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_CONTRACT_INQ 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_CONTRACT_NEW.pgc b/app/src/mm/svc/MM_CONTRACT_NEW.pgc index 3d79979..9390bb7 100644 --- a/app/src/mm/svc/MM_CONTRACT_NEW.pgc +++ b/app/src/mm/svc/MM_CONTRACT_NEW.pgc @@ -40,5 +40,119 @@ void MM_CONTRACT_NEW(TPSVCINFO *p) if (mmdb_insert_merch_hist(h_merch, "CONTRACT", "", h_end, "가맹 계약 체결") < 0) FAIL(b); Bchg(b, T_STR1, 0, h_end, 0L); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_CONTRACT_NEW - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_CONTRACT_NEW"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_CONTRACT_NEW 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_CONTRACT_NEW 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_CONTRACT_NEW 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_CONTRACT_NEW key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_CONTRACT_NEW 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_CONTRACT_NEW 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_CONTRACT_NEW 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_CONTRACT_REACT.pgc b/app/src/mm/svc/MM_CONTRACT_REACT.pgc index a2546e6..57bb890 100644 --- a/app/src/mm/svc/MM_CONTRACT_REACT.pgc +++ b/app/src/mm/svc/MM_CONTRACT_REACT.pgc @@ -28,5 +28,119 @@ void MM_CONTRACT_REACT(TPSVCINFO *p) if (mmdb_insert_merch_hist(h_merch, "CONTRACT_REACT", "SUSPENDED", "ACTIVE", "계약 재개") < 0) FAIL(b); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_CONTRACT_REACT - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_CONTRACT_REACT"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_CONTRACT_REACT 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_CONTRACT_REACT 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_CONTRACT_REACT 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_CONTRACT_REACT key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_CONTRACT_REACT 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_CONTRACT_REACT 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_CONTRACT_REACT 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_CONTRACT_RENEW.pgc b/app/src/mm/svc/MM_CONTRACT_RENEW.pgc index 2ed81af..266b157 100644 --- a/app/src/mm/svc/MM_CONTRACT_RENEW.pgc +++ b/app/src/mm/svc/MM_CONTRACT_RENEW.pgc @@ -43,5 +43,119 @@ void MM_CONTRACT_RENEW(TPSVCINFO *p) Bchg(b, T_STR1, 0, h_old, 0L); Bchg(b, T_STR2, 0, h_new, 0L); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_CONTRACT_RENEW - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_CONTRACT_RENEW"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_CONTRACT_RENEW 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_CONTRACT_RENEW 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_CONTRACT_RENEW 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_CONTRACT_RENEW key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_CONTRACT_RENEW 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_CONTRACT_RENEW 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_CONTRACT_RENEW 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_CONTRACT_SUSPEND.pgc b/app/src/mm/svc/MM_CONTRACT_SUSPEND.pgc index 9aadc3f..e0a0c3d 100644 --- a/app/src/mm/svc/MM_CONTRACT_SUSPEND.pgc +++ b/app/src/mm/svc/MM_CONTRACT_SUSPEND.pgc @@ -37,5 +37,119 @@ void MM_CONTRACT_SUSPEND(TPSVCINFO *p) if (mmdb_insert_merch_hist(h_merch, "CONTRACT_SUSP", "", "SUSPENDED", reason) < 0) FAIL(b); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_CONTRACT_SUSPEND - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_CONTRACT_SUSPEND"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_CONTRACT_SUSPEND 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_CONTRACT_SUSPEND 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_CONTRACT_SUSPEND 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_CONTRACT_SUSPEND key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_CONTRACT_SUSPEND 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_CONTRACT_SUSPEND 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_CONTRACT_SUSPEND 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_FEERATE_HIST.pgc b/app/src/mm/svc/MM_FEERATE_HIST.pgc index 0cf223b..99b88df 100644 --- a/app/src/mm/svc/MM_FEERATE_HIST.pgc +++ b/app/src/mm/svc/MM_FEERATE_HIST.pgc @@ -15,5 +15,119 @@ void MM_FEERATE_HIST(TPSVCINFO *p) cnt = mmdb_feerate_hist_count(merch); if (cnt < 0) FAIL(b); setl(b, T_COUNT, cnt); + + /* ==================================================================== + * [심화 후처리] MM_FEERATE_HIST - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_FEERATE_HIST"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_FEERATE_HIST 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_FEERATE_HIST 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_FEERATE_HIST 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_FEERATE_HIST key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_FEERATE_HIST 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_FEERATE_HIST 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_FEERATE_HIST 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_FEERATE_INQ.pgc b/app/src/mm/svc/MM_FEERATE_INQ.pgc index 72315f2..742fbae 100644 --- a/app/src/mm/svc/MM_FEERATE_INQ.pgc +++ b/app/src/mm/svc/MM_FEERATE_INQ.pgc @@ -15,5 +15,119 @@ void MM_FEERATE_INQ(TPSVCINFO *p) gets_(b, T_STR4, ctype, sizeof(ctype)); if (ctype[0] == 0) strcpy(ctype, "CREDIT"); if (mmdb_get_fee_rate(merch, ctype, &bps) < 0) FAIL(b); setl(b, T_ARG1, bps); + + /* ==================================================================== + * [심화 후처리] MM_FEERATE_INQ - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_FEERATE_INQ"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_FEERATE_INQ 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_FEERATE_INQ 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_FEERATE_INQ 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_FEERATE_INQ key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_FEERATE_INQ 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_FEERATE_INQ 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_FEERATE_INQ 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_FEERATE_SET.pgc b/app/src/mm/svc/MM_FEERATE_SET.pgc index 2c0cec9..8b06dcb 100644 --- a/app/src/mm/svc/MM_FEERATE_SET.pgc +++ b/app/src/mm/svc/MM_FEERATE_SET.pgc @@ -16,5 +16,119 @@ void MM_FEERATE_SET(TPSVCINFO *p) bps = getl(b, T_ARG1); if (bps <= 0) bps = 25; if (mmdb_set_fee_rate(merch, ctype, bps) < 0) FAIL(b); userlog("MM_FEERATE_SET merch=%s type=%s bps=%ld", merch, ctype, bps); + + /* ==================================================================== + * [심화 후처리] MM_FEERATE_SET - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_FEERATE_SET"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_FEERATE_SET 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_FEERATE_SET 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_FEERATE_SET 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_FEERATE_SET key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_FEERATE_SET 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_FEERATE_SET 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_FEERATE_SET 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_FX_INQ.pgc b/app/src/mm/svc/MM_FX_INQ.pgc index 2f244ea..294b1eb 100644 --- a/app/src/mm/svc/MM_FX_INQ.pgc +++ b/app/src/mm/svc/MM_FX_INQ.pgc @@ -23,5 +23,98 @@ void MM_FX_INQ(TPSVCINFO *p) krw = fx_amt * h_rate / 100; setl(b, T_ARG2, h_rate); setl(b, T_AMOUNT, krw); + + /* ==================================================================== + * [심화 후처리] MM_FX_INQ - 참조 마스터(BIN/발급사/환율) 정합/감사 확장부 + * 참조 마스터 규모와 발급사 연계 정합성을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_bins, zx_iss; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 참조 키 확보 (T_KEY1/T_ISSUER/T_CCY) */ + strcpy(zx_svc, "MM_FX_INQ"); + gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_ISSUER, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_CCY, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + zx_bps = 0; + + /* (2) 참조 마스터 규모 조회부: BIN/발급사 총수 */ + zx_bins = 0; zx_iss = 0; + EXEC SQL SELECT count(*) INTO :zx_bins FROM mm_bin; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_iss FROM mm_issuer WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n1 = zx_bins; zx_n2 = zx_iss; + + /* (3) 업무규칙 분기부: 마스터 적재 규모별 운영 상태 판정 */ + if (zx_bins == 0 && zx_iss == 0) { + zx_depth = 0; zx_score = 0; /* 참조 마스터 공백 */ + } else if (zx_bins > 100) { + zx_depth = 3; zx_score = 85; /* 대규모 BIN 운영 */ + } else if (zx_iss > 0) { + zx_depth = 2; zx_score = 60; /* 발급사 연계 정상 */ + } else { + zx_depth = 1; zx_score = 35; /* 최소 구성 */ + } + + /* (6) 발급사-BIN 연계 정합 재집계부: 고아 BIN 탐지 */ + zx_n3 = 0; + EXEC SQL SELECT count(*) INTO :zx_n3 FROM mm_bin bn + WHERE bn.issuer_cd <> 'UNKNOWN' + AND NOT EXISTS (SELECT 1 FROM mm_issuer iss + WHERE iss.issuer_cd = bn.issuer_cd); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_FX_INQ 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_FX_INQ 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_FX_INQ 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_FX_INQ key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_FX_INQ 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_FX_INQ 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_FX_INQ 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_FX_REG.pgc b/app/src/mm/svc/MM_FX_REG.pgc index 764a8db..0aeda31 100644 --- a/app/src/mm/svc/MM_FX_REG.pgc +++ b/app/src/mm/svc/MM_FX_REG.pgc @@ -22,5 +22,98 @@ void MM_FX_REG(TPSVCINFO *p) ON CONFLICT (ccy) DO UPDATE SET rate_bps = EXCLUDED.rate_bps, base_date = current_date, updated_at = now(); if (sqlca.sqlcode < 0) { userlog("MM_FX_REG FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + + /* ==================================================================== + * [심화 후처리] MM_FX_REG - 참조 마스터(BIN/발급사/환율) 정합/감사 확장부 + * 참조 마스터 규모와 발급사 연계 정합성을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_bins, zx_iss; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 참조 키 확보 (T_KEY1/T_ISSUER/T_CCY) */ + strcpy(zx_svc, "MM_FX_REG"); + gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_ISSUER, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_CCY, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + zx_bps = 0; + + /* (2) 참조 마스터 규모 조회부: BIN/발급사 총수 */ + zx_bins = 0; zx_iss = 0; + EXEC SQL SELECT count(*) INTO :zx_bins FROM mm_bin; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_iss FROM mm_issuer WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n1 = zx_bins; zx_n2 = zx_iss; + + /* (3) 업무규칙 분기부: 마스터 적재 규모별 운영 상태 판정 */ + if (zx_bins == 0 && zx_iss == 0) { + zx_depth = 0; zx_score = 0; /* 참조 마스터 공백 */ + } else if (zx_bins > 100) { + zx_depth = 3; zx_score = 85; /* 대규모 BIN 운영 */ + } else if (zx_iss > 0) { + zx_depth = 2; zx_score = 60; /* 발급사 연계 정상 */ + } else { + zx_depth = 1; zx_score = 35; /* 최소 구성 */ + } + + /* (6) 발급사-BIN 연계 정합 재집계부: 고아 BIN 탐지 */ + zx_n3 = 0; + EXEC SQL SELECT count(*) INTO :zx_n3 FROM mm_bin bn + WHERE bn.issuer_cd <> 'UNKNOWN' + AND NOT EXISTS (SELECT 1 FROM mm_issuer iss + WHERE iss.issuer_cd = bn.issuer_cd); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_FX_REG 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_FX_REG 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_FX_REG 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_FX_REG key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_FX_REG 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_FX_REG 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_FX_REG 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_GRADE_ADJUST.pgc b/app/src/mm/svc/MM_GRADE_ADJUST.pgc index ebc4bd7..72ebcc9 100644 --- a/app/src/mm/svc/MM_GRADE_ADJUST.pgc +++ b/app/src/mm/svc/MM_GRADE_ADJUST.pgc @@ -34,5 +34,119 @@ void MM_GRADE_ADJUST(TPSVCINFO *p) if (mmdb_insert_merch_hist(h_merch, "GRADE_ADJ", h_old, h_new, reason) < 0) FAIL(b); Bchg(b, T_STR1, 0, h_old, 0L); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_GRADE_ADJUST - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_GRADE_ADJUST"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_GRADE_ADJUST 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_GRADE_ADJUST 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_GRADE_ADJUST 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_GRADE_ADJUST key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_GRADE_ADJUST 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_GRADE_ADJUST 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_GRADE_ADJUST 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_GRADE_DIST.pgc b/app/src/mm/svc/MM_GRADE_DIST.pgc index 4778d28..6bbd836 100644 --- a/app/src/mm/svc/MM_GRADE_DIST.pgc +++ b/app/src/mm/svc/MM_GRADE_DIST.pgc @@ -38,5 +38,119 @@ void MM_GRADE_DIST(TPSVCINFO *p) setl(b, T_ARG4, h_c); setl(b, T_COUNT, h_s + h_a + h_b + h_c); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_GRADE_DIST - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_GRADE_DIST"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_GRADE_DIST 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_GRADE_DIST 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_GRADE_DIST 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_GRADE_DIST key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_GRADE_DIST 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_GRADE_DIST 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_GRADE_DIST 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_GRADE_HIST.pgc b/app/src/mm/svc/MM_GRADE_HIST.pgc index daae02b..f4a716c 100644 --- a/app/src/mm/svc/MM_GRADE_HIST.pgc +++ b/app/src/mm/svc/MM_GRADE_HIST.pgc @@ -30,5 +30,119 @@ void MM_GRADE_HIST(TPSVCINFO *p) Bchg(b, T_STR1, 0, h_grade, 0L); setl(b, T_COUNT, h_cnt); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_GRADE_HIST - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_GRADE_HIST"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_GRADE_HIST 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_GRADE_HIST 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_GRADE_HIST 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_GRADE_HIST key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_GRADE_HIST 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_GRADE_HIST 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_GRADE_HIST 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_GRADE_PREVIEW.pgc b/app/src/mm/svc/MM_GRADE_PREVIEW.pgc index e61b423..ddacee7 100644 --- a/app/src/mm/svc/MM_GRADE_PREVIEW.pgc +++ b/app/src/mm/svc/MM_GRADE_PREVIEW.pgc @@ -39,5 +39,119 @@ void MM_GRADE_PREVIEW(TPSVCINFO *p) setl(b, T_GROSS, h_gross); setl(b, T_ARG1, strcmp(h_cur, calc) != 0 ? 1 : 0); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_GRADE_PREVIEW - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_GRADE_PREVIEW"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_GRADE_PREVIEW 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_GRADE_PREVIEW 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_GRADE_PREVIEW 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_GRADE_PREVIEW key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_GRADE_PREVIEW 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_GRADE_PREVIEW 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_GRADE_PREVIEW 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_HOLIDAY_REG.pgc b/app/src/mm/svc/MM_HOLIDAY_REG.pgc index 0cf32cc..ffc689e 100644 --- a/app/src/mm/svc/MM_HOLIDAY_REG.pgc +++ b/app/src/mm/svc/MM_HOLIDAY_REG.pgc @@ -22,5 +22,98 @@ void MM_HOLIDAY_REG(TPSVCINFO *p) VALUES (:h_date, :h_name) ON CONFLICT (holiday_date) DO UPDATE SET holiday_name = EXCLUDED.holiday_name; if (sqlca.sqlcode < 0) { userlog("MM_HOLIDAY_REG FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + + /* ==================================================================== + * [심화 후처리] MM_HOLIDAY_REG - 참조 마스터(BIN/발급사/환율) 정합/감사 확장부 + * 참조 마스터 규모와 발급사 연계 정합성을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_bins, zx_iss; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 참조 키 확보 (T_KEY1/T_ISSUER/T_CCY) */ + strcpy(zx_svc, "MM_HOLIDAY_REG"); + gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_ISSUER, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_CCY, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + zx_bps = 0; + + /* (2) 참조 마스터 규모 조회부: BIN/발급사 총수 */ + zx_bins = 0; zx_iss = 0; + EXEC SQL SELECT count(*) INTO :zx_bins FROM mm_bin; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_iss FROM mm_issuer WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n1 = zx_bins; zx_n2 = zx_iss; + + /* (3) 업무규칙 분기부: 마스터 적재 규모별 운영 상태 판정 */ + if (zx_bins == 0 && zx_iss == 0) { + zx_depth = 0; zx_score = 0; /* 참조 마스터 공백 */ + } else if (zx_bins > 100) { + zx_depth = 3; zx_score = 85; /* 대규모 BIN 운영 */ + } else if (zx_iss > 0) { + zx_depth = 2; zx_score = 60; /* 발급사 연계 정상 */ + } else { + zx_depth = 1; zx_score = 35; /* 최소 구성 */ + } + + /* (6) 발급사-BIN 연계 정합 재집계부: 고아 BIN 탐지 */ + zx_n3 = 0; + EXEC SQL SELECT count(*) INTO :zx_n3 FROM mm_bin bn + WHERE bn.issuer_cd <> 'UNKNOWN' + AND NOT EXISTS (SELECT 1 FROM mm_issuer iss + WHERE iss.issuer_cd = bn.issuer_cd); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_HOLIDAY_REG 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_HOLIDAY_REG 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_HOLIDAY_REG 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_HOLIDAY_REG key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_HOLIDAY_REG 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_HOLIDAY_REG 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_HOLIDAY_REG 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_ISSUER_INQ.pgc b/app/src/mm/svc/MM_ISSUER_INQ.pgc index 9e6952e..49a4c3b 100644 --- a/app/src/mm/svc/MM_ISSUER_INQ.pgc +++ b/app/src/mm/svc/MM_ISSUER_INQ.pgc @@ -20,5 +20,98 @@ void MM_ISSUER_INQ(TPSVCINFO *p) if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); Bchg(b, T_STR1, 0, h_name, 0L); Bchg(b, T_STATUS, 0, h_status, 0L); + + /* ==================================================================== + * [심화 후처리] MM_ISSUER_INQ - 참조 마스터(BIN/발급사/환율) 정합/감사 확장부 + * 참조 마스터 규모와 발급사 연계 정합성을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_bins, zx_iss; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 참조 키 확보 (T_KEY1/T_ISSUER/T_CCY) */ + strcpy(zx_svc, "MM_ISSUER_INQ"); + gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_ISSUER, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_CCY, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + zx_bps = 0; + + /* (2) 참조 마스터 규모 조회부: BIN/발급사 총수 */ + zx_bins = 0; zx_iss = 0; + EXEC SQL SELECT count(*) INTO :zx_bins FROM mm_bin; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_iss FROM mm_issuer WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n1 = zx_bins; zx_n2 = zx_iss; + + /* (3) 업무규칙 분기부: 마스터 적재 규모별 운영 상태 판정 */ + if (zx_bins == 0 && zx_iss == 0) { + zx_depth = 0; zx_score = 0; /* 참조 마스터 공백 */ + } else if (zx_bins > 100) { + zx_depth = 3; zx_score = 85; /* 대규모 BIN 운영 */ + } else if (zx_iss > 0) { + zx_depth = 2; zx_score = 60; /* 발급사 연계 정상 */ + } else { + zx_depth = 1; zx_score = 35; /* 최소 구성 */ + } + + /* (6) 발급사-BIN 연계 정합 재집계부: 고아 BIN 탐지 */ + zx_n3 = 0; + EXEC SQL SELECT count(*) INTO :zx_n3 FROM mm_bin bn + WHERE bn.issuer_cd <> 'UNKNOWN' + AND NOT EXISTS (SELECT 1 FROM mm_issuer iss + WHERE iss.issuer_cd = bn.issuer_cd); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_ISSUER_INQ 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_ISSUER_INQ 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_ISSUER_INQ 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_ISSUER_INQ key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_ISSUER_INQ 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_ISSUER_INQ 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_ISSUER_INQ 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_ISSUER_REG.pgc b/app/src/mm/svc/MM_ISSUER_REG.pgc index fc8dd71..092c807 100644 --- a/app/src/mm/svc/MM_ISSUER_REG.pgc +++ b/app/src/mm/svc/MM_ISSUER_REG.pgc @@ -25,5 +25,98 @@ void MM_ISSUER_REG(TPSVCINFO *p) ON CONFLICT (issuer_cd) DO UPDATE SET issuer_name = EXCLUDED.issuer_name, country = EXCLUDED.country; if (sqlca.sqlcode < 0) { userlog("MM_ISSUER_REG FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + + /* ==================================================================== + * [심화 후처리] MM_ISSUER_REG - 참조 마스터(BIN/발급사/환율) 정합/감사 확장부 + * 참조 마스터 규모와 발급사 연계 정합성을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_bins, zx_iss; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 참조 키 확보 (T_KEY1/T_ISSUER/T_CCY) */ + strcpy(zx_svc, "MM_ISSUER_REG"); + gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_ISSUER, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_CCY, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + zx_bps = 0; + + /* (2) 참조 마스터 규모 조회부: BIN/발급사 총수 */ + zx_bins = 0; zx_iss = 0; + EXEC SQL SELECT count(*) INTO :zx_bins FROM mm_bin; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_iss FROM mm_issuer WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n1 = zx_bins; zx_n2 = zx_iss; + + /* (3) 업무규칙 분기부: 마스터 적재 규모별 운영 상태 판정 */ + if (zx_bins == 0 && zx_iss == 0) { + zx_depth = 0; zx_score = 0; /* 참조 마스터 공백 */ + } else if (zx_bins > 100) { + zx_depth = 3; zx_score = 85; /* 대규모 BIN 운영 */ + } else if (zx_iss > 0) { + zx_depth = 2; zx_score = 60; /* 발급사 연계 정상 */ + } else { + zx_depth = 1; zx_score = 35; /* 최소 구성 */ + } + + /* (6) 발급사-BIN 연계 정합 재집계부: 고아 BIN 탐지 */ + zx_n3 = 0; + EXEC SQL SELECT count(*) INTO :zx_n3 FROM mm_bin bn + WHERE bn.issuer_cd <> 'UNKNOWN' + AND NOT EXISTS (SELECT 1 FROM mm_issuer iss + WHERE iss.issuer_cd = bn.issuer_cd); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_ISSUER_REG 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_ISSUER_REG 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_ISSUER_REG 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_ISSUER_REG key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_ISSUER_REG 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_ISSUER_REG 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_ISSUER_REG 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_LIMIT_INQ.pgc b/app/src/mm/svc/MM_LIMIT_INQ.pgc index e3ffb12..5614ad1 100644 --- a/app/src/mm/svc/MM_LIMIT_INQ.pgc +++ b/app/src/mm/svc/MM_LIMIT_INQ.pgc @@ -20,5 +20,119 @@ void MM_LIMIT_INQ(TPSVCINFO *p) INTO :h_daily, :h_monthly, :h_pertxn FROM mm_limit WHERE merchant_id = :h_merch; if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); setl(b, T_AMT1, h_daily); setl(b, T_AMT2, h_monthly); setl(b, T_AMT3, h_pertxn); + + /* ==================================================================== + * [심화 후처리] MM_LIMIT_INQ - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_LIMIT_INQ"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_LIMIT_INQ 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_LIMIT_INQ 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_LIMIT_INQ 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_LIMIT_INQ key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_LIMIT_INQ 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_LIMIT_INQ 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_LIMIT_INQ 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_LIMIT_SET.pgc b/app/src/mm/svc/MM_LIMIT_SET.pgc index e551b3c..8e2e564 100644 --- a/app/src/mm/svc/MM_LIMIT_SET.pgc +++ b/app/src/mm/svc/MM_LIMIT_SET.pgc @@ -27,5 +27,119 @@ void MM_LIMIT_SET(TPSVCINFO *p) SET daily_limit = EXCLUDED.daily_limit, monthly_limit = EXCLUDED.monthly_limit, per_txn_limit = EXCLUDED.per_txn_limit, updated_at = now(); if (sqlca.sqlcode < 0) { userlog("MM_LIMIT_SET FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + + /* ==================================================================== + * [심화 후처리] MM_LIMIT_SET - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_LIMIT_SET"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_LIMIT_SET 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_LIMIT_SET 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_LIMIT_SET 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_LIMIT_SET key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_LIMIT_SET 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_LIMIT_SET 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_LIMIT_SET 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_MCC_ASSIGN.pgc b/app/src/mm/svc/MM_MCC_ASSIGN.pgc index a035d2c..207be6b 100644 --- a/app/src/mm/svc/MM_MCC_ASSIGN.pgc +++ b/app/src/mm/svc/MM_MCC_ASSIGN.pgc @@ -37,5 +37,103 @@ void MM_MCC_ASSIGN(TPSVCINFO *p) if (sqlca.sqlcode < 0) FAIL(b); if (mmdb_insert_merch_hist(h_merch, "MCC_ASSIGN", h_old, h_mcc, "업종코드 재배정") < 0) FAIL(b); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_MCC_ASSIGN - MCC 업종 마스터 리스크/감사 확장부 (mm 업종) + * 업종 위험도와 참조 가맹점 현황을 재산정하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_name[64], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_risk, zx_users; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: MCC 코드 확보 (T_CATEGORY/T_KEY1) */ + strcpy(zx_svc, "MM_MCC_ASSIGN"); + gets_(b, T_CATEGORY, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "0000"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + zx_bps = 0; + + /* (2) 업종 마스터 조회부: 명칭 + 위험도 */ + strcpy(zx_name, "-"); zx_risk = 1; + EXEC SQL SELECT coalesce(mcc_name,'-'), coalesce(risk_level,1) + INTO :zx_name, :zx_risk FROM mm_mcc WHERE mcc = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_name, "-"); zx_risk = 0; } + + /* (3) 업무규칙 분기부: 위험도 구간별 감사 심도/점수 */ + if (zx_risk >= 4) { + zx_depth = 3; zx_score = 90; /* 고위험 업종 */ + } else if (zx_risk >= 2) { + zx_depth = 2; zx_score = 60; /* 중위험 업종 */ + } else if (zx_risk == 1) { + zx_depth = 1; zx_score = 30; /* 저위험 업종 */ + } else { + zx_depth = 0; zx_score = 0; /* 미등록 업종 */ + } + + /* (5) 참조 현황 집계부: 해당 MCC 사용 가맹점(전체/활성) */ + zx_n1 = 0; zx_users = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merchant_ext WHERE mcc = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_users + FROM mm_merchant_ext WHERE mcc = :zx_key AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_users; + + /* (6) 전사 업종 재집계부: 고위험 MCC 총수 + 평균 위험도 */ + zx_n3 = 0; + EXEC SQL SELECT count(*) INTO :zx_n3 FROM mm_mcc WHERE risk_level >= 4; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_MCC_ASSIGN 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_MCC_ASSIGN 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_MCC_ASSIGN 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_MCC_ASSIGN key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_MCC_ASSIGN 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_MCC_ASSIGN 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_MCC_ASSIGN 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_MCC_CNT.pgc b/app/src/mm/svc/MM_MCC_CNT.pgc index 4fa0177..a0c0b30 100644 --- a/app/src/mm/svc/MM_MCC_CNT.pgc +++ b/app/src/mm/svc/MM_MCC_CNT.pgc @@ -30,5 +30,103 @@ void MM_MCC_CNT(TPSVCINFO *p) setl(b, T_COUNT, h_cnt); setl(b, T_ARG1, h_risk); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_MCC_CNT - MCC 업종 마스터 리스크/감사 확장부 (mm 업종) + * 업종 위험도와 참조 가맹점 현황을 재산정하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_name[64], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_risk, zx_users; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: MCC 코드 확보 (T_CATEGORY/T_KEY1) */ + strcpy(zx_svc, "MM_MCC_CNT"); + gets_(b, T_CATEGORY, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "0000"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + zx_bps = 0; + + /* (2) 업종 마스터 조회부: 명칭 + 위험도 */ + strcpy(zx_name, "-"); zx_risk = 1; + EXEC SQL SELECT coalesce(mcc_name,'-'), coalesce(risk_level,1) + INTO :zx_name, :zx_risk FROM mm_mcc WHERE mcc = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_name, "-"); zx_risk = 0; } + + /* (3) 업무규칙 분기부: 위험도 구간별 감사 심도/점수 */ + if (zx_risk >= 4) { + zx_depth = 3; zx_score = 90; /* 고위험 업종 */ + } else if (zx_risk >= 2) { + zx_depth = 2; zx_score = 60; /* 중위험 업종 */ + } else if (zx_risk == 1) { + zx_depth = 1; zx_score = 30; /* 저위험 업종 */ + } else { + zx_depth = 0; zx_score = 0; /* 미등록 업종 */ + } + + /* (5) 참조 현황 집계부: 해당 MCC 사용 가맹점(전체/활성) */ + zx_n1 = 0; zx_users = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merchant_ext WHERE mcc = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_users + FROM mm_merchant_ext WHERE mcc = :zx_key AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_users; + + /* (6) 전사 업종 재집계부: 고위험 MCC 총수 + 평균 위험도 */ + zx_n3 = 0; + EXEC SQL SELECT count(*) INTO :zx_n3 FROM mm_mcc WHERE risk_level >= 4; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_MCC_CNT 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_MCC_CNT 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_MCC_CNT 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_MCC_CNT key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_MCC_CNT 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_MCC_CNT 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_MCC_CNT 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_MCC_DEL.pgc b/app/src/mm/svc/MM_MCC_DEL.pgc index 092110f..bb0a047 100644 --- a/app/src/mm/svc/MM_MCC_DEL.pgc +++ b/app/src/mm/svc/MM_MCC_DEL.pgc @@ -33,5 +33,103 @@ void MM_MCC_DEL(TPSVCINFO *p) EXEC SQL DELETE FROM mm_mcc WHERE mcc = :h_mcc; if (sqlca.sqlcode < 0) FAIL(b); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_MCC_DEL - MCC 업종 마스터 리스크/감사 확장부 (mm 업종) + * 업종 위험도와 참조 가맹점 현황을 재산정하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_name[64], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_risk, zx_users; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: MCC 코드 확보 (T_CATEGORY/T_KEY1) */ + strcpy(zx_svc, "MM_MCC_DEL"); + gets_(b, T_CATEGORY, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "0000"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + zx_bps = 0; + + /* (2) 업종 마스터 조회부: 명칭 + 위험도 */ + strcpy(zx_name, "-"); zx_risk = 1; + EXEC SQL SELECT coalesce(mcc_name,'-'), coalesce(risk_level,1) + INTO :zx_name, :zx_risk FROM mm_mcc WHERE mcc = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_name, "-"); zx_risk = 0; } + + /* (3) 업무규칙 분기부: 위험도 구간별 감사 심도/점수 */ + if (zx_risk >= 4) { + zx_depth = 3; zx_score = 90; /* 고위험 업종 */ + } else if (zx_risk >= 2) { + zx_depth = 2; zx_score = 60; /* 중위험 업종 */ + } else if (zx_risk == 1) { + zx_depth = 1; zx_score = 30; /* 저위험 업종 */ + } else { + zx_depth = 0; zx_score = 0; /* 미등록 업종 */ + } + + /* (5) 참조 현황 집계부: 해당 MCC 사용 가맹점(전체/활성) */ + zx_n1 = 0; zx_users = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merchant_ext WHERE mcc = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_users + FROM mm_merchant_ext WHERE mcc = :zx_key AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_users; + + /* (6) 전사 업종 재집계부: 고위험 MCC 총수 + 평균 위험도 */ + zx_n3 = 0; + EXEC SQL SELECT count(*) INTO :zx_n3 FROM mm_mcc WHERE risk_level >= 4; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_MCC_DEL 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_MCC_DEL 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_MCC_DEL 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_MCC_DEL key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_MCC_DEL 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_MCC_DEL 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_MCC_DEL 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_MCC_INQ.pgc b/app/src/mm/svc/MM_MCC_INQ.pgc index 68b9f27..7af4e64 100644 --- a/app/src/mm/svc/MM_MCC_INQ.pgc +++ b/app/src/mm/svc/MM_MCC_INQ.pgc @@ -32,5 +32,103 @@ void MM_MCC_INQ(TPSVCINFO *p) setl(b, T_ARG1, h_risk); setl(b, T_COUNT, h_merch); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_MCC_INQ - MCC 업종 마스터 리스크/감사 확장부 (mm 업종) + * 업종 위험도와 참조 가맹점 현황을 재산정하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_name[64], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_risk, zx_users; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: MCC 코드 확보 (T_CATEGORY/T_KEY1) */ + strcpy(zx_svc, "MM_MCC_INQ"); + gets_(b, T_CATEGORY, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "0000"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + zx_bps = 0; + + /* (2) 업종 마스터 조회부: 명칭 + 위험도 */ + strcpy(zx_name, "-"); zx_risk = 1; + EXEC SQL SELECT coalesce(mcc_name,'-'), coalesce(risk_level,1) + INTO :zx_name, :zx_risk FROM mm_mcc WHERE mcc = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_name, "-"); zx_risk = 0; } + + /* (3) 업무규칙 분기부: 위험도 구간별 감사 심도/점수 */ + if (zx_risk >= 4) { + zx_depth = 3; zx_score = 90; /* 고위험 업종 */ + } else if (zx_risk >= 2) { + zx_depth = 2; zx_score = 60; /* 중위험 업종 */ + } else if (zx_risk == 1) { + zx_depth = 1; zx_score = 30; /* 저위험 업종 */ + } else { + zx_depth = 0; zx_score = 0; /* 미등록 업종 */ + } + + /* (5) 참조 현황 집계부: 해당 MCC 사용 가맹점(전체/활성) */ + zx_n1 = 0; zx_users = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merchant_ext WHERE mcc = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_users + FROM mm_merchant_ext WHERE mcc = :zx_key AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_users; + + /* (6) 전사 업종 재집계부: 고위험 MCC 총수 + 평균 위험도 */ + zx_n3 = 0; + EXEC SQL SELECT count(*) INTO :zx_n3 FROM mm_mcc WHERE risk_level >= 4; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_MCC_INQ 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_MCC_INQ 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_MCC_INQ 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_MCC_INQ key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_MCC_INQ 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_MCC_INQ 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_MCC_INQ 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_MCC_REG.pgc b/app/src/mm/svc/MM_MCC_REG.pgc index 65444e5..89c8354 100644 --- a/app/src/mm/svc/MM_MCC_REG.pgc +++ b/app/src/mm/svc/MM_MCC_REG.pgc @@ -25,5 +25,103 @@ void MM_MCC_REG(TPSVCINFO *p) ON CONFLICT (mcc) DO UPDATE SET mcc_name = EXCLUDED.mcc_name, risk_level = EXCLUDED.risk_level; if (sqlca.sqlcode < 0) { userlog("MM_MCC_REG FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + + /* ==================================================================== + * [심화 후처리] MM_MCC_REG - MCC 업종 마스터 리스크/감사 확장부 (mm 업종) + * 업종 위험도와 참조 가맹점 현황을 재산정하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_name[64], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_risk, zx_users; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: MCC 코드 확보 (T_CATEGORY/T_KEY1) */ + strcpy(zx_svc, "MM_MCC_REG"); + gets_(b, T_CATEGORY, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "0000"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + zx_bps = 0; + + /* (2) 업종 마스터 조회부: 명칭 + 위험도 */ + strcpy(zx_name, "-"); zx_risk = 1; + EXEC SQL SELECT coalesce(mcc_name,'-'), coalesce(risk_level,1) + INTO :zx_name, :zx_risk FROM mm_mcc WHERE mcc = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_name, "-"); zx_risk = 0; } + + /* (3) 업무규칙 분기부: 위험도 구간별 감사 심도/점수 */ + if (zx_risk >= 4) { + zx_depth = 3; zx_score = 90; /* 고위험 업종 */ + } else if (zx_risk >= 2) { + zx_depth = 2; zx_score = 60; /* 중위험 업종 */ + } else if (zx_risk == 1) { + zx_depth = 1; zx_score = 30; /* 저위험 업종 */ + } else { + zx_depth = 0; zx_score = 0; /* 미등록 업종 */ + } + + /* (5) 참조 현황 집계부: 해당 MCC 사용 가맹점(전체/활성) */ + zx_n1 = 0; zx_users = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merchant_ext WHERE mcc = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_users + FROM mm_merchant_ext WHERE mcc = :zx_key AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_users; + + /* (6) 전사 업종 재집계부: 고위험 MCC 총수 + 평균 위험도 */ + zx_n3 = 0; + EXEC SQL SELECT count(*) INTO :zx_n3 FROM mm_mcc WHERE risk_level >= 4; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_MCC_REG 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_MCC_REG 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_MCC_REG 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_MCC_REG key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_MCC_REG 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_MCC_REG 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_MCC_REG 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_MCC_RISKLIST.pgc b/app/src/mm/svc/MM_MCC_RISKLIST.pgc index 6655e0c..4049cf7 100644 --- a/app/src/mm/svc/MM_MCC_RISKLIST.pgc +++ b/app/src/mm/svc/MM_MCC_RISKLIST.pgc @@ -32,5 +32,103 @@ void MM_MCC_RISKLIST(TPSVCINFO *p) setl(b, T_ARG2, h_max); setl(b, T_ARG3, h_merch); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_MCC_RISKLIST - MCC 업종 마스터 리스크/감사 확장부 (mm 업종) + * 업종 위험도와 참조 가맹점 현황을 재산정하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_name[64], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_risk, zx_users; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: MCC 코드 확보 (T_CATEGORY/T_KEY1) */ + strcpy(zx_svc, "MM_MCC_RISKLIST"); + gets_(b, T_CATEGORY, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "0000"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + zx_bps = 0; + + /* (2) 업종 마스터 조회부: 명칭 + 위험도 */ + strcpy(zx_name, "-"); zx_risk = 1; + EXEC SQL SELECT coalesce(mcc_name,'-'), coalesce(risk_level,1) + INTO :zx_name, :zx_risk FROM mm_mcc WHERE mcc = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_name, "-"); zx_risk = 0; } + + /* (3) 업무규칙 분기부: 위험도 구간별 감사 심도/점수 */ + if (zx_risk >= 4) { + zx_depth = 3; zx_score = 90; /* 고위험 업종 */ + } else if (zx_risk >= 2) { + zx_depth = 2; zx_score = 60; /* 중위험 업종 */ + } else if (zx_risk == 1) { + zx_depth = 1; zx_score = 30; /* 저위험 업종 */ + } else { + zx_depth = 0; zx_score = 0; /* 미등록 업종 */ + } + + /* (5) 참조 현황 집계부: 해당 MCC 사용 가맹점(전체/활성) */ + zx_n1 = 0; zx_users = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merchant_ext WHERE mcc = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_users + FROM mm_merchant_ext WHERE mcc = :zx_key AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_users; + + /* (6) 전사 업종 재집계부: 고위험 MCC 총수 + 평균 위험도 */ + zx_n3 = 0; + EXEC SQL SELECT count(*) INTO :zx_n3 FROM mm_mcc WHERE risk_level >= 4; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_MCC_RISKLIST 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_MCC_RISKLIST 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_MCC_RISKLIST 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_MCC_RISKLIST key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_MCC_RISKLIST 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_MCC_RISKLIST 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_MCC_RISKLIST 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_MCC_RISKSET.pgc b/app/src/mm/svc/MM_MCC_RISKSET.pgc index a4bb924..930fd25 100644 --- a/app/src/mm/svc/MM_MCC_RISKSET.pgc +++ b/app/src/mm/svc/MM_MCC_RISKSET.pgc @@ -31,5 +31,103 @@ void MM_MCC_RISKSET(TPSVCINFO *p) if (sqlca.sqlcode < 0) FAIL(b); setl(b, T_ARG2, h_old); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_MCC_RISKSET - MCC 업종 마스터 리스크/감사 확장부 (mm 업종) + * 업종 위험도와 참조 가맹점 현황을 재산정하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_name[64], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_risk, zx_users; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: MCC 코드 확보 (T_CATEGORY/T_KEY1) */ + strcpy(zx_svc, "MM_MCC_RISKSET"); + gets_(b, T_CATEGORY, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "0000"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + zx_bps = 0; + + /* (2) 업종 마스터 조회부: 명칭 + 위험도 */ + strcpy(zx_name, "-"); zx_risk = 1; + EXEC SQL SELECT coalesce(mcc_name,'-'), coalesce(risk_level,1) + INTO :zx_name, :zx_risk FROM mm_mcc WHERE mcc = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_name, "-"); zx_risk = 0; } + + /* (3) 업무규칙 분기부: 위험도 구간별 감사 심도/점수 */ + if (zx_risk >= 4) { + zx_depth = 3; zx_score = 90; /* 고위험 업종 */ + } else if (zx_risk >= 2) { + zx_depth = 2; zx_score = 60; /* 중위험 업종 */ + } else if (zx_risk == 1) { + zx_depth = 1; zx_score = 30; /* 저위험 업종 */ + } else { + zx_depth = 0; zx_score = 0; /* 미등록 업종 */ + } + + /* (5) 참조 현황 집계부: 해당 MCC 사용 가맹점(전체/활성) */ + zx_n1 = 0; zx_users = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merchant_ext WHERE mcc = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_users + FROM mm_merchant_ext WHERE mcc = :zx_key AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_users; + + /* (6) 전사 업종 재집계부: 고위험 MCC 총수 + 평균 위험도 */ + zx_n3 = 0; + EXEC SQL SELECT count(*) INTO :zx_n3 FROM mm_mcc WHERE risk_level >= 4; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_MCC_RISKSET 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_MCC_RISKSET 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_MCC_RISKSET 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_MCC_RISKSET key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_MCC_RISKSET 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_MCC_RISKSET 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_MCC_RISKSET 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_MCC_UPD.pgc b/app/src/mm/svc/MM_MCC_UPD.pgc index 87821e2..f1ad9f5 100644 --- a/app/src/mm/svc/MM_MCC_UPD.pgc +++ b/app/src/mm/svc/MM_MCC_UPD.pgc @@ -33,5 +33,103 @@ void MM_MCC_UPD(TPSVCINFO *p) WHERE mcc = :h_mcc; if (sqlca.sqlcode < 0) FAIL(b); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_MCC_UPD - MCC 업종 마스터 리스크/감사 확장부 (mm 업종) + * 업종 위험도와 참조 가맹점 현황을 재산정하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_name[64], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_risk, zx_users; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: MCC 코드 확보 (T_CATEGORY/T_KEY1) */ + strcpy(zx_svc, "MM_MCC_UPD"); + gets_(b, T_CATEGORY, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "0000"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + zx_bps = 0; + + /* (2) 업종 마스터 조회부: 명칭 + 위험도 */ + strcpy(zx_name, "-"); zx_risk = 1; + EXEC SQL SELECT coalesce(mcc_name,'-'), coalesce(risk_level,1) + INTO :zx_name, :zx_risk FROM mm_mcc WHERE mcc = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_name, "-"); zx_risk = 0; } + + /* (3) 업무규칙 분기부: 위험도 구간별 감사 심도/점수 */ + if (zx_risk >= 4) { + zx_depth = 3; zx_score = 90; /* 고위험 업종 */ + } else if (zx_risk >= 2) { + zx_depth = 2; zx_score = 60; /* 중위험 업종 */ + } else if (zx_risk == 1) { + zx_depth = 1; zx_score = 30; /* 저위험 업종 */ + } else { + zx_depth = 0; zx_score = 0; /* 미등록 업종 */ + } + + /* (5) 참조 현황 집계부: 해당 MCC 사용 가맹점(전체/활성) */ + zx_n1 = 0; zx_users = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merchant_ext WHERE mcc = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_users + FROM mm_merchant_ext WHERE mcc = :zx_key AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_users; + + /* (6) 전사 업종 재집계부: 고위험 MCC 총수 + 평균 위험도 */ + zx_n3 = 0; + EXEC SQL SELECT count(*) INTO :zx_n3 FROM mm_mcc WHERE risk_level >= 4; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_MCC_UPD 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_MCC_UPD 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_MCC_UPD 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_MCC_UPD key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_MCC_UPD 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_MCC_UPD 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_MCC_UPD 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_MERCH_ACCT.pgc b/app/src/mm/svc/MM_MERCH_ACCT.pgc index 19975c8..5cc6e47 100644 --- a/app/src/mm/svc/MM_MERCH_ACCT.pgc +++ b/app/src/mm/svc/MM_MERCH_ACCT.pgc @@ -21,5 +21,119 @@ void MM_MERCH_ACCT(TPSVCINFO *p) WHERE merchant_id = :h_merch; if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); if (mmdb_insert_merch_hist(merch, "ACCT", "", acct, "계좌 변경") < 0) FAIL(b); + + /* ==================================================================== + * [심화 후처리] MM_MERCH_ACCT - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_MERCH_ACCT"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_MERCH_ACCT 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_MERCH_ACCT 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_MERCH_ACCT 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_MERCH_ACCT key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_MERCH_ACCT 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_MERCH_ACCT 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_MERCH_ACCT 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_MERCH_BULK.pgc b/app/src/mm/svc/MM_MERCH_BULK.pgc index 5c68105..662d4b6 100644 --- a/app/src/mm/svc/MM_MERCH_BULK.pgc +++ b/app/src/mm/svc/MM_MERCH_BULK.pgc @@ -29,5 +29,119 @@ void MM_MERCH_BULK(TPSVCINFO *p) } setl(b, T_COUNT, made); userlog("MM_MERCH_BULK prefix=%s 생성=%ld", prefix, made); + + /* ==================================================================== + * [심화 후처리] MM_MERCH_BULK - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_MERCH_BULK"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_MERCH_BULK 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_MERCH_BULK 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_MERCH_BULK 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_MERCH_BULK key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_MERCH_BULK 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_MERCH_BULK 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_MERCH_BULK 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_MERCH_CLOSE.pgc b/app/src/mm/svc/MM_MERCH_CLOSE.pgc index 9a98098..8d98d62 100644 --- a/app/src/mm/svc/MM_MERCH_CLOSE.pgc +++ b/app/src/mm/svc/MM_MERCH_CLOSE.pgc @@ -42,5 +42,119 @@ void MM_MERCH_CLOSE(TPSVCINFO *p) if (mmdb_insert_merch_hist(h_merch, "CLOSE", h_stat, "DELETED", memo) < 0) FAIL(b); setl(b, T_COUNT, nterm); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_MERCH_CLOSE - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_MERCH_CLOSE"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_MERCH_CLOSE 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_MERCH_CLOSE 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_MERCH_CLOSE 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_MERCH_CLOSE key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_MERCH_CLOSE 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_MERCH_CLOSE 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_MERCH_CLOSE 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_MERCH_DEL.pgc b/app/src/mm/svc/MM_MERCH_DEL.pgc index 8bb541b..d82731c 100644 --- a/app/src/mm/svc/MM_MERCH_DEL.pgc +++ b/app/src/mm/svc/MM_MERCH_DEL.pgc @@ -14,5 +14,119 @@ void MM_MERCH_DEL(TPSVCINFO *p) if (mmdb_update_merchant_status(merch, "DELETED") < 0) FAIL(b); if (mmdb_insert_merch_hist(merch, "DEL", "ACTIVE", "DELETED", "논리 삭제") < 0) FAIL(b); Bchg(b, T_STATUS, 0, "DELETED", 0L); + + /* ==================================================================== + * [심화 후처리] MM_MERCH_DEL - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_MERCH_DEL"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_MERCH_DEL 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_MERCH_DEL 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_MERCH_DEL 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_MERCH_DEL key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_MERCH_DEL 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_MERCH_DEL 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_MERCH_DEL 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_MERCH_DORMANT.pgc b/app/src/mm/svc/MM_MERCH_DORMANT.pgc index a78346a..dcff9c6 100644 --- a/app/src/mm/svc/MM_MERCH_DORMANT.pgc +++ b/app/src/mm/svc/MM_MERCH_DORMANT.pgc @@ -38,5 +38,119 @@ void MM_MERCH_DORMANT(TPSVCINFO *p) if (sqlca.sqlcode < 0) FAIL(b); if (mmdb_insert_merch_hist(h_merch, "DORMANT", "ACTIVE", "DORMANT", reason) < 0) FAIL(b); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_MERCH_DORMANT - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_MERCH_DORMANT"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_MERCH_DORMANT 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_MERCH_DORMANT 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_MERCH_DORMANT 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_MERCH_DORMANT key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_MERCH_DORMANT 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_MERCH_DORMANT 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_MERCH_DORMANT 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_MERCH_GRADE.pgc b/app/src/mm/svc/MM_MERCH_GRADE.pgc index 346bdbb..d140599 100644 --- a/app/src/mm/svc/MM_MERCH_GRADE.pgc +++ b/app/src/mm/svc/MM_MERCH_GRADE.pgc @@ -15,5 +15,119 @@ void MM_MERCH_GRADE(TPSVCINFO *p) if (mmdb_update_merchant_grade(merch, grade) < 0) FAIL(b); if (mmdb_insert_merch_hist(merch, "GRADE", "", grade, "등급 변경") < 0) FAIL(b); Bchg(b, T_STR2, 0, grade, 0L); + + /* ==================================================================== + * [심화 후처리] MM_MERCH_GRADE - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_MERCH_GRADE"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_MERCH_GRADE 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_MERCH_GRADE 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_MERCH_GRADE 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_MERCH_GRADE key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_MERCH_GRADE 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_MERCH_GRADE 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_MERCH_GRADE 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_MERCH_INQ.pgc b/app/src/mm/svc/MM_MERCH_INQ.pgc index 5e1ab7e..2c4b0b2 100644 --- a/app/src/mm/svc/MM_MERCH_INQ.pgc +++ b/app/src/mm/svc/MM_MERCH_INQ.pgc @@ -15,5 +15,119 @@ void MM_MERCH_INQ(TPSVCINFO *p) Bchg(b, T_STR1, 0, name, 0L); Bchg(b, T_STR2, 0, grade, 0L); Bchg(b, T_STATUS, 0, status, 0L); + + /* ==================================================================== + * [심화 후처리] MM_MERCH_INQ - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_MERCH_INQ"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_MERCH_INQ 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_MERCH_INQ 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_MERCH_INQ 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_MERCH_INQ key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_MERCH_INQ 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_MERCH_INQ 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_MERCH_INQ 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_MERCH_PAUSE.pgc b/app/src/mm/svc/MM_MERCH_PAUSE.pgc index 46289db..4a39ef6 100644 --- a/app/src/mm/svc/MM_MERCH_PAUSE.pgc +++ b/app/src/mm/svc/MM_MERCH_PAUSE.pgc @@ -36,5 +36,119 @@ void MM_MERCH_PAUSE(TPSVCINFO *p) if (mmdb_insert_merch_hist(h_merch, "PAUSE", "ACTIVE", "HOLD", reason) < 0) FAIL(b); setl(b, T_COUNT, nterm); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_MERCH_PAUSE - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_MERCH_PAUSE"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_MERCH_PAUSE 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_MERCH_PAUSE 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_MERCH_PAUSE 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_MERCH_PAUSE key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_MERCH_PAUSE 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_MERCH_PAUSE 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_MERCH_PAUSE 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_MERCH_REG.pgc b/app/src/mm/svc/MM_MERCH_REG.pgc index 5888872..28a7949 100644 --- a/app/src/mm/svc/MM_MERCH_REG.pgc +++ b/app/src/mm/svc/MM_MERCH_REG.pgc @@ -22,5 +22,119 @@ void MM_MERCH_REG(TPSVCINFO *p) Bchg(b, T_STATUS, 0, "ACTIVE", 0L); userlog("MM_MERCH_REG merch=%s grade=%s 등록", merch, grade); + + /* ==================================================================== + * [심화 후처리] MM_MERCH_REG - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_MERCH_REG"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_MERCH_REG 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_MERCH_REG 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_MERCH_REG 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_MERCH_REG key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_MERCH_REG 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_MERCH_REG 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_MERCH_REG 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_MERCH_RESUME.pgc b/app/src/mm/svc/MM_MERCH_RESUME.pgc index be39570..4995f05 100644 --- a/app/src/mm/svc/MM_MERCH_RESUME.pgc +++ b/app/src/mm/svc/MM_MERCH_RESUME.pgc @@ -34,5 +34,119 @@ void MM_MERCH_RESUME(TPSVCINFO *p) if (mmdb_insert_merch_hist(h_merch, "RESUME", "HOLD", "ACTIVE", "영업 재개") < 0) FAIL(b); setl(b, T_COUNT, nterm); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_MERCH_RESUME - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_MERCH_RESUME"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_MERCH_RESUME 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_MERCH_RESUME 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_MERCH_RESUME 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_MERCH_RESUME key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_MERCH_RESUME 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_MERCH_RESUME 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_MERCH_RESUME 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_MERCH_STATUS.pgc b/app/src/mm/svc/MM_MERCH_STATUS.pgc index 4441070..ed4409a 100644 --- a/app/src/mm/svc/MM_MERCH_STATUS.pgc +++ b/app/src/mm/svc/MM_MERCH_STATUS.pgc @@ -14,5 +14,119 @@ void MM_MERCH_STATUS(TPSVCINFO *p) gets_(b, T_STATUS, status, sizeof(status)); if (status[0] == 0) strcpy(status, "HOLD"); if (mmdb_update_merchant_status(merch, status) < 0) FAIL(b); if (mmdb_insert_merch_hist(merch, "STATUS", "", status, "상태 변경") < 0) FAIL(b); + + /* ==================================================================== + * [심화 후처리] MM_MERCH_STATUS - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_MERCH_STATUS"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_MERCH_STATUS 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_MERCH_STATUS 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_MERCH_STATUS 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_MERCH_STATUS key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_MERCH_STATUS 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_MERCH_STATUS 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_MERCH_STATUS 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_MERCH_SUM.pgc b/app/src/mm/svc/MM_MERCH_SUM.pgc index 7a019b8..b4b380d 100644 --- a/app/src/mm/svc/MM_MERCH_SUM.pgc +++ b/app/src/mm/svc/MM_MERCH_SUM.pgc @@ -19,5 +19,119 @@ void MM_MERCH_SUM(TPSVCINFO *p) EXEC SQL SELECT count(*) INTO :h_cnt FROM mm_merchant_ext WHERE status = :h_status; if (sqlca.sqlcode < 0) FAIL(b); setl(b, T_COUNT, h_cnt); + + /* ==================================================================== + * [심화 후처리] MM_MERCH_SUM - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_MERCH_SUM"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_MERCH_SUM 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_MERCH_SUM 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_MERCH_SUM 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_MERCH_SUM key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_MERCH_SUM 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_MERCH_SUM 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_MERCH_SUM 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_MERCH_UPD.pgc b/app/src/mm/svc/MM_MERCH_UPD.pgc index ea748ca..810f9f9 100644 --- a/app/src/mm/svc/MM_MERCH_UPD.pgc +++ b/app/src/mm/svc/MM_MERCH_UPD.pgc @@ -28,5 +28,119 @@ void MM_MERCH_UPD(TPSVCINFO *p) userlog("MM_MERCH_UPD FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } if (mmdb_insert_merch_hist(merch, "UPD", "", name, "정보 수정") < 0) FAIL(b); + + /* ==================================================================== + * [심화 후처리] MM_MERCH_UPD - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_MERCH_UPD"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_MERCH_UPD 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_MERCH_UPD 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_MERCH_UPD 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_MERCH_UPD key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_MERCH_UPD 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_MERCH_UPD 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_MERCH_UPD 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_PARTNER_INQ.pgc b/app/src/mm/svc/MM_PARTNER_INQ.pgc index b7890f2..4911129 100644 --- a/app/src/mm/svc/MM_PARTNER_INQ.pgc +++ b/app/src/mm/svc/MM_PARTNER_INQ.pgc @@ -28,5 +28,100 @@ void MM_PARTNER_INQ(TPSVCINFO *p) Bchg(b, T_STATUS, 0, h_stat, 0L); setl(b, T_ARG1, h_sort); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_PARTNER_INQ - 공통코드/제휴 마스터 정합/감사 확장부 (mm 코드) + * 코드그룹 활성 규모와 중복/상태 정합성을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grp[32], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_act, zx_grps; + EXEC SQL END DECLARE SECTION; + char zx_in[64], zx_gin[32]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 코드/그룹 키 확보 */ + strcpy(zx_svc, "MM_PARTNER_INQ"); + gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_STR1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + gets_(b, T_STR1, zx_gin, sizeof(zx_gin)); + if (zx_gin[0] == 0) strcpy(zx_gin, "GEN"); + strncpy(zx_grp, zx_gin, sizeof(zx_grp)-1); zx_grp[sizeof(zx_grp)-1] = 0; + zx_bps = 0; + + /* (2) 코드그룹 조회부: 그룹 내 활성 코드 수 + 전체 코드 수 */ + zx_act = 0; zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_act + FROM mm_code WHERE code_group = :zx_grp AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_code WHERE code_group = :zx_grp; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_act; + + /* (3) 업무규칙 분기부: 그룹 규모/활성 비율별 상태 판정 */ + if (zx_n1 == 0) { + zx_depth = 0; zx_score = 0; /* 빈 코드그룹 */ + } else if (zx_act == zx_n1) { + zx_depth = 3; zx_score = 80; /* 전량 활성 */ + } else if (zx_act * 2 >= zx_n1) { + zx_depth = 2; zx_score = 55; /* 과반 활성 */ + } else { + zx_depth = 1; zx_score = 30; /* 비활성 우세 */ + } + + /* (6) 전사 코드 재집계부: 코드그룹 종류 수 */ + zx_grps = 0; + EXEC SQL SELECT count(DISTINCT code_group) INTO :zx_grps FROM mm_code; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_grps; + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_PARTNER_INQ 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_PARTNER_INQ 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_PARTNER_INQ 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_PARTNER_INQ key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_PARTNER_INQ 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_PARTNER_INQ 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_PARTNER_INQ 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_PARTNER_LIST.pgc b/app/src/mm/svc/MM_PARTNER_LIST.pgc index 0a64ef1..59e2d6e 100644 --- a/app/src/mm/svc/MM_PARTNER_LIST.pgc +++ b/app/src/mm/svc/MM_PARTNER_LIST.pgc @@ -23,5 +23,100 @@ void MM_PARTNER_LIST(TPSVCINFO *p) setl(b, T_COUNT, h_active); setl(b, T_ARG1, h_all); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_PARTNER_LIST - 공통코드/제휴 마스터 정합/감사 확장부 (mm 코드) + * 코드그룹 활성 규모와 중복/상태 정합성을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grp[32], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_act, zx_grps; + EXEC SQL END DECLARE SECTION; + char zx_in[64], zx_gin[32]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 코드/그룹 키 확보 */ + strcpy(zx_svc, "MM_PARTNER_LIST"); + gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_STR1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + gets_(b, T_STR1, zx_gin, sizeof(zx_gin)); + if (zx_gin[0] == 0) strcpy(zx_gin, "GEN"); + strncpy(zx_grp, zx_gin, sizeof(zx_grp)-1); zx_grp[sizeof(zx_grp)-1] = 0; + zx_bps = 0; + + /* (2) 코드그룹 조회부: 그룹 내 활성 코드 수 + 전체 코드 수 */ + zx_act = 0; zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_act + FROM mm_code WHERE code_group = :zx_grp AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_code WHERE code_group = :zx_grp; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_act; + + /* (3) 업무규칙 분기부: 그룹 규모/활성 비율별 상태 판정 */ + if (zx_n1 == 0) { + zx_depth = 0; zx_score = 0; /* 빈 코드그룹 */ + } else if (zx_act == zx_n1) { + zx_depth = 3; zx_score = 80; /* 전량 활성 */ + } else if (zx_act * 2 >= zx_n1) { + zx_depth = 2; zx_score = 55; /* 과반 활성 */ + } else { + zx_depth = 1; zx_score = 30; /* 비활성 우세 */ + } + + /* (6) 전사 코드 재집계부: 코드그룹 종류 수 */ + zx_grps = 0; + EXEC SQL SELECT count(DISTINCT code_group) INTO :zx_grps FROM mm_code; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_grps; + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_PARTNER_LIST 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_PARTNER_LIST 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_PARTNER_LIST 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_PARTNER_LIST key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_PARTNER_LIST 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_PARTNER_LIST 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_PARTNER_LIST 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_PARTNER_REG.pgc b/app/src/mm/svc/MM_PARTNER_REG.pgc index d1f2641..355ab72 100644 --- a/app/src/mm/svc/MM_PARTNER_REG.pgc +++ b/app/src/mm/svc/MM_PARTNER_REG.pgc @@ -29,5 +29,100 @@ void MM_PARTNER_REG(TPSVCINFO *p) DO UPDATE SET code_name = EXCLUDED.code_name, sort_order = EXCLUDED.sort_order; if (sqlca.sqlcode < 0) FAIL(b); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_PARTNER_REG - 공통코드/제휴 마스터 정합/감사 확장부 (mm 코드) + * 코드그룹 활성 규모와 중복/상태 정합성을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grp[32], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_act, zx_grps; + EXEC SQL END DECLARE SECTION; + char zx_in[64], zx_gin[32]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 코드/그룹 키 확보 */ + strcpy(zx_svc, "MM_PARTNER_REG"); + gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_STR1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + gets_(b, T_STR1, zx_gin, sizeof(zx_gin)); + if (zx_gin[0] == 0) strcpy(zx_gin, "GEN"); + strncpy(zx_grp, zx_gin, sizeof(zx_grp)-1); zx_grp[sizeof(zx_grp)-1] = 0; + zx_bps = 0; + + /* (2) 코드그룹 조회부: 그룹 내 활성 코드 수 + 전체 코드 수 */ + zx_act = 0; zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_act + FROM mm_code WHERE code_group = :zx_grp AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_code WHERE code_group = :zx_grp; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_act; + + /* (3) 업무규칙 분기부: 그룹 규모/활성 비율별 상태 판정 */ + if (zx_n1 == 0) { + zx_depth = 0; zx_score = 0; /* 빈 코드그룹 */ + } else if (zx_act == zx_n1) { + zx_depth = 3; zx_score = 80; /* 전량 활성 */ + } else if (zx_act * 2 >= zx_n1) { + zx_depth = 2; zx_score = 55; /* 과반 활성 */ + } else { + zx_depth = 1; zx_score = 30; /* 비활성 우세 */ + } + + /* (6) 전사 코드 재집계부: 코드그룹 종류 수 */ + zx_grps = 0; + EXEC SQL SELECT count(DISTINCT code_group) INTO :zx_grps FROM mm_code; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_grps; + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_PARTNER_REG 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_PARTNER_REG 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_PARTNER_REG 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_PARTNER_REG key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_PARTNER_REG 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_PARTNER_REG 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_PARTNER_REG 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_PARTNER_STATUS.pgc b/app/src/mm/svc/MM_PARTNER_STATUS.pgc index 59ecbd8..f9c1834 100644 --- a/app/src/mm/svc/MM_PARTNER_STATUS.pgc +++ b/app/src/mm/svc/MM_PARTNER_STATUS.pgc @@ -28,5 +28,100 @@ void MM_PARTNER_STATUS(TPSVCINFO *p) if (sqlca.sqlcode < 0) FAIL(b); if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 1); OK(b); } setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_PARTNER_STATUS - 공통코드/제휴 마스터 정합/감사 확장부 (mm 코드) + * 코드그룹 활성 규모와 중복/상태 정합성을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grp[32], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_act, zx_grps; + EXEC SQL END DECLARE SECTION; + char zx_in[64], zx_gin[32]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 코드/그룹 키 확보 */ + strcpy(zx_svc, "MM_PARTNER_STATUS"); + gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_STR1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + gets_(b, T_STR1, zx_gin, sizeof(zx_gin)); + if (zx_gin[0] == 0) strcpy(zx_gin, "GEN"); + strncpy(zx_grp, zx_gin, sizeof(zx_grp)-1); zx_grp[sizeof(zx_grp)-1] = 0; + zx_bps = 0; + + /* (2) 코드그룹 조회부: 그룹 내 활성 코드 수 + 전체 코드 수 */ + zx_act = 0; zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_act + FROM mm_code WHERE code_group = :zx_grp AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_code WHERE code_group = :zx_grp; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_act; + + /* (3) 업무규칙 분기부: 그룹 규모/활성 비율별 상태 판정 */ + if (zx_n1 == 0) { + zx_depth = 0; zx_score = 0; /* 빈 코드그룹 */ + } else if (zx_act == zx_n1) { + zx_depth = 3; zx_score = 80; /* 전량 활성 */ + } else if (zx_act * 2 >= zx_n1) { + zx_depth = 2; zx_score = 55; /* 과반 활성 */ + } else { + zx_depth = 1; zx_score = 30; /* 비활성 우세 */ + } + + /* (6) 전사 코드 재집계부: 코드그룹 종류 수 */ + zx_grps = 0; + EXEC SQL SELECT count(DISTINCT code_group) INTO :zx_grps FROM mm_code; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_grps; + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_PARTNER_STATUS 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_PARTNER_STATUS 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_PARTNER_STATUS 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_PARTNER_STATUS key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_PARTNER_STATUS 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_PARTNER_STATUS 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_PARTNER_STATUS 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_PARTNER_UPD.pgc b/app/src/mm/svc/MM_PARTNER_UPD.pgc index d31ee2b..0ea6234 100644 --- a/app/src/mm/svc/MM_PARTNER_UPD.pgc +++ b/app/src/mm/svc/MM_PARTNER_UPD.pgc @@ -32,5 +32,100 @@ void MM_PARTNER_UPD(TPSVCINFO *p) WHERE code_group = 'PARTNER' AND code = :h_code; if (sqlca.sqlcode < 0) FAIL(b); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_PARTNER_UPD - 공통코드/제휴 마스터 정합/감사 확장부 (mm 코드) + * 코드그룹 활성 규모와 중복/상태 정합성을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grp[32], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_act, zx_grps; + EXEC SQL END DECLARE SECTION; + char zx_in[64], zx_gin[32]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 코드/그룹 키 확보 */ + strcpy(zx_svc, "MM_PARTNER_UPD"); + gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_STR1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + gets_(b, T_STR1, zx_gin, sizeof(zx_gin)); + if (zx_gin[0] == 0) strcpy(zx_gin, "GEN"); + strncpy(zx_grp, zx_gin, sizeof(zx_grp)-1); zx_grp[sizeof(zx_grp)-1] = 0; + zx_bps = 0; + + /* (2) 코드그룹 조회부: 그룹 내 활성 코드 수 + 전체 코드 수 */ + zx_act = 0; zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_act + FROM mm_code WHERE code_group = :zx_grp AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_code WHERE code_group = :zx_grp; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_act; + + /* (3) 업무규칙 분기부: 그룹 규모/활성 비율별 상태 판정 */ + if (zx_n1 == 0) { + zx_depth = 0; zx_score = 0; /* 빈 코드그룹 */ + } else if (zx_act == zx_n1) { + zx_depth = 3; zx_score = 80; /* 전량 활성 */ + } else if (zx_act * 2 >= zx_n1) { + zx_depth = 2; zx_score = 55; /* 과반 활성 */ + } else { + zx_depth = 1; zx_score = 30; /* 비활성 우세 */ + } + + /* (6) 전사 코드 재집계부: 코드그룹 종류 수 */ + zx_grps = 0; + EXEC SQL SELECT count(DISTINCT code_group) INTO :zx_grps FROM mm_code; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_grps; + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_PARTNER_UPD 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_PARTNER_UPD 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_PARTNER_UPD 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_PARTNER_UPD key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_PARTNER_UPD 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_PARTNER_UPD 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_PARTNER_UPD 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_RATE_APPROVE.pgc b/app/src/mm/svc/MM_RATE_APPROVE.pgc index 2ac335e..d8dedcb 100644 --- a/app/src/mm/svc/MM_RATE_APPROVE.pgc +++ b/app/src/mm/svc/MM_RATE_APPROVE.pgc @@ -43,5 +43,119 @@ void MM_RATE_APPROVE(TPSVCINFO *p) Bchg(b, T_STR2, 0, card, 0L); setl(b, T_AMT1, bps); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_RATE_APPROVE - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_RATE_APPROVE"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_RATE_APPROVE 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_RATE_APPROVE 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_RATE_APPROVE 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_RATE_APPROVE key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_RATE_APPROVE 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_RATE_APPROVE 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_RATE_APPROVE 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_RATE_CANCEL.pgc b/app/src/mm/svc/MM_RATE_CANCEL.pgc index 6956573..3babf20 100644 --- a/app/src/mm/svc/MM_RATE_CANCEL.pgc +++ b/app/src/mm/svc/MM_RATE_CANCEL.pgc @@ -29,5 +29,119 @@ void MM_RATE_CANCEL(TPSVCINFO *p) if (mmdb_insert_merch_hist(h_merch, "RATE_REJ", "PENDING", "CANCELLED", "요청인 철회") < 0) FAIL(b); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_RATE_CANCEL - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_RATE_CANCEL"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_RATE_CANCEL 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_RATE_CANCEL 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_RATE_CANCEL 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_RATE_CANCEL key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_RATE_CANCEL 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_RATE_CANCEL 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_RATE_CANCEL 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_RATE_HIST.pgc b/app/src/mm/svc/MM_RATE_HIST.pgc index cdafab6..352ddb4 100644 --- a/app/src/mm/svc/MM_RATE_HIST.pgc +++ b/app/src/mm/svc/MM_RATE_HIST.pgc @@ -31,5 +31,119 @@ void MM_RATE_HIST(TPSVCINFO *p) setl(b, T_ARG1, h_apv); setl(b, T_ARG2, h_rej); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_RATE_HIST - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_RATE_HIST"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_RATE_HIST 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_RATE_HIST 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_RATE_HIST 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_RATE_HIST key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_RATE_HIST 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_RATE_HIST 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_RATE_HIST 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_RATE_PENDING.pgc b/app/src/mm/svc/MM_RATE_PENDING.pgc index 957fef0..2481ef0 100644 --- a/app/src/mm/svc/MM_RATE_PENDING.pgc +++ b/app/src/mm/svc/MM_RATE_PENDING.pgc @@ -26,5 +26,119 @@ void MM_RATE_PENDING(TPSVCINFO *p) setl(b, T_COUNT, h_cnt); setl(b, T_ARG1, h_merch); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_RATE_PENDING - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_RATE_PENDING"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_RATE_PENDING 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_RATE_PENDING 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_RATE_PENDING 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_RATE_PENDING key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_RATE_PENDING 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_RATE_PENDING 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_RATE_PENDING 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_RATE_REJECT.pgc b/app/src/mm/svc/MM_RATE_REJECT.pgc index 7c28c7d..508fa66 100644 --- a/app/src/mm/svc/MM_RATE_REJECT.pgc +++ b/app/src/mm/svc/MM_RATE_REJECT.pgc @@ -33,5 +33,119 @@ void MM_RATE_REJECT(TPSVCINFO *p) if (mmdb_insert_merch_hist(h_merch, "RATE_REJ", h_req, "", reason) < 0) FAIL(b); Bchg(b, T_STR1, 0, h_req, 0L); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_RATE_REJECT - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_RATE_REJECT"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_RATE_REJECT 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_RATE_REJECT 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_RATE_REJECT 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_RATE_REJECT key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_RATE_REJECT 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_RATE_REJECT 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_RATE_REJECT 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_RATE_REQ.pgc b/app/src/mm/svc/MM_RATE_REQ.pgc index 79df903..13c5b79 100644 --- a/app/src/mm/svc/MM_RATE_REQ.pgc +++ b/app/src/mm/svc/MM_RATE_REQ.pgc @@ -47,5 +47,119 @@ void MM_RATE_REQ(TPSVCINFO *p) if (mmdb_insert_merch_hist(h_merch, "RATE_REQ", "", afterv, reason) < 0) FAIL(b); setl(b, T_AMT1, cur); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_RATE_REQ - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_RATE_REQ"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_RATE_REQ 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_RATE_REQ 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_RATE_REQ 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_RATE_REQ key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_RATE_REQ 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_RATE_REQ 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_RATE_REQ 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_RATE_STATUS.pgc b/app/src/mm/svc/MM_RATE_STATUS.pgc index d30d75d..7dca69f 100644 --- a/app/src/mm/svc/MM_RATE_STATUS.pgc +++ b/app/src/mm/svc/MM_RATE_STATUS.pgc @@ -37,5 +37,119 @@ void MM_RATE_STATUS(TPSVCINFO *p) else st = "PENDING"; Bchg(b, T_STR1, 0, (char *)st, 0L); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_RATE_STATUS - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_RATE_STATUS"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_RATE_STATUS 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_RATE_STATUS 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_RATE_STATUS 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_RATE_STATUS key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_RATE_STATUS 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_RATE_STATUS 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_RATE_STATUS 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_REL_CHILDREN.pgc b/app/src/mm/svc/MM_REL_CHILDREN.pgc index 30311be..fdef8e1 100644 --- a/app/src/mm/svc/MM_REL_CHILDREN.pgc +++ b/app/src/mm/svc/MM_REL_CHILDREN.pgc @@ -29,5 +29,119 @@ void MM_REL_CHILDREN(TPSVCINFO *p) setl(b, T_COUNT, h_cnt); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_REL_CHILDREN - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_REL_CHILDREN"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_REL_CHILDREN 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_REL_CHILDREN 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_REL_CHILDREN 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_REL_CHILDREN key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_REL_CHILDREN 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_REL_CHILDREN 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_REL_CHILDREN 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_REL_LINK.pgc b/app/src/mm/svc/MM_REL_LINK.pgc index b1d5cac..2de1fc6 100644 --- a/app/src/mm/svc/MM_REL_LINK.pgc +++ b/app/src/mm/svc/MM_REL_LINK.pgc @@ -44,5 +44,119 @@ void MM_REL_LINK(TPSVCINFO *p) if (sqlca.sqlcode < 0) FAIL(b); if (mmdb_insert_merch_hist(h_child, "REL_LINK", "", h_parent, "대표가맹점 연결") < 0) FAIL(b); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_REL_LINK - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_REL_LINK"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_REL_LINK 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_REL_LINK 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_REL_LINK 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_REL_LINK key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_REL_LINK 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_REL_LINK 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_REL_LINK 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_REL_LIST.pgc b/app/src/mm/svc/MM_REL_LIST.pgc index c6116ca..938021f 100644 --- a/app/src/mm/svc/MM_REL_LIST.pgc +++ b/app/src/mm/svc/MM_REL_LIST.pgc @@ -29,5 +29,119 @@ void MM_REL_LIST(TPSVCINFO *p) setl(b, T_COUNT, h_cnt); Bchg(b, T_STR1, 0, h_first, 0L); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_REL_LIST - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_REL_LIST"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_REL_LIST 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_REL_LIST 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_REL_LIST 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_REL_LIST key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_REL_LIST 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_REL_LIST 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_REL_LIST 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_REL_MOVE.pgc b/app/src/mm/svc/MM_REL_MOVE.pgc index cdef453..7c3f536 100644 --- a/app/src/mm/svc/MM_REL_MOVE.pgc +++ b/app/src/mm/svc/MM_REL_MOVE.pgc @@ -38,5 +38,119 @@ void MM_REL_MOVE(TPSVCINFO *p) if (mmdb_insert_merch_hist(h_child, "REL_MOVE", h_old, h_new, "대표 가맹점 이관") < 0) FAIL(b); Bchg(b, T_STR1, 0, h_old, 0L); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_REL_MOVE - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_REL_MOVE"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_REL_MOVE 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_REL_MOVE 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_REL_MOVE 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_REL_MOVE key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_REL_MOVE 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_REL_MOVE 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_REL_MOVE 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_REL_PARENT.pgc b/app/src/mm/svc/MM_REL_PARENT.pgc index 2e9dae9..d49843e 100644 --- a/app/src/mm/svc/MM_REL_PARENT.pgc +++ b/app/src/mm/svc/MM_REL_PARENT.pgc @@ -26,5 +26,119 @@ void MM_REL_PARENT(TPSVCINFO *p) Bchg(b, T_STR1, 0, h_parent, 0L); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_REL_PARENT - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_REL_PARENT"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_REL_PARENT 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_REL_PARENT 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_REL_PARENT 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_REL_PARENT key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_REL_PARENT 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_REL_PARENT 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_REL_PARENT 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_REL_UNLINK.pgc b/app/src/mm/svc/MM_REL_UNLINK.pgc index d0e90cc..09a6d68 100644 --- a/app/src/mm/svc/MM_REL_UNLINK.pgc +++ b/app/src/mm/svc/MM_REL_UNLINK.pgc @@ -28,5 +28,119 @@ void MM_REL_UNLINK(TPSVCINFO *p) if (sqlca.sqlcode < 0) FAIL(b); if (mmdb_insert_merch_hist(h_child, "REL_UNLINK", h_parent, "", "대표가맹점 연결 해제") < 0) FAIL(b); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_REL_UNLINK - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_REL_UNLINK"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_REL_UNLINK 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_REL_UNLINK 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_REL_UNLINK 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_REL_UNLINK key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_REL_UNLINK 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_REL_UNLINK 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_REL_UNLINK 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_REL_VERIFY.pgc b/app/src/mm/svc/MM_REL_VERIFY.pgc index 3eca381..1ef8712 100644 --- a/app/src/mm/svc/MM_REL_VERIFY.pgc +++ b/app/src/mm/svc/MM_REL_VERIFY.pgc @@ -35,5 +35,119 @@ void MM_REL_VERIFY(TPSVCINFO *p) setl(b, T_COUNT, 1); setl(b, T_ARG1, h_defect); setl(b, T_RC, h_defect > 0 ? 7 : 0); + + /* ==================================================================== + * [심화 후처리] MM_REL_VERIFY - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_REL_VERIFY"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_REL_VERIFY 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_REL_VERIFY 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_REL_VERIFY 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_REL_VERIFY key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_REL_VERIFY 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_REL_VERIFY 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_REL_VERIFY 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_RISK_INQ.pgc b/app/src/mm/svc/MM_RISK_INQ.pgc index 283b546..a6c850e 100644 --- a/app/src/mm/svc/MM_RISK_INQ.pgc +++ b/app/src/mm/svc/MM_RISK_INQ.pgc @@ -37,5 +37,119 @@ void MM_RISK_INQ(TPSVCINFO *p) Bchg(b, T_STR1, 0, h_band, 0L); setl(b, T_ARG1, h_setcnt > 0 ? atol(h_setval) : 0); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_RISK_INQ - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_RISK_INQ"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_RISK_INQ 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_RISK_INQ 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_RISK_INQ 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_RISK_INQ key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_RISK_INQ 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_RISK_INQ 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_RISK_INQ 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_RISK_SCORE.pgc b/app/src/mm/svc/MM_RISK_SCORE.pgc index f4da298..24a1477 100644 --- a/app/src/mm/svc/MM_RISK_SCORE.pgc +++ b/app/src/mm/svc/MM_RISK_SCORE.pgc @@ -42,5 +42,119 @@ void MM_RISK_SCORE(TPSVCINFO *p) Bchg(b, T_STR1, 0, (char *)band, 0L); setl(b, T_ARG1, score); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_RISK_SCORE - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_RISK_SCORE"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_RISK_SCORE 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_RISK_SCORE 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_RISK_SCORE 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_RISK_SCORE key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_RISK_SCORE 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_RISK_SCORE 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_RISK_SCORE 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_RISK_SET.pgc b/app/src/mm/svc/MM_RISK_SET.pgc index 66d9950..36b6704 100644 --- a/app/src/mm/svc/MM_RISK_SET.pgc +++ b/app/src/mm/svc/MM_RISK_SET.pgc @@ -28,5 +28,119 @@ void MM_RISK_SET(TPSVCINFO *p) snprintf(val, sizeof(val), "%ld", lvl); if (mmdb_insert_merch_hist(h_merch, "RISK_SET", "", val, "리스크 등급 수동 설정") < 0) FAIL(b); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_RISK_SET - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_RISK_SET"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_RISK_SET 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_RISK_SET 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_RISK_SET 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_RISK_SET key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_RISK_SET 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_RISK_SET 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_RISK_SET 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_STATUS.pgc b/app/src/mm/svc/MM_STATUS.pgc index 69c8100..ceb45b3 100644 --- a/app/src/mm/svc/MM_STATUS.pgc +++ b/app/src/mm/svc/MM_STATUS.pgc @@ -17,5 +17,119 @@ void MM_STATUS(TPSVCINFO *p) setl(b, T_COUNT, h_cnt); setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "mm 마스터 서비스 정상", 0L); + + /* ==================================================================== + * [심화 후처리] MM_STATUS - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_STATUS"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_STATUS 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_STATUS 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_STATUS 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_STATUS key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_STATUS 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_STATUS 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_STATUS 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_TERMINAL_INQ.pgc b/app/src/mm/svc/MM_TERMINAL_INQ.pgc index ddaeeb8..05cb674 100644 --- a/app/src/mm/svc/MM_TERMINAL_INQ.pgc +++ b/app/src/mm/svc/MM_TERMINAL_INQ.pgc @@ -20,5 +20,111 @@ void MM_TERMINAL_INQ(TPSVCINFO *p) if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); Bchg(b, T_MERCHANT, 0, h_merch, 0L); Bchg(b, T_STATUS, 0, h_status, 0L); + + /* ==================================================================== + * [심화 후처리] MM_TERMINAL_INQ - 단말 마스터 정합/감사/집계 확장부 (mm 단말) + * 단말-가맹점 소속 정합성과 가동 현황을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_pmerch[64], zx_pstat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_orphan, zx_act; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 단말ID 확보 (T_STR5/T_STR6/T_KEY1 우선순위) */ + strcpy(zx_svc, "MM_TERMINAL_INQ"); + gets_(b, T_STR5, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_STR6, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + zx_bps = 0; + + /* (2) 단말 마스터 조회부: 소속 가맹점 + 소속 가맹점 상태 */ + strcpy(zx_pmerch, "-"); strcpy(zx_pstat, "NONE"); + EXEC SQL SELECT coalesce(merchant_id,'-') INTO :zx_pmerch + FROM mm_terminal WHERE terminal_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) strcpy(zx_pmerch, "-"); + EXEC SQL SELECT coalesce(status,'NONE') INTO :zx_pstat + FROM mm_merchant_ext WHERE merchant_id = :zx_pmerch; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) strcpy(zx_pstat, "NONE"); + + /* (3) 업무규칙 분기부: 소속 유효성에 따른 정합 등급 판정 */ + if (strcmp(zx_pmerch, "-") == 0) { + zx_depth = 0; zx_score = 0; /* 미존재 단말 */ + } else if (strcmp(zx_pstat, "ACTIVE") == 0) { + zx_depth = 3; zx_score = 85; /* 정상 소속 */ + } else if (strcmp(zx_pstat, "NONE") == 0) { + zx_depth = 1; zx_score = 20; /* 고아 단말 */ + } else { + zx_depth = 2; zx_score = 50; /* 비활성 소속 */ + } + + /* (5) 단말 현황 집계부: 소속 가맹점 보유/가동/폐기 건수 */ + zx_n1 = 0; zx_n2 = 0; zx_orphan = 0; zx_act = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_terminal WHERE merchant_id = :zx_pmerch; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_act + FROM mm_terminal WHERE merchant_id = :zx_pmerch AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_act; + + /* (6) 전사 단말 정합 재집계부: 고아 단말 탐지 */ + EXEC SQL SELECT count(*) INTO :zx_orphan FROM mm_terminal t + WHERE NOT EXISTS (SELECT 1 FROM mm_merchant_ext m + WHERE m.merchant_id = t.merchant_id); + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_orphan; + strncpy(zx_key, zx_pmerch, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_TERMINAL_INQ 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_TERMINAL_INQ 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_TERMINAL_INQ 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_TERMINAL_INQ key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_TERMINAL_INQ 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_TERMINAL_INQ 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_TERMINAL_INQ 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_TERMINAL_REG.pgc b/app/src/mm/svc/MM_TERMINAL_REG.pgc index cee1ded..1a41f77 100644 --- a/app/src/mm/svc/MM_TERMINAL_REG.pgc +++ b/app/src/mm/svc/MM_TERMINAL_REG.pgc @@ -25,5 +25,111 @@ void MM_TERMINAL_REG(TPSVCINFO *p) ON CONFLICT (terminal_id) DO UPDATE SET merchant_id = EXCLUDED.merchant_id, model = EXCLUDED.model; if (sqlca.sqlcode < 0) { userlog("MM_TERMINAL_REG FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + + /* ==================================================================== + * [심화 후처리] MM_TERMINAL_REG - 단말 마스터 정합/감사/집계 확장부 (mm 단말) + * 단말-가맹점 소속 정합성과 가동 현황을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_pmerch[64], zx_pstat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_orphan, zx_act; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 단말ID 확보 (T_STR5/T_STR6/T_KEY1 우선순위) */ + strcpy(zx_svc, "MM_TERMINAL_REG"); + gets_(b, T_STR5, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_STR6, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + zx_bps = 0; + + /* (2) 단말 마스터 조회부: 소속 가맹점 + 소속 가맹점 상태 */ + strcpy(zx_pmerch, "-"); strcpy(zx_pstat, "NONE"); + EXEC SQL SELECT coalesce(merchant_id,'-') INTO :zx_pmerch + FROM mm_terminal WHERE terminal_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) strcpy(zx_pmerch, "-"); + EXEC SQL SELECT coalesce(status,'NONE') INTO :zx_pstat + FROM mm_merchant_ext WHERE merchant_id = :zx_pmerch; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) strcpy(zx_pstat, "NONE"); + + /* (3) 업무규칙 분기부: 소속 유효성에 따른 정합 등급 판정 */ + if (strcmp(zx_pmerch, "-") == 0) { + zx_depth = 0; zx_score = 0; /* 미존재 단말 */ + } else if (strcmp(zx_pstat, "ACTIVE") == 0) { + zx_depth = 3; zx_score = 85; /* 정상 소속 */ + } else if (strcmp(zx_pstat, "NONE") == 0) { + zx_depth = 1; zx_score = 20; /* 고아 단말 */ + } else { + zx_depth = 2; zx_score = 50; /* 비활성 소속 */ + } + + /* (5) 단말 현황 집계부: 소속 가맹점 보유/가동/폐기 건수 */ + zx_n1 = 0; zx_n2 = 0; zx_orphan = 0; zx_act = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_terminal WHERE merchant_id = :zx_pmerch; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_act + FROM mm_terminal WHERE merchant_id = :zx_pmerch AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_act; + + /* (6) 전사 단말 정합 재집계부: 고아 단말 탐지 */ + EXEC SQL SELECT count(*) INTO :zx_orphan FROM mm_terminal t + WHERE NOT EXISTS (SELECT 1 FROM mm_merchant_ext m + WHERE m.merchant_id = t.merchant_id); + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_orphan; + strncpy(zx_key, zx_pmerch, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_TERMINAL_REG 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_TERMINAL_REG 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_TERMINAL_REG 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_TERMINAL_REG key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_TERMINAL_REG 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_TERMINAL_REG 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_TERMINAL_REG 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_TERM_ACTIVATE.pgc b/app/src/mm/svc/MM_TERM_ACTIVATE.pgc index db6945f..9fcf62d 100644 --- a/app/src/mm/svc/MM_TERM_ACTIVATE.pgc +++ b/app/src/mm/svc/MM_TERM_ACTIVATE.pgc @@ -28,5 +28,111 @@ void MM_TERM_ACTIVATE(TPSVCINFO *p) if (sqlca.sqlcode < 0) FAIL(b); Bchg(b, T_STATUS, 0, h_prev, 0L); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_TERM_ACTIVATE - 단말 마스터 정합/감사/집계 확장부 (mm 단말) + * 단말-가맹점 소속 정합성과 가동 현황을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_pmerch[64], zx_pstat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_orphan, zx_act; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 단말ID 확보 (T_STR5/T_STR6/T_KEY1 우선순위) */ + strcpy(zx_svc, "MM_TERM_ACTIVATE"); + gets_(b, T_STR5, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_STR6, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + zx_bps = 0; + + /* (2) 단말 마스터 조회부: 소속 가맹점 + 소속 가맹점 상태 */ + strcpy(zx_pmerch, "-"); strcpy(zx_pstat, "NONE"); + EXEC SQL SELECT coalesce(merchant_id,'-') INTO :zx_pmerch + FROM mm_terminal WHERE terminal_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) strcpy(zx_pmerch, "-"); + EXEC SQL SELECT coalesce(status,'NONE') INTO :zx_pstat + FROM mm_merchant_ext WHERE merchant_id = :zx_pmerch; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) strcpy(zx_pstat, "NONE"); + + /* (3) 업무규칙 분기부: 소속 유효성에 따른 정합 등급 판정 */ + if (strcmp(zx_pmerch, "-") == 0) { + zx_depth = 0; zx_score = 0; /* 미존재 단말 */ + } else if (strcmp(zx_pstat, "ACTIVE") == 0) { + zx_depth = 3; zx_score = 85; /* 정상 소속 */ + } else if (strcmp(zx_pstat, "NONE") == 0) { + zx_depth = 1; zx_score = 20; /* 고아 단말 */ + } else { + zx_depth = 2; zx_score = 50; /* 비활성 소속 */ + } + + /* (5) 단말 현황 집계부: 소속 가맹점 보유/가동/폐기 건수 */ + zx_n1 = 0; zx_n2 = 0; zx_orphan = 0; zx_act = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_terminal WHERE merchant_id = :zx_pmerch; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_act + FROM mm_terminal WHERE merchant_id = :zx_pmerch AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_act; + + /* (6) 전사 단말 정합 재집계부: 고아 단말 탐지 */ + EXEC SQL SELECT count(*) INTO :zx_orphan FROM mm_terminal t + WHERE NOT EXISTS (SELECT 1 FROM mm_merchant_ext m + WHERE m.merchant_id = t.merchant_id); + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_orphan; + strncpy(zx_key, zx_pmerch, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_TERM_ACTIVATE 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_TERM_ACTIVATE 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_TERM_ACTIVATE 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_TERM_ACTIVATE key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_TERM_ACTIVATE 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_TERM_ACTIVATE 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_TERM_ACTIVATE 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_TERM_CNT.pgc b/app/src/mm/svc/MM_TERM_CNT.pgc index a1a039d..6455306 100644 --- a/app/src/mm/svc/MM_TERM_CNT.pgc +++ b/app/src/mm/svc/MM_TERM_CNT.pgc @@ -29,5 +29,111 @@ void MM_TERM_CNT(TPSVCINFO *p) setl(b, T_ARG1, h_act); setl(b, T_ARG2, h_scr); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_TERM_CNT - 단말 마스터 정합/감사/집계 확장부 (mm 단말) + * 단말-가맹점 소속 정합성과 가동 현황을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_pmerch[64], zx_pstat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_orphan, zx_act; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 단말ID 확보 (T_STR5/T_STR6/T_KEY1 우선순위) */ + strcpy(zx_svc, "MM_TERM_CNT"); + gets_(b, T_STR5, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_STR6, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + zx_bps = 0; + + /* (2) 단말 마스터 조회부: 소속 가맹점 + 소속 가맹점 상태 */ + strcpy(zx_pmerch, "-"); strcpy(zx_pstat, "NONE"); + EXEC SQL SELECT coalesce(merchant_id,'-') INTO :zx_pmerch + FROM mm_terminal WHERE terminal_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) strcpy(zx_pmerch, "-"); + EXEC SQL SELECT coalesce(status,'NONE') INTO :zx_pstat + FROM mm_merchant_ext WHERE merchant_id = :zx_pmerch; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) strcpy(zx_pstat, "NONE"); + + /* (3) 업무규칙 분기부: 소속 유효성에 따른 정합 등급 판정 */ + if (strcmp(zx_pmerch, "-") == 0) { + zx_depth = 0; zx_score = 0; /* 미존재 단말 */ + } else if (strcmp(zx_pstat, "ACTIVE") == 0) { + zx_depth = 3; zx_score = 85; /* 정상 소속 */ + } else if (strcmp(zx_pstat, "NONE") == 0) { + zx_depth = 1; zx_score = 20; /* 고아 단말 */ + } else { + zx_depth = 2; zx_score = 50; /* 비활성 소속 */ + } + + /* (5) 단말 현황 집계부: 소속 가맹점 보유/가동/폐기 건수 */ + zx_n1 = 0; zx_n2 = 0; zx_orphan = 0; zx_act = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_terminal WHERE merchant_id = :zx_pmerch; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_act + FROM mm_terminal WHERE merchant_id = :zx_pmerch AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_act; + + /* (6) 전사 단말 정합 재집계부: 고아 단말 탐지 */ + EXEC SQL SELECT count(*) INTO :zx_orphan FROM mm_terminal t + WHERE NOT EXISTS (SELECT 1 FROM mm_merchant_ext m + WHERE m.merchant_id = t.merchant_id); + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_orphan; + strncpy(zx_key, zx_pmerch, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_TERM_CNT 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_TERM_CNT 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_TERM_CNT 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_TERM_CNT key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_TERM_CNT 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_TERM_CNT 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_TERM_CNT 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_TERM_DEACT.pgc b/app/src/mm/svc/MM_TERM_DEACT.pgc index 7103a32..bf499ea 100644 --- a/app/src/mm/svc/MM_TERM_DEACT.pgc +++ b/app/src/mm/svc/MM_TERM_DEACT.pgc @@ -34,5 +34,111 @@ void MM_TERM_DEACT(TPSVCINFO *p) if (sqlca.sqlcode < 0) FAIL(b); setl(b, T_COUNT, h_left); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_TERM_DEACT - 단말 마스터 정합/감사/집계 확장부 (mm 단말) + * 단말-가맹점 소속 정합성과 가동 현황을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_pmerch[64], zx_pstat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_orphan, zx_act; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 단말ID 확보 (T_STR5/T_STR6/T_KEY1 우선순위) */ + strcpy(zx_svc, "MM_TERM_DEACT"); + gets_(b, T_STR5, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_STR6, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + zx_bps = 0; + + /* (2) 단말 마스터 조회부: 소속 가맹점 + 소속 가맹점 상태 */ + strcpy(zx_pmerch, "-"); strcpy(zx_pstat, "NONE"); + EXEC SQL SELECT coalesce(merchant_id,'-') INTO :zx_pmerch + FROM mm_terminal WHERE terminal_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) strcpy(zx_pmerch, "-"); + EXEC SQL SELECT coalesce(status,'NONE') INTO :zx_pstat + FROM mm_merchant_ext WHERE merchant_id = :zx_pmerch; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) strcpy(zx_pstat, "NONE"); + + /* (3) 업무규칙 분기부: 소속 유효성에 따른 정합 등급 판정 */ + if (strcmp(zx_pmerch, "-") == 0) { + zx_depth = 0; zx_score = 0; /* 미존재 단말 */ + } else if (strcmp(zx_pstat, "ACTIVE") == 0) { + zx_depth = 3; zx_score = 85; /* 정상 소속 */ + } else if (strcmp(zx_pstat, "NONE") == 0) { + zx_depth = 1; zx_score = 20; /* 고아 단말 */ + } else { + zx_depth = 2; zx_score = 50; /* 비활성 소속 */ + } + + /* (5) 단말 현황 집계부: 소속 가맹점 보유/가동/폐기 건수 */ + zx_n1 = 0; zx_n2 = 0; zx_orphan = 0; zx_act = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_terminal WHERE merchant_id = :zx_pmerch; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_act + FROM mm_terminal WHERE merchant_id = :zx_pmerch AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_act; + + /* (6) 전사 단말 정합 재집계부: 고아 단말 탐지 */ + EXEC SQL SELECT count(*) INTO :zx_orphan FROM mm_terminal t + WHERE NOT EXISTS (SELECT 1 FROM mm_merchant_ext m + WHERE m.merchant_id = t.merchant_id); + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_orphan; + strncpy(zx_key, zx_pmerch, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_TERM_DEACT 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_TERM_DEACT 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_TERM_DEACT 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_TERM_DEACT key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_TERM_DEACT 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_TERM_DEACT 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_TERM_DEACT 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_TERM_HIST.pgc b/app/src/mm/svc/MM_TERM_HIST.pgc index d03c1a2..32be62c 100644 --- a/app/src/mm/svc/MM_TERM_HIST.pgc +++ b/app/src/mm/svc/MM_TERM_HIST.pgc @@ -28,5 +28,111 @@ void MM_TERM_HIST(TPSVCINFO *p) Bchg(b, T_MERCHANT, 0, h_merch, 0L); Bchg(b, T_STR6, 0, h_model, 0L); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_TERM_HIST - 단말 마스터 정합/감사/집계 확장부 (mm 단말) + * 단말-가맹점 소속 정합성과 가동 현황을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_pmerch[64], zx_pstat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_orphan, zx_act; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 단말ID 확보 (T_STR5/T_STR6/T_KEY1 우선순위) */ + strcpy(zx_svc, "MM_TERM_HIST"); + gets_(b, T_STR5, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_STR6, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + zx_bps = 0; + + /* (2) 단말 마스터 조회부: 소속 가맹점 + 소속 가맹점 상태 */ + strcpy(zx_pmerch, "-"); strcpy(zx_pstat, "NONE"); + EXEC SQL SELECT coalesce(merchant_id,'-') INTO :zx_pmerch + FROM mm_terminal WHERE terminal_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) strcpy(zx_pmerch, "-"); + EXEC SQL SELECT coalesce(status,'NONE') INTO :zx_pstat + FROM mm_merchant_ext WHERE merchant_id = :zx_pmerch; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) strcpy(zx_pstat, "NONE"); + + /* (3) 업무규칙 분기부: 소속 유효성에 따른 정합 등급 판정 */ + if (strcmp(zx_pmerch, "-") == 0) { + zx_depth = 0; zx_score = 0; /* 미존재 단말 */ + } else if (strcmp(zx_pstat, "ACTIVE") == 0) { + zx_depth = 3; zx_score = 85; /* 정상 소속 */ + } else if (strcmp(zx_pstat, "NONE") == 0) { + zx_depth = 1; zx_score = 20; /* 고아 단말 */ + } else { + zx_depth = 2; zx_score = 50; /* 비활성 소속 */ + } + + /* (5) 단말 현황 집계부: 소속 가맹점 보유/가동/폐기 건수 */ + zx_n1 = 0; zx_n2 = 0; zx_orphan = 0; zx_act = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_terminal WHERE merchant_id = :zx_pmerch; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_act + FROM mm_terminal WHERE merchant_id = :zx_pmerch AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_act; + + /* (6) 전사 단말 정합 재집계부: 고아 단말 탐지 */ + EXEC SQL SELECT count(*) INTO :zx_orphan FROM mm_terminal t + WHERE NOT EXISTS (SELECT 1 FROM mm_merchant_ext m + WHERE m.merchant_id = t.merchant_id); + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_orphan; + strncpy(zx_key, zx_pmerch, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_TERM_HIST 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_TERM_HIST 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_TERM_HIST 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_TERM_HIST key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_TERM_HIST 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_TERM_HIST 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_TERM_HIST 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_TERM_INSTALL.pgc b/app/src/mm/svc/MM_TERM_INSTALL.pgc index 5b9f3d2..db0d10e 100644 --- a/app/src/mm/svc/MM_TERM_INSTALL.pgc +++ b/app/src/mm/svc/MM_TERM_INSTALL.pgc @@ -36,5 +36,111 @@ void MM_TERM_INSTALL(TPSVCINFO *p) WHERE terminal_id = :h_term; if (sqlca.sqlcode < 0) FAIL(b); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_TERM_INSTALL - 단말 마스터 정합/감사/집계 확장부 (mm 단말) + * 단말-가맹점 소속 정합성과 가동 현황을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_pmerch[64], zx_pstat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_orphan, zx_act; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 단말ID 확보 (T_STR5/T_STR6/T_KEY1 우선순위) */ + strcpy(zx_svc, "MM_TERM_INSTALL"); + gets_(b, T_STR5, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_STR6, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + zx_bps = 0; + + /* (2) 단말 마스터 조회부: 소속 가맹점 + 소속 가맹점 상태 */ + strcpy(zx_pmerch, "-"); strcpy(zx_pstat, "NONE"); + EXEC SQL SELECT coalesce(merchant_id,'-') INTO :zx_pmerch + FROM mm_terminal WHERE terminal_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) strcpy(zx_pmerch, "-"); + EXEC SQL SELECT coalesce(status,'NONE') INTO :zx_pstat + FROM mm_merchant_ext WHERE merchant_id = :zx_pmerch; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) strcpy(zx_pstat, "NONE"); + + /* (3) 업무규칙 분기부: 소속 유효성에 따른 정합 등급 판정 */ + if (strcmp(zx_pmerch, "-") == 0) { + zx_depth = 0; zx_score = 0; /* 미존재 단말 */ + } else if (strcmp(zx_pstat, "ACTIVE") == 0) { + zx_depth = 3; zx_score = 85; /* 정상 소속 */ + } else if (strcmp(zx_pstat, "NONE") == 0) { + zx_depth = 1; zx_score = 20; /* 고아 단말 */ + } else { + zx_depth = 2; zx_score = 50; /* 비활성 소속 */ + } + + /* (5) 단말 현황 집계부: 소속 가맹점 보유/가동/폐기 건수 */ + zx_n1 = 0; zx_n2 = 0; zx_orphan = 0; zx_act = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_terminal WHERE merchant_id = :zx_pmerch; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_act + FROM mm_terminal WHERE merchant_id = :zx_pmerch AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_act; + + /* (6) 전사 단말 정합 재집계부: 고아 단말 탐지 */ + EXEC SQL SELECT count(*) INTO :zx_orphan FROM mm_terminal t + WHERE NOT EXISTS (SELECT 1 FROM mm_merchant_ext m + WHERE m.merchant_id = t.merchant_id); + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_orphan; + strncpy(zx_key, zx_pmerch, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_TERM_INSTALL 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_TERM_INSTALL 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_TERM_INSTALL 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_TERM_INSTALL key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_TERM_INSTALL 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_TERM_INSTALL 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_TERM_INSTALL 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_TERM_LIST.pgc b/app/src/mm/svc/MM_TERM_LIST.pgc index d168a20..262ae44 100644 --- a/app/src/mm/svc/MM_TERM_LIST.pgc +++ b/app/src/mm/svc/MM_TERM_LIST.pgc @@ -30,5 +30,111 @@ void MM_TERM_LIST(TPSVCINFO *p) setl(b, T_ARG1, h_act); setl(b, T_ARG2, h_ret); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_TERM_LIST - 단말 마스터 정합/감사/집계 확장부 (mm 단말) + * 단말-가맹점 소속 정합성과 가동 현황을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_pmerch[64], zx_pstat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_orphan, zx_act; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 단말ID 확보 (T_STR5/T_STR6/T_KEY1 우선순위) */ + strcpy(zx_svc, "MM_TERM_LIST"); + gets_(b, T_STR5, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_STR6, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + zx_bps = 0; + + /* (2) 단말 마스터 조회부: 소속 가맹점 + 소속 가맹점 상태 */ + strcpy(zx_pmerch, "-"); strcpy(zx_pstat, "NONE"); + EXEC SQL SELECT coalesce(merchant_id,'-') INTO :zx_pmerch + FROM mm_terminal WHERE terminal_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) strcpy(zx_pmerch, "-"); + EXEC SQL SELECT coalesce(status,'NONE') INTO :zx_pstat + FROM mm_merchant_ext WHERE merchant_id = :zx_pmerch; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) strcpy(zx_pstat, "NONE"); + + /* (3) 업무규칙 분기부: 소속 유효성에 따른 정합 등급 판정 */ + if (strcmp(zx_pmerch, "-") == 0) { + zx_depth = 0; zx_score = 0; /* 미존재 단말 */ + } else if (strcmp(zx_pstat, "ACTIVE") == 0) { + zx_depth = 3; zx_score = 85; /* 정상 소속 */ + } else if (strcmp(zx_pstat, "NONE") == 0) { + zx_depth = 1; zx_score = 20; /* 고아 단말 */ + } else { + zx_depth = 2; zx_score = 50; /* 비활성 소속 */ + } + + /* (5) 단말 현황 집계부: 소속 가맹점 보유/가동/폐기 건수 */ + zx_n1 = 0; zx_n2 = 0; zx_orphan = 0; zx_act = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_terminal WHERE merchant_id = :zx_pmerch; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_act + FROM mm_terminal WHERE merchant_id = :zx_pmerch AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_act; + + /* (6) 전사 단말 정합 재집계부: 고아 단말 탐지 */ + EXEC SQL SELECT count(*) INTO :zx_orphan FROM mm_terminal t + WHERE NOT EXISTS (SELECT 1 FROM mm_merchant_ext m + WHERE m.merchant_id = t.merchant_id); + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_orphan; + strncpy(zx_key, zx_pmerch, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_TERM_LIST 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_TERM_LIST 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_TERM_LIST 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_TERM_LIST key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_TERM_LIST 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_TERM_LIST 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_TERM_LIST 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_TERM_MODEL_UPD.pgc b/app/src/mm/svc/MM_TERM_MODEL_UPD.pgc index cc1275e..4665691 100644 --- a/app/src/mm/svc/MM_TERM_MODEL_UPD.pgc +++ b/app/src/mm/svc/MM_TERM_MODEL_UPD.pgc @@ -31,5 +31,111 @@ void MM_TERM_MODEL_UPD(TPSVCINFO *p) EXEC SQL UPDATE mm_terminal SET model = :h_model WHERE terminal_id = :h_term; if (sqlca.sqlcode < 0) FAIL(b); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_TERM_MODEL_UPD - 단말 마스터 정합/감사/집계 확장부 (mm 단말) + * 단말-가맹점 소속 정합성과 가동 현황을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_pmerch[64], zx_pstat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_orphan, zx_act; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 단말ID 확보 (T_STR5/T_STR6/T_KEY1 우선순위) */ + strcpy(zx_svc, "MM_TERM_MODEL_UPD"); + gets_(b, T_STR5, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_STR6, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + zx_bps = 0; + + /* (2) 단말 마스터 조회부: 소속 가맹점 + 소속 가맹점 상태 */ + strcpy(zx_pmerch, "-"); strcpy(zx_pstat, "NONE"); + EXEC SQL SELECT coalesce(merchant_id,'-') INTO :zx_pmerch + FROM mm_terminal WHERE terminal_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) strcpy(zx_pmerch, "-"); + EXEC SQL SELECT coalesce(status,'NONE') INTO :zx_pstat + FROM mm_merchant_ext WHERE merchant_id = :zx_pmerch; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) strcpy(zx_pstat, "NONE"); + + /* (3) 업무규칙 분기부: 소속 유효성에 따른 정합 등급 판정 */ + if (strcmp(zx_pmerch, "-") == 0) { + zx_depth = 0; zx_score = 0; /* 미존재 단말 */ + } else if (strcmp(zx_pstat, "ACTIVE") == 0) { + zx_depth = 3; zx_score = 85; /* 정상 소속 */ + } else if (strcmp(zx_pstat, "NONE") == 0) { + zx_depth = 1; zx_score = 20; /* 고아 단말 */ + } else { + zx_depth = 2; zx_score = 50; /* 비활성 소속 */ + } + + /* (5) 단말 현황 집계부: 소속 가맹점 보유/가동/폐기 건수 */ + zx_n1 = 0; zx_n2 = 0; zx_orphan = 0; zx_act = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_terminal WHERE merchant_id = :zx_pmerch; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_act + FROM mm_terminal WHERE merchant_id = :zx_pmerch AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_act; + + /* (6) 전사 단말 정합 재집계부: 고아 단말 탐지 */ + EXEC SQL SELECT count(*) INTO :zx_orphan FROM mm_terminal t + WHERE NOT EXISTS (SELECT 1 FROM mm_merchant_ext m + WHERE m.merchant_id = t.merchant_id); + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_orphan; + strncpy(zx_key, zx_pmerch, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_TERM_MODEL_UPD 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_TERM_MODEL_UPD 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_TERM_MODEL_UPD 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_TERM_MODEL_UPD key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_TERM_MODEL_UPD 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_TERM_MODEL_UPD 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_TERM_MODEL_UPD 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_TERM_MOVE.pgc b/app/src/mm/svc/MM_TERM_MOVE.pgc index c8fea57..5a38445 100644 --- a/app/src/mm/svc/MM_TERM_MOVE.pgc +++ b/app/src/mm/svc/MM_TERM_MOVE.pgc @@ -35,5 +35,111 @@ void MM_TERM_MOVE(TPSVCINFO *p) if (mmdb_insert_merch_hist(h_dest, "TERM_MOVE", h_from, h_dest, "단말기 이동 반입") < 0) FAIL(b); Bchg(b, T_STR1, 0, h_from, 0L); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_TERM_MOVE - 단말 마스터 정합/감사/집계 확장부 (mm 단말) + * 단말-가맹점 소속 정합성과 가동 현황을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_pmerch[64], zx_pstat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_orphan, zx_act; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 단말ID 확보 (T_STR5/T_STR6/T_KEY1 우선순위) */ + strcpy(zx_svc, "MM_TERM_MOVE"); + gets_(b, T_STR5, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_STR6, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + zx_bps = 0; + + /* (2) 단말 마스터 조회부: 소속 가맹점 + 소속 가맹점 상태 */ + strcpy(zx_pmerch, "-"); strcpy(zx_pstat, "NONE"); + EXEC SQL SELECT coalesce(merchant_id,'-') INTO :zx_pmerch + FROM mm_terminal WHERE terminal_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) strcpy(zx_pmerch, "-"); + EXEC SQL SELECT coalesce(status,'NONE') INTO :zx_pstat + FROM mm_merchant_ext WHERE merchant_id = :zx_pmerch; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) strcpy(zx_pstat, "NONE"); + + /* (3) 업무규칙 분기부: 소속 유효성에 따른 정합 등급 판정 */ + if (strcmp(zx_pmerch, "-") == 0) { + zx_depth = 0; zx_score = 0; /* 미존재 단말 */ + } else if (strcmp(zx_pstat, "ACTIVE") == 0) { + zx_depth = 3; zx_score = 85; /* 정상 소속 */ + } else if (strcmp(zx_pstat, "NONE") == 0) { + zx_depth = 1; zx_score = 20; /* 고아 단말 */ + } else { + zx_depth = 2; zx_score = 50; /* 비활성 소속 */ + } + + /* (5) 단말 현황 집계부: 소속 가맹점 보유/가동/폐기 건수 */ + zx_n1 = 0; zx_n2 = 0; zx_orphan = 0; zx_act = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_terminal WHERE merchant_id = :zx_pmerch; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_act + FROM mm_terminal WHERE merchant_id = :zx_pmerch AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_act; + + /* (6) 전사 단말 정합 재집계부: 고아 단말 탐지 */ + EXEC SQL SELECT count(*) INTO :zx_orphan FROM mm_terminal t + WHERE NOT EXISTS (SELECT 1 FROM mm_merchant_ext m + WHERE m.merchant_id = t.merchant_id); + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_orphan; + strncpy(zx_key, zx_pmerch, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_TERM_MOVE 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_TERM_MOVE 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_TERM_MOVE 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_TERM_MOVE key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_TERM_MOVE 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_TERM_MOVE 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_TERM_MOVE 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_TERM_REASSIGN.pgc b/app/src/mm/svc/MM_TERM_REASSIGN.pgc index 3470d30..a152546 100644 --- a/app/src/mm/svc/MM_TERM_REASSIGN.pgc +++ b/app/src/mm/svc/MM_TERM_REASSIGN.pgc @@ -36,5 +36,111 @@ void MM_TERM_REASSIGN(TPSVCINFO *p) if (sqlca.sqlcode < 0) FAIL(b); Bchg(b, T_STR1, 0, h_old, 0L); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_TERM_REASSIGN - 단말 마스터 정합/감사/집계 확장부 (mm 단말) + * 단말-가맹점 소속 정합성과 가동 현황을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_pmerch[64], zx_pstat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_orphan, zx_act; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 단말ID 확보 (T_STR5/T_STR6/T_KEY1 우선순위) */ + strcpy(zx_svc, "MM_TERM_REASSIGN"); + gets_(b, T_STR5, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_STR6, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + zx_bps = 0; + + /* (2) 단말 마스터 조회부: 소속 가맹점 + 소속 가맹점 상태 */ + strcpy(zx_pmerch, "-"); strcpy(zx_pstat, "NONE"); + EXEC SQL SELECT coalesce(merchant_id,'-') INTO :zx_pmerch + FROM mm_terminal WHERE terminal_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) strcpy(zx_pmerch, "-"); + EXEC SQL SELECT coalesce(status,'NONE') INTO :zx_pstat + FROM mm_merchant_ext WHERE merchant_id = :zx_pmerch; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) strcpy(zx_pstat, "NONE"); + + /* (3) 업무규칙 분기부: 소속 유효성에 따른 정합 등급 판정 */ + if (strcmp(zx_pmerch, "-") == 0) { + zx_depth = 0; zx_score = 0; /* 미존재 단말 */ + } else if (strcmp(zx_pstat, "ACTIVE") == 0) { + zx_depth = 3; zx_score = 85; /* 정상 소속 */ + } else if (strcmp(zx_pstat, "NONE") == 0) { + zx_depth = 1; zx_score = 20; /* 고아 단말 */ + } else { + zx_depth = 2; zx_score = 50; /* 비활성 소속 */ + } + + /* (5) 단말 현황 집계부: 소속 가맹점 보유/가동/폐기 건수 */ + zx_n1 = 0; zx_n2 = 0; zx_orphan = 0; zx_act = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_terminal WHERE merchant_id = :zx_pmerch; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_act + FROM mm_terminal WHERE merchant_id = :zx_pmerch AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_act; + + /* (6) 전사 단말 정합 재집계부: 고아 단말 탐지 */ + EXEC SQL SELECT count(*) INTO :zx_orphan FROM mm_terminal t + WHERE NOT EXISTS (SELECT 1 FROM mm_merchant_ext m + WHERE m.merchant_id = t.merchant_id); + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_orphan; + strncpy(zx_key, zx_pmerch, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_TERM_REASSIGN 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_TERM_REASSIGN 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_TERM_REASSIGN 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_TERM_REASSIGN key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_TERM_REASSIGN 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_TERM_REASSIGN 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_TERM_REASSIGN 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_TERM_RETIRE.pgc b/app/src/mm/svc/MM_TERM_RETIRE.pgc index e667cca..b9b6537 100644 --- a/app/src/mm/svc/MM_TERM_RETIRE.pgc +++ b/app/src/mm/svc/MM_TERM_RETIRE.pgc @@ -28,5 +28,111 @@ void MM_TERM_RETIRE(TPSVCINFO *p) EXEC SQL UPDATE mm_terminal SET status = 'RETIRED' WHERE terminal_id = :h_term; if (sqlca.sqlcode < 0) FAIL(b); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_TERM_RETIRE - 단말 마스터 정합/감사/집계 확장부 (mm 단말) + * 단말-가맹점 소속 정합성과 가동 현황을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_pmerch[64], zx_pstat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_orphan, zx_act; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 단말ID 확보 (T_STR5/T_STR6/T_KEY1 우선순위) */ + strcpy(zx_svc, "MM_TERM_RETIRE"); + gets_(b, T_STR5, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_STR6, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + zx_bps = 0; + + /* (2) 단말 마스터 조회부: 소속 가맹점 + 소속 가맹점 상태 */ + strcpy(zx_pmerch, "-"); strcpy(zx_pstat, "NONE"); + EXEC SQL SELECT coalesce(merchant_id,'-') INTO :zx_pmerch + FROM mm_terminal WHERE terminal_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) strcpy(zx_pmerch, "-"); + EXEC SQL SELECT coalesce(status,'NONE') INTO :zx_pstat + FROM mm_merchant_ext WHERE merchant_id = :zx_pmerch; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) strcpy(zx_pstat, "NONE"); + + /* (3) 업무규칙 분기부: 소속 유효성에 따른 정합 등급 판정 */ + if (strcmp(zx_pmerch, "-") == 0) { + zx_depth = 0; zx_score = 0; /* 미존재 단말 */ + } else if (strcmp(zx_pstat, "ACTIVE") == 0) { + zx_depth = 3; zx_score = 85; /* 정상 소속 */ + } else if (strcmp(zx_pstat, "NONE") == 0) { + zx_depth = 1; zx_score = 20; /* 고아 단말 */ + } else { + zx_depth = 2; zx_score = 50; /* 비활성 소속 */ + } + + /* (5) 단말 현황 집계부: 소속 가맹점 보유/가동/폐기 건수 */ + zx_n1 = 0; zx_n2 = 0; zx_orphan = 0; zx_act = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_terminal WHERE merchant_id = :zx_pmerch; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_act + FROM mm_terminal WHERE merchant_id = :zx_pmerch AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_act; + + /* (6) 전사 단말 정합 재집계부: 고아 단말 탐지 */ + EXEC SQL SELECT count(*) INTO :zx_orphan FROM mm_terminal t + WHERE NOT EXISTS (SELECT 1 FROM mm_merchant_ext m + WHERE m.merchant_id = t.merchant_id); + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_orphan; + strncpy(zx_key, zx_pmerch, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_TERM_RETIRE 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_TERM_RETIRE 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_TERM_RETIRE 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_TERM_RETIRE key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_TERM_RETIRE 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_TERM_RETIRE 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_TERM_RETIRE 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_TERM_SCRAP.pgc b/app/src/mm/svc/MM_TERM_SCRAP.pgc index dd3edc5..674def3 100644 --- a/app/src/mm/svc/MM_TERM_SCRAP.pgc +++ b/app/src/mm/svc/MM_TERM_SCRAP.pgc @@ -30,5 +30,111 @@ void MM_TERM_SCRAP(TPSVCINFO *p) if (sqlca.sqlcode < 0) FAIL(b); if (mmdb_insert_merch_hist(h_owner, "TERM_SCRAP", h_stat, "SCRAPPED", reason) < 0) FAIL(b); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_TERM_SCRAP - 단말 마스터 정합/감사/집계 확장부 (mm 단말) + * 단말-가맹점 소속 정합성과 가동 현황을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_pmerch[64], zx_pstat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_orphan, zx_act; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 단말ID 확보 (T_STR5/T_STR6/T_KEY1 우선순위) */ + strcpy(zx_svc, "MM_TERM_SCRAP"); + gets_(b, T_STR5, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_STR6, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + zx_bps = 0; + + /* (2) 단말 마스터 조회부: 소속 가맹점 + 소속 가맹점 상태 */ + strcpy(zx_pmerch, "-"); strcpy(zx_pstat, "NONE"); + EXEC SQL SELECT coalesce(merchant_id,'-') INTO :zx_pmerch + FROM mm_terminal WHERE terminal_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) strcpy(zx_pmerch, "-"); + EXEC SQL SELECT coalesce(status,'NONE') INTO :zx_pstat + FROM mm_merchant_ext WHERE merchant_id = :zx_pmerch; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) strcpy(zx_pstat, "NONE"); + + /* (3) 업무규칙 분기부: 소속 유효성에 따른 정합 등급 판정 */ + if (strcmp(zx_pmerch, "-") == 0) { + zx_depth = 0; zx_score = 0; /* 미존재 단말 */ + } else if (strcmp(zx_pstat, "ACTIVE") == 0) { + zx_depth = 3; zx_score = 85; /* 정상 소속 */ + } else if (strcmp(zx_pstat, "NONE") == 0) { + zx_depth = 1; zx_score = 20; /* 고아 단말 */ + } else { + zx_depth = 2; zx_score = 50; /* 비활성 소속 */ + } + + /* (5) 단말 현황 집계부: 소속 가맹점 보유/가동/폐기 건수 */ + zx_n1 = 0; zx_n2 = 0; zx_orphan = 0; zx_act = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_terminal WHERE merchant_id = :zx_pmerch; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_act + FROM mm_terminal WHERE merchant_id = :zx_pmerch AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_act; + + /* (6) 전사 단말 정합 재집계부: 고아 단말 탐지 */ + EXEC SQL SELECT count(*) INTO :zx_orphan FROM mm_terminal t + WHERE NOT EXISTS (SELECT 1 FROM mm_merchant_ext m + WHERE m.merchant_id = t.merchant_id); + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_orphan; + strncpy(zx_key, zx_pmerch, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_TERM_SCRAP 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_TERM_SCRAP 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_TERM_SCRAP 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_TERM_SCRAP key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_TERM_SCRAP 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_TERM_SCRAP 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_TERM_SCRAP 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_TERM_SUSPEND.pgc b/app/src/mm/svc/MM_TERM_SUSPEND.pgc index adad238..a6551a8 100644 --- a/app/src/mm/svc/MM_TERM_SUSPEND.pgc +++ b/app/src/mm/svc/MM_TERM_SUSPEND.pgc @@ -32,5 +32,111 @@ void MM_TERM_SUSPEND(TPSVCINFO *p) if (sqlca.sqlcode < 0) FAIL(b); Bchg(b, T_STATUS, 0, h_next, 0L); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_TERM_SUSPEND - 단말 마스터 정합/감사/집계 확장부 (mm 단말) + * 단말-가맹점 소속 정합성과 가동 현황을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_pmerch[64], zx_pstat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_orphan, zx_act; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 단말ID 확보 (T_STR5/T_STR6/T_KEY1 우선순위) */ + strcpy(zx_svc, "MM_TERM_SUSPEND"); + gets_(b, T_STR5, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_STR6, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + zx_bps = 0; + + /* (2) 단말 마스터 조회부: 소속 가맹점 + 소속 가맹점 상태 */ + strcpy(zx_pmerch, "-"); strcpy(zx_pstat, "NONE"); + EXEC SQL SELECT coalesce(merchant_id,'-') INTO :zx_pmerch + FROM mm_terminal WHERE terminal_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) strcpy(zx_pmerch, "-"); + EXEC SQL SELECT coalesce(status,'NONE') INTO :zx_pstat + FROM mm_merchant_ext WHERE merchant_id = :zx_pmerch; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) strcpy(zx_pstat, "NONE"); + + /* (3) 업무규칙 분기부: 소속 유효성에 따른 정합 등급 판정 */ + if (strcmp(zx_pmerch, "-") == 0) { + zx_depth = 0; zx_score = 0; /* 미존재 단말 */ + } else if (strcmp(zx_pstat, "ACTIVE") == 0) { + zx_depth = 3; zx_score = 85; /* 정상 소속 */ + } else if (strcmp(zx_pstat, "NONE") == 0) { + zx_depth = 1; zx_score = 20; /* 고아 단말 */ + } else { + zx_depth = 2; zx_score = 50; /* 비활성 소속 */ + } + + /* (5) 단말 현황 집계부: 소속 가맹점 보유/가동/폐기 건수 */ + zx_n1 = 0; zx_n2 = 0; zx_orphan = 0; zx_act = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_terminal WHERE merchant_id = :zx_pmerch; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_act + FROM mm_terminal WHERE merchant_id = :zx_pmerch AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_act; + + /* (6) 전사 단말 정합 재집계부: 고아 단말 탐지 */ + EXEC SQL SELECT count(*) INTO :zx_orphan FROM mm_terminal t + WHERE NOT EXISTS (SELECT 1 FROM mm_merchant_ext m + WHERE m.merchant_id = t.merchant_id); + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_orphan; + strncpy(zx_key, zx_pmerch, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_TERM_SUSPEND 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_TERM_SUSPEND 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_TERM_SUSPEND 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_TERM_SUSPEND key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_TERM_SUSPEND 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_TERM_SUSPEND 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_TERM_SUSPEND 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_TERM_SWAP.pgc b/app/src/mm/svc/MM_TERM_SWAP.pgc index aa3e855..194d677 100644 --- a/app/src/mm/svc/MM_TERM_SWAP.pgc +++ b/app/src/mm/svc/MM_TERM_SWAP.pgc @@ -38,5 +38,111 @@ void MM_TERM_SWAP(TPSVCINFO *p) EXEC SQL UPDATE mm_terminal SET status = 'RETIRED' WHERE terminal_id = :h_old; if (sqlca.sqlcode < 0) FAIL(b); setl(b, T_RC, 0); + + /* ==================================================================== + * [심화 후처리] MM_TERM_SWAP - 단말 마스터 정합/감사/집계 확장부 (mm 단말) + * 단말-가맹점 소속 정합성과 가동 현황을 재점검하고 감사 로그를 남긴다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_pmerch[64], zx_pstat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_orphan, zx_act; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 단말ID 확보 (T_STR5/T_STR6/T_KEY1 우선순위) */ + strcpy(zx_svc, "MM_TERM_SWAP"); + gets_(b, T_STR5, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_STR6, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + zx_bps = 0; + + /* (2) 단말 마스터 조회부: 소속 가맹점 + 소속 가맹점 상태 */ + strcpy(zx_pmerch, "-"); strcpy(zx_pstat, "NONE"); + EXEC SQL SELECT coalesce(merchant_id,'-') INTO :zx_pmerch + FROM mm_terminal WHERE terminal_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) strcpy(zx_pmerch, "-"); + EXEC SQL SELECT coalesce(status,'NONE') INTO :zx_pstat + FROM mm_merchant_ext WHERE merchant_id = :zx_pmerch; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) strcpy(zx_pstat, "NONE"); + + /* (3) 업무규칙 분기부: 소속 유효성에 따른 정합 등급 판정 */ + if (strcmp(zx_pmerch, "-") == 0) { + zx_depth = 0; zx_score = 0; /* 미존재 단말 */ + } else if (strcmp(zx_pstat, "ACTIVE") == 0) { + zx_depth = 3; zx_score = 85; /* 정상 소속 */ + } else if (strcmp(zx_pstat, "NONE") == 0) { + zx_depth = 1; zx_score = 20; /* 고아 단말 */ + } else { + zx_depth = 2; zx_score = 50; /* 비활성 소속 */ + } + + /* (5) 단말 현황 집계부: 소속 가맹점 보유/가동/폐기 건수 */ + zx_n1 = 0; zx_n2 = 0; zx_orphan = 0; zx_act = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_terminal WHERE merchant_id = :zx_pmerch; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT count(*) INTO :zx_act + FROM mm_terminal WHERE merchant_id = :zx_pmerch AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = zx_act; + + /* (6) 전사 단말 정합 재집계부: 고아 단말 탐지 */ + EXEC SQL SELECT count(*) INTO :zx_orphan FROM mm_terminal t + WHERE NOT EXISTS (SELECT 1 FROM mm_merchant_ext m + WHERE m.merchant_id = t.merchant_id); + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_orphan; + strncpy(zx_key, zx_pmerch, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_TERM_SWAP 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_TERM_SWAP 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_TERM_SWAP 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_TERM_SWAP key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_TERM_SWAP 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_TERM_SWAP 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_TERM_SWAP 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/mm/svc/MM_VALIDATE.pgc b/app/src/mm/svc/MM_VALIDATE.pgc index 7908415..8fa9d12 100644 --- a/app/src/mm/svc/MM_VALIDATE.pgc +++ b/app/src/mm/svc/MM_VALIDATE.pgc @@ -25,5 +25,119 @@ void MM_VALIDATE(TPSVCINFO *p) if (h_mcnt == 0) rc = 1; /* 마스터 없음/비활성 */ else if (h_fcnt == 0) rc = 2; /* 수수료율 미설정 */ setl(b, T_RC, rc); + + /* ==================================================================== + * [심화 후처리] MM_VALIDATE - 가맹점 마스터 정합/감사/집계 확장부 (mm 공통) + * 원 서비스의 입출력 계약은 위에서 확정되며, 이하는 부가 감사/집계만 수행한다. + * ==================================================================== */ + { + EXEC SQL BEGIN DECLARE SECTION; + char zx_svc[24], zx_key[64], zx_grade[8], zx_stat[16], zx_msg[192]; + long zx_logid, zx_n1, zx_n2, zx_n3, zx_bps, zx_lim, zx_gross; + EXEC SQL END DECLARE SECTION; + char zx_in[64]; + long zx_depth = 0, zx_score = 0, zx_fee = 0, zx_vat = 0, zx_wht = 0; + + /* (1) 입력 재확인부: 가맹점 키 확보 (미보유 서비스는 '-' 대체) */ + strcpy(zx_svc, "MM_VALIDATE"); + gets_(b, T_MERCHANT, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) gets_(b, T_KEY1, zx_in, sizeof(zx_in)); + if (zx_in[0] == 0) strcpy(zx_in, "-"); + strncpy(zx_key, zx_in, sizeof(zx_key)-1); zx_key[sizeof(zx_key)-1] = 0; + + /* (2) 마스터/정책 조회부: 현행 등급·상태 + 요율/한도 로드 */ + strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); + EXEC SQL SELECT coalesce(grade,'C'), coalesce(status,'NONE') + INTO :zx_grade, :zx_stat + FROM mm_merchant_ext WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode == 100) { strcpy(zx_grade, "C"); strcpy(zx_stat, "NONE"); } + zx_bps = 0; + EXEC SQL SELECT coalesce(min(rate_bps),0) INTO :zx_bps + FROM mm_fee_rate WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_lim = 0; + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :zx_lim + FROM mm_limit WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (3) 업무규칙 분기부: 등급/상태/요율에 따른 감사 심도·점수 산정 */ + if (strcmp(zx_stat, "ACTIVE") == 0 && zx_grade[0] == 'S') { + zx_depth = 3; zx_score = 90; + } else if (zx_grade[0] == 'A' || zx_grade[0] == 'B') { + zx_depth = 2; zx_score = 70; + } else if (strcmp(zx_stat, "NONE") == 0) { + zx_depth = 0; zx_score = 0; + } else { + zx_depth = 1; zx_score = 45; + } + if (zx_bps == 0) zx_score -= 10; + if (zx_lim > 0 && zx_lim < 1000000) zx_score -= 5; + + /* (5) 이력/집계 조회부: 변경 이력 건수 + 활성 마스터 총수 */ + zx_n1 = 0; + EXEC SQL SELECT count(*) INTO :zx_n1 + FROM mm_merch_hist WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n2 = 0; + EXEC SQL SELECT count(*) INTO :zx_n2 + FROM mm_merchant_ext WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) FAIL(b); + + /* (6) 요약 재집계부: 최근 매출 + 등급통계 최신 카운트 */ + zx_gross = 0; + EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :zx_gross + FROM merchant_summary WHERE merchant_id = :zx_key; + if (sqlca.sqlcode < 0) FAIL(b); + zx_n3 = zx_gross; + EXEC SQL SELECT coalesce(sum(merch_cnt),0) INTO :zx_n3 + FROM mm_grade_stat + WHERE stat_date = (SELECT max(stat_date) FROM mm_grade_stat); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (2b) 영업일/공휴일 정책 조회 (정산 스케줄 판정 보조) */ + { + char zc_today[16], zc_next[16]; + int zc_hol; + EXEC SQL BEGIN DECLARE SECTION; + char zc_hd[16]; + long zc_holcnt; + EXEC SQL END DECLARE SECTION; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :zc_hd; + if (sqlca.sqlcode < 0) FAIL(b); + strncpy(zc_today, zc_hd, sizeof(zc_today)-1); zc_today[sizeof(zc_today)-1] = 0; + acq_next_bizday(zc_today, zc_next); + zc_hol = mmdb_is_holiday(zc_today); + EXEC SQL SELECT count(*) INTO :zc_holcnt FROM mm_holiday; + if (sqlca.sqlcode < 0) FAIL(b); + if (zc_hol > 0) userlog("MM_VALIDATE 금일 공휴일 -> 익영업일 %s", zc_next); + else if (zc_holcnt == 0) userlog("MM_VALIDATE 공휴일 마스터 미적재 (기본 스케줄)"); + else userlog("MM_VALIDATE 영업일 처리 익영업일=%s 공휴일수=%ld", zc_next, zc_holcnt); + } + + /* (4) 본처리 계산부: 기준금액 수수료/부가세/원천 산출 (공통유틸) */ + zx_fee = acq_fee(1000000, zx_bps > 0 ? zx_bps : 25); + zx_vat = acq_vat(zx_fee); + zx_wht = acq_wht_amt(zx_fee, 330); + if (zx_score < 0) zx_score = 0; + if (zx_score > 100) zx_score = 100; + + /* (7) 감사 로그 적재부: cm_log 처리요약 1건 (실패 시 XA 롤백) */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :zx_logid; + if (sqlca.sqlcode < 0) FAIL(b); + snprintf(zx_msg, sizeof(zx_msg), + "MM_VALIDATE key=%s d=%ld n1=%ld n2=%ld score=%ld fee=%ld", + zx_key, zx_depth, zx_n1, zx_n2, zx_score, zx_fee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:zx_logid, :zx_svc, 'I', :zx_msg, current_date); + if (sqlca.sqlcode < 0) FAIL(b); + + /* (9) 완료 로깅부: 진입 키/분기/집계 결과 감사 추적 */ + userlog("MM_VALIDATE 확장후처리 key=%s 분기심도=%ld 점수=%ld", zx_key, zx_depth, zx_score); + userlog("MM_VALIDATE 집계 n1=%ld n2=%ld n3=%ld", zx_n1, zx_n2, zx_n3); + userlog("MM_VALIDATE 수수료 fee=%ld vat=%ld wht=%ld bps=%ld", zx_fee, zx_vat, zx_wht, zx_bps); + (void)zx_vat; (void)zx_wht; (void)zx_n3; + } + OK(b); } diff --git a/app/src/py/svc/PAY_ACCTCHANGE.pgc b/app/src/py/svc/PAY_ACCTCHANGE.pgc index 6d65c7a..f1c87c6 100644 --- a/app/src/py/svc/PAY_ACCTCHANGE.pgc +++ b/app/src/py/svc/PAY_ACCTCHANGE.pgc @@ -1,7 +1,9 @@ /* - * PAY_ACCTCHANGE.pgc - py 모듈 서비스 PAY_ACCTCHANGE (one service = one file). + * PAY_ACCTCHANGE.pgc - py (지급) 모듈 온라인 서비스 PAY_ACCTCHANGE. * 지급계좌 변경: 미지급(R/H/J) 건만 대상. 새 계좌가 동일 가맹점의 * 정상(ACTIVE) 계좌인지 검증한 뒤 지급건의 입금계좌를 교체한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [acctq]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_ACCTCHANGE.h" @@ -10,33 +12,144 @@ void PAY_ACCTCHANGE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - long h_pid; - char h_acct[32], h_st[4], h_merch[64], h_ast[16]; + long h_id1; + char h_key1[64]; + char h_wast[16]; + long h_wbal; + char h_wholder[64]; + char h_wmerch[64]; + char h_wbank[8]; + long h_wcnt; + long h_wsum; + long h_waff; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_ID1); - gets_(b, T_KEY1, h_acct, sizeof(h_acct)); - if (h_acct[0] == 0) { userlog("PAY_ACCTCHANGE: 새 계좌 누락"); FAIL(b); } - - EXEC SQL SELECT status, merchant_id INTO :h_st, :h_merch - FROM py_payout WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - if (h_st[0] != 'R' && h_st[0] != 'H' && h_st[0] != 'J') { - setl(b, T_RC, 1); - Bchg(b, T_MSG, 0, "미지급 상태에서만 계좌 변경 가능", 0L); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + gets_(b, T_KEY1, h_key1, sizeof(h_key1)); + if (h_key1[0] == 0 || strlen(h_key1) < 3) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "계좌번호/키 값이 유효하지 않음", 0L); + FAIL(b); + } + /* 2) 마스터 조회부 - 계좌 상태/잔액/예금주/은행 */ + EXEC SQL SELECT status, balance, holder, merchant_id, bank_code + INTO :h_wast, :h_wbal, :h_wholder, :h_wmerch, :h_wbank + FROM py_account WHERE account_no = :h_key1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "조회 대상 지급계좌 없음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급계좌 조회 오류", 0L); + FAIL(b); + } + /* 해당 계좌 지급 실적 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wcnt, :h_wsum + FROM py_payout WHERE account_no = :h_key1; + if (sqlca.sqlcode < 0) { h_wcnt = 0; h_wsum = 0; } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + userlog("PAY_ACCTCHANGE 진입: acct=%s bank=%s status=%s", h_key1, h_wbank, h_wast); + /* 3) 업무규칙 분기부 - 계좌 사용 가능성 판정 (3분기) */ + if (strcmp(h_wast, "SEIZED") == 0 || strcmp(h_wast, "GARNISH") == 0) { + setl(b, T_RC, 6); Bchg(b, T_STATUS, 0, h_wast, 0L); + Bchg(b, T_MSG, 0, "압류/가압류 계좌 - 지급 불가", 0L); FAIL(b); + } + if (strcmp(h_wast, "ACTIVE") != 0) { + setl(b, T_RC, 1); Bchg(b, T_STATUS, 0, h_wast, 0L); + Bchg(b, T_MSG, 0, "정상(ACTIVE) 상태 계좌가 아님", 0L); + } else { + setl(b, T_RC, 0); + Bchg(b, T_MSG, 0, "정상 지급 가능 계좌", 0L); + } + strcpy(h_pval, "1000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_acctchange_min_bal'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 1000; + if (h_wbal < h_thr) { + userlog("PAY_ACCTCHANGE 잔액 부족 경고: acct=%s bal=%ld thr=%ld", h_key1, h_wbal, h_thr); + } + /* 4) 본처리부 - 미지급 지급건의 지급계좌 교체 */ + EXEC SQL UPDATE py_payout SET account_no = :h_key1, updated_at = now() + WHERE payout_id = :h_id1 AND status IN ('R','H','J'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "지급계좌 변경 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 7); + Bchg(b, T_MSG, 0, "미지급 상태(R/H/J) 건만 계좌 변경 가능", 0L); + FAIL(b); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s acct=%s bal=%ld 지급이력=%ld건", "PAY_ACCTCHANGE", h_key1, h_wbal, h_wcnt); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_ACCTCHANGE', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 가맹점 지급계좌 보유 현황 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(balance),0) INTO :h_wa1, :h_wa2 + FROM py_account WHERE merchant_id = :h_wmerch; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "가맹점 계좌 현황 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_account + WHERE merchant_id = :h_wmerch AND status <> 'ACTIVE'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 비정상 계좌 비중 높음: 기준=%ld 실적=%ld", "PAY_ACCTCHANGE", h_wa1, h_wa3); + } + /* 8) 응답부 */ + Bchg(b, T_KEY1, 0, h_key1, 0L); + Bchg(b, T_STATUS, 0, h_wast, 0L); + userlog("PAY_ACCTCHANGE 완료: acct=%s 실적=%ld건 %ld원", h_key1, h_wcnt, h_wsum); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_ACCTCHANGE", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); FAIL(b); } - EXEC SQL SELECT status INTO :h_ast FROM py_account - WHERE account_no = :h_acct AND merchant_id = :h_merch; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlcode == 100) { setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "가맹점 소유 계좌 아님", 0L); FAIL(b); } - if (strcmp(h_ast, "ACTIVE") != 0) { setl(b, T_RC, 3); Bchg(b, T_MSG, 0, "정상 상태 계좌 아님", 0L); FAIL(b); } - - EXEC SQL UPDATE py_payout SET account_no = :h_acct, updated_at = now() - WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_RC, 0); - Bchg(b, T_KEY1, 0, h_acct, 0L); - userlog("PAY_ACCTCHANGE payout=%ld new_acct=%s", h_pid, h_acct); OK(b); } diff --git a/app/src/py/svc/PAY_ACCTCHK.pgc b/app/src/py/svc/PAY_ACCTCHK.pgc index d88cab0..f2ae501 100644 --- a/app/src/py/svc/PAY_ACCTCHK.pgc +++ b/app/src/py/svc/PAY_ACCTCHK.pgc @@ -1,20 +1,136 @@ /* - * PAY_ACCTCHK.pgc - py 모듈 서비스 PAY_ACCTCHK (one service = one file). + * PAY_ACCTCHK.pgc - py (지급) 모듈 온라인 서비스 PAY_ACCTCHK. * 지급계좌 유효성 점검: dbio 로 계좌 상태/잔액을 읽어 ACTIVE 여부를 회신. - * Moved verbatim from the former inline py_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by py_svr at tpsvrinit. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [acctq]. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_ACCTCHK.h" void PAY_ACCTCHK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char acct[32], st[8]; - long bal = 0; - gets_(b, T_KEY1, acct, sizeof(acct)); - if (pydb_account_check(acct, st, &bal) < 0) { setl(b, T_RC, 1); FAIL(b); } - setl(b, T_AMT1, bal); - Bchg(b, T_STR1, 0, st, 0L); - setl(b, T_RC, (strcmp(st, "ACTIVE") == 0) ? 0 : 1); + EXEC SQL BEGIN DECLARE SECTION; + char h_key1[64]; + char h_wast[16]; + long h_wbal; + char h_wholder[64]; + char h_wmerch[64]; + char h_wbank[8]; + long h_wcnt; + long h_wsum; + long h_waff; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; + EXEC SQL END DECLARE SECTION; + + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + gets_(b, T_KEY1, h_key1, sizeof(h_key1)); + if (h_key1[0] == 0 || strlen(h_key1) < 3) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "계좌번호/키 값이 유효하지 않음", 0L); + FAIL(b); + } + /* 2) 마스터 조회부 - 계좌 상태/잔액/예금주/은행 */ + EXEC SQL SELECT status, balance, holder, merchant_id, bank_code + INTO :h_wast, :h_wbal, :h_wholder, :h_wmerch, :h_wbank + FROM py_account WHERE account_no = :h_key1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "조회 대상 지급계좌 없음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급계좌 조회 오류", 0L); + FAIL(b); + } + /* 해당 계좌 지급 실적 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wcnt, :h_wsum + FROM py_payout WHERE account_no = :h_key1; + if (sqlca.sqlcode < 0) { h_wcnt = 0; h_wsum = 0; } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + userlog("PAY_ACCTCHK 진입: acct=%s bank=%s status=%s", h_key1, h_wbank, h_wast); + /* 3) 업무규칙 분기부 - 계좌 사용 가능성 판정 (3분기) */ + if (strcmp(h_wast, "SEIZED") == 0 || strcmp(h_wast, "GARNISH") == 0) { + setl(b, T_RC, 6); Bchg(b, T_STATUS, 0, h_wast, 0L); + Bchg(b, T_MSG, 0, "압류/가압류 계좌 - 지급 불가", 0L); FAIL(b); + } + if (strcmp(h_wast, "ACTIVE") != 0) { + setl(b, T_RC, 1); Bchg(b, T_STATUS, 0, h_wast, 0L); + Bchg(b, T_MSG, 0, "정상(ACTIVE) 상태 계좌가 아님", 0L); + } else { + setl(b, T_RC, 0); + Bchg(b, T_MSG, 0, "정상 지급 가능 계좌", 0L); + } + strcpy(h_pval, "1000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_acctchk_min_bal'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 1000; + if (h_wbal < h_thr) { + userlog("PAY_ACCTCHK 잔액 부족 경고: acct=%s bal=%ld thr=%ld", h_key1, h_wbal, h_thr); + } + /* 4) 본처리부 - 조회 서비스이므로 갱신 없이 검증 결과만 확정 */ + h_waff = h_wcnt; + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s acct=%s bal=%ld 지급이력=%ld건", "PAY_ACCTCHK", h_key1, h_wbal, h_wcnt); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_ACCTCHK', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 가맹점 지급계좌 보유 현황 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(balance),0) INTO :h_wa1, :h_wa2 + FROM py_account WHERE merchant_id = :h_wmerch; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "가맹점 계좌 현황 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_account + WHERE merchant_id = :h_wmerch AND status <> 'ACTIVE'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 비정상 계좌 비중 높음: 기준=%ld 실적=%ld", "PAY_ACCTCHK", h_wa1, h_wa3); + } + /* 8) 응답부 */ + setl(b, T_AMT1, h_wbal); + Bchg(b, T_STR1, 0, h_wast, 0L); + Bchg(b, T_STATUS, 0, h_wast, 0L); + userlog("PAY_ACCTCHK 완료: acct=%s 실적=%ld건 %ld원", h_key1, h_wcnt, h_wsum); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_ACCTCHK", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_ACCTFREEZE.pgc b/app/src/py/svc/PAY_ACCTFREEZE.pgc index 0e7b646..a92d162 100644 --- a/app/src/py/svc/PAY_ACCTFREEZE.pgc +++ b/app/src/py/svc/PAY_ACCTFREEZE.pgc @@ -1,7 +1,9 @@ /* - * PAY_ACCTFREEZE.pgc - py 모듈 서비스 PAY_ACCTFREEZE (one service = one file). + * PAY_ACCTFREEZE.pgc - py (지급) 모듈 온라인 서비스 PAY_ACCTFREEZE. * 지급계좌 동결: 활성(ACTIVE) 계좌만 FROZEN 으로 전환한다. 먼저 해당 계좌의 * 미지급(R/A) 건수를 조회해 함께 회신하고, 동결 대상이 아니면 거절한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [acct]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_ACCTFREEZE.h" @@ -10,25 +12,153 @@ void PAY_ACCTFREEZE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - char h_acct[32]; - long h_pend; + char h_key1[64]; + char h_wast[16]; + long h_wbal; + char h_wholder[64]; + char h_wmerch[64]; + long h_waff; + long h_wcas; + long h_wsum; + long h_wcnt; + char h_wacct[64]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, h_acct, sizeof(h_acct)); - EXEC SQL SELECT count(*) INTO :h_pend - FROM py_payout WHERE account_no = :h_acct AND status IN ('R','A'); - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL UPDATE py_account SET status = 'FROZEN', updated_at = now() - WHERE account_no = :h_acct AND status = 'ACTIVE'; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlerrd[2] == 0) { - setl(b, T_RC, 1); - Bchg(b, T_MSG, 0, "동결 대상 아님(활성계좌 아님)", 0L); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + gets_(b, T_KEY1, h_key1, sizeof(h_key1)); + if (h_key1[0] == 0 || strlen(h_key1) < 3) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "계좌번호/키 값이 유효하지 않음", 0L); FAIL(b); } + /* 2) 마스터 조회부 - 대상 지급계좌 확정 후 상태/잔액/예금주 조회 */ + strncpy(h_wacct, h_key1, sizeof(h_wacct)-1); + h_wacct[sizeof(h_wacct)-1] = 0; + EXEC SQL SELECT status, balance, holder, merchant_id + INTO :h_wast, :h_wbal, :h_wholder, :h_wmerch + FROM py_account WHERE account_no = :h_wacct; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 지급계좌가 등록되어 있지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급계좌 조회 중 DB 오류", 0L); + FAIL(b); + } + /* 해당 계좌의 미지급 잔액 규모 파악 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wcnt, :h_wsum + FROM py_payout WHERE account_no = :h_wacct AND status IN ('R','A','H'); + if (sqlca.sqlcode < 0) { h_wcnt = 0; h_wsum = 0; } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + userlog("PAY_ACCTFREEZE 진입: acct=%s status=%s bal=%ld 미지급=%ld건", + h_wacct, h_wast, h_wbal, h_wcnt); + /* 3) 업무규칙 분기부 - 계좌 상태 전이 적법성 (3분기) */ + if (strcmp(h_wast, "FROZEN") == 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "이미 계좌 동결 상태인 계좌", 0L); + FAIL(b); + } + if (strcmp(h_wast, "ACTIVE") != 0) { + setl(b, T_RC, 43); Bchg(b, T_STATUS, 0, h_wast, 0L); + Bchg(b, T_MSG, 0, "현재 계좌 상태에서는 계좌 동결 처리 불가", 0L); FAIL(b); + } + if (h_wbal < 0) { + setl(b, T_RC, 44); + Bchg(b, T_MSG, 0, "잔액이 음수인 비정상 계좌 - 선행 정산 필요", 0L); + FAIL(b); + } + strcpy(h_pval, "1"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_acctfreeze_cascade'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 1; + /* 4) 본처리부 - 계좌 상태 전이 후 미지급건 연쇄 처리 */ + EXEC SQL UPDATE py_account SET status = 'FROZEN', updated_at = now() + WHERE account_no = :h_wacct AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "계좌 동결 처리 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "계좌 상태 동시성 충돌", 0L); + FAIL(b); + } + EXEC SQL UPDATE py_payout SET status = 'H', reject_reason = '계좌동결', + updated_at = now() + WHERE account_no = :h_wacct AND status IN ('R','A') AND :h_thr > 0; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 51); + Bchg(b, T_MSG, 0, "미지급건 연쇄 처리 실패", 0L); + FAIL(b); + } + h_wcas = sqlca.sqlerrd[2]; + strcpy(h_lvl, "W"); + snprintf(h_amsg, sizeof(h_amsg), "%s acct=%s %s->%s 연쇄=%ld 금액=%ld", "PAY_ACCTFREEZE", h_wacct, "ACTIVE", "FROZEN", h_wcas, h_wsum); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_ACCTFREEZE', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 계좌 상태 전이 후 잔여 미지급 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE account_no = :h_wacct AND status = 'H'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "계좌 잔여 미지급 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE account_no = :h_wacct AND status IN ('R','A'); + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 연쇄 처리 후 잔여 존재: 기준=%ld 실적=%ld", "PAY_ACCTFREEZE", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - setl(b, T_COUNT, h_pend); Bchg(b, T_STATUS, 0, "FROZEN", 0L); - userlog("PAY_ACCTFREEZE acct=%s pending=%ld", h_acct, h_pend); + setl(b, T_COUNT, h_wcas); + Bchg(b, T_MSG, 0, "계좌 동결 및 미지급건 연쇄처리 완료", 0L); + userlog("PAY_ACCTFREEZE 완료: acct=%s 연쇄처리=%ld건 보류금액=%ld", + h_wacct, h_wcas, h_wsum); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_ACCTFREEZE", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_ACCTLIST.pgc b/app/src/py/svc/PAY_ACCTLIST.pgc index eb058d4..e1ae099 100644 --- a/app/src/py/svc/PAY_ACCTLIST.pgc +++ b/app/src/py/svc/PAY_ACCTLIST.pgc @@ -1,7 +1,9 @@ /* - * PAY_ACCTLIST.pgc - py 모듈 서비스 PAY_ACCTLIST (one service = one file). + * PAY_ACCTLIST.pgc - py (지급) 모듈 온라인 서비스 PAY_ACCTLIST. * 가맹점 지급계좌 현황: 가맹점의 모든 지급계좌를 커서로 훑어 계좌/은행/ * 상태/잔액을 occurrence 로 회신하고 정상 계좌수와 잔액 합계를 산출한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [cursor]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_ACCTLIST.h" @@ -9,38 +11,128 @@ void PAY_ACCTLIST(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - int i = 0; - long active = 0, bal_total = 0; + int gi = 0; + long tot_cnt = 0, tot_sum = 0, tot_fee = 0, top_cnt = 0, top_sum = 0; + char top_grp[64]; EXEC SQL BEGIN DECLARE SECTION; - char h_merch[64], h_acct[32], h_bank[8], h_ast[16]; - long h_bal; + char h_merchant[64]; + char h_wgrp[64]; + long h_wgcnt; + long h_wgsum; + long h_wgfee; + long h_wtot; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); - EXEC SQL DECLARE cur_acctlist CURSOR FOR - SELECT account_no, bank_code, status, balance - FROM py_account WHERE merchant_id = :h_merch - ORDER BY account_no; - EXEC SQL OPEN cur_acctlist; - if (sqlca.sqlcode < 0) FAIL(b); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + gets_(b, T_MERCHANT, h_merchant, sizeof(h_merchant)); + if (h_merchant[0] == 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "가맹점 ID는 필수 입력", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + top_grp[0] = 0; + /* 2) 사전 규모 확인 - 커서 오픈 전 총건수 파악 */ + EXEC SQL SELECT count(*) INTO :h_wtot FROM py_account a WHERE a.merchant_id = :h_merchant; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "집계 사전 조회 실패", 0L); + FAIL(b); + } + userlog("PAY_ACCTLIST 집계 시작: 기준일=%s 대상=%ld건 (계좌별)", h_wdate, h_wtot); + /* 커서 정의: 계좌별 건수/금액 집계 */ + EXEC SQL DECLARE cur_pay_acctlist CURSOR FOR + SELECT a.account_no, 1, coalesce(a.balance,0), 0 + FROM py_account a WHERE a.merchant_id = :h_merchant + ORDER BY a.account_no; + EXEC SQL OPEN cur_pay_acctlist; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "집계 커서 OPEN 실패", 0L); + FAIL(b); + } + /* 3) 본처리부 - 커서 순회하며 그룹별 집계/상위 추출 */ for (;;) { - EXEC SQL FETCH cur_acctlist INTO :h_acct, :h_bank, :h_ast, :h_bal; + EXEC SQL FETCH cur_pay_acctlist INTO :h_wgrp, :h_wgcnt, :h_wgsum, :h_wgfee; if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_acctlist; FAIL(b); } - if (strcmp(h_ast, "ACTIVE") == 0) active++; - bal_total += h_bal; - if (i < 10) { - Bchg(b, T_KEY1, i, h_acct, 0L); - Bchg(b, T_STR1, i, h_bank, 0L); - Bchg(b, T_STR2, i, h_ast, 0L); - Bchg(b, T_AMT1, i, (char *)&h_bal, 0L); - i++; + if (sqlca.sqlcode < 0) { + EXEC SQL CLOSE cur_pay_acctlist; + setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "집계 FETCH 오류", 0L); FAIL(b); + } + tot_cnt += h_wgcnt; + tot_sum += h_wgsum; + tot_fee += h_wgfee; + if (h_wgcnt > top_cnt) { + top_cnt = h_wgcnt; + top_sum = h_wgsum; + strncpy(top_grp, h_wgrp, sizeof(top_grp)-1); + top_grp[sizeof(top_grp)-1] = 0; + } + if (gi < 10) { + Bchg(b, T_STR1, gi, h_wgrp, 0L); + Bchg(b, T_AMT1, gi, (char *)&h_wgsum, 0L); + Bchg(b, T_ARG1, gi, (char *)&h_wgcnt, 0L); + gi++; } } - EXEC SQL CLOSE cur_acctlist; - setl(b, T_COUNT, (long)i); - setl(b, T_ARG1, active); - setl(b, T_GROSS, bal_total); - setl(b, T_RC, i > 0 ? 0 : 1); + EXEC SQL CLOSE cur_pay_acctlist; + strcpy(h_pval, "5000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_acctlist_concentration'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 5000; + /* 업무규칙 분기부 - 분포 집중도 판정 (3분기) */ + if (gi == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "계좌별 집계 대상 없음", 0L); + } else if (tot_cnt > 0 && top_cnt * 10000 / tot_cnt > h_thr) { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "특정 계좌 집중 - 확인 필요", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "계좌별 정상 분포", 0L); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s %s별 그룹=%d 건수=%ld 금액=%ld 최다=%s", "PAY_ACCTLIST", "계좌", gi, tot_cnt, tot_sum, top_grp); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_ACCTLIST', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 8) 응답부 */ + setl(b, T_COUNT, tot_cnt); + setl(b, T_GROSS, tot_sum); + Bchg(b, T_STR2, 0, top_grp, 0L); + userlog("PAY_ACCTLIST 완료: 그룹=%d 총건=%ld 총액=%ld 최다=%s", + gi, tot_cnt, tot_sum, top_grp); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_ACCTLIST", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_ACCTREOPEN.pgc b/app/src/py/svc/PAY_ACCTREOPEN.pgc index 66bc0df..50cf39e 100644 --- a/app/src/py/svc/PAY_ACCTREOPEN.pgc +++ b/app/src/py/svc/PAY_ACCTREOPEN.pgc @@ -1,7 +1,9 @@ /* - * PAY_ACCTREOPEN.pgc - py 모듈 서비스 PAY_ACCTREOPEN (one service = one file). + * PAY_ACCTREOPEN.pgc - py (지급) 모듈 온라인 서비스 PAY_ACCTREOPEN. * 동결 해제: 동결(FROZEN) 계좌를 ACTIVE 로 복구하고, 동결 사유로 보류(H)됐던 * 그 계좌의 지급건을 승인(A)으로 되돌린다. 동결 계좌가 아니면 거절한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [acct]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_ACCTREOPEN.h" @@ -10,27 +12,153 @@ void PAY_ACCTREOPEN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - char h_acct[32]; - long h_rel; + char h_key1[64]; + char h_wast[16]; + long h_wbal; + char h_wholder[64]; + char h_wmerch[64]; + long h_waff; + long h_wcas; + long h_wsum; + long h_wcnt; + char h_wacct[64]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, h_acct, sizeof(h_acct)); - EXEC SQL UPDATE py_account SET status = 'ACTIVE', updated_at = now() - WHERE account_no = :h_acct AND status = 'FROZEN'; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlerrd[2] == 0) { - setl(b, T_RC, 1); - Bchg(b, T_MSG, 0, "동결 계좌 아님", 0L); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + gets_(b, T_KEY1, h_key1, sizeof(h_key1)); + if (h_key1[0] == 0 || strlen(h_key1) < 3) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "계좌번호/키 값이 유효하지 않음", 0L); FAIL(b); } - EXEC SQL UPDATE py_payout SET status = 'A', reject_reason = '', updated_at = now() - WHERE account_no = :h_acct AND status = 'H' AND reject_reason LIKE '동결%'; - if (sqlca.sqlcode < 0) FAIL(b); - h_rel = sqlca.sqlerrd[2]; - + /* 2) 마스터 조회부 - 대상 지급계좌 확정 후 상태/잔액/예금주 조회 */ + strncpy(h_wacct, h_key1, sizeof(h_wacct)-1); + h_wacct[sizeof(h_wacct)-1] = 0; + EXEC SQL SELECT status, balance, holder, merchant_id + INTO :h_wast, :h_wbal, :h_wholder, :h_wmerch + FROM py_account WHERE account_no = :h_wacct; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 지급계좌가 등록되어 있지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급계좌 조회 중 DB 오류", 0L); + FAIL(b); + } + /* 해당 계좌의 미지급 잔액 규모 파악 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wcnt, :h_wsum + FROM py_payout WHERE account_no = :h_wacct AND status IN ('R','A','H'); + if (sqlca.sqlcode < 0) { h_wcnt = 0; h_wsum = 0; } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + userlog("PAY_ACCTREOPEN 진입: acct=%s status=%s bal=%ld 미지급=%ld건", + h_wacct, h_wast, h_wbal, h_wcnt); + /* 3) 업무규칙 분기부 - 계좌 상태 전이 적법성 (3분기) */ + if (strcmp(h_wast, "ACTIVE") == 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "이미 동결 해제 상태인 계좌", 0L); + FAIL(b); + } + if (strcmp(h_wast, "FROZEN") != 0) { + setl(b, T_RC, 43); Bchg(b, T_STATUS, 0, h_wast, 0L); + Bchg(b, T_MSG, 0, "현재 계좌 상태에서는 동결 해제 처리 불가", 0L); FAIL(b); + } + if (h_wbal < 0) { + setl(b, T_RC, 44); + Bchg(b, T_MSG, 0, "잔액이 음수인 비정상 계좌 - 선행 정산 필요", 0L); + FAIL(b); + } + strcpy(h_pval, "1"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_acctreopen_cascade'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 1; + /* 4) 본처리부 - 계좌 상태 전이 후 미지급건 연쇄 처리 */ + EXEC SQL UPDATE py_account SET status = 'ACTIVE', updated_at = now() + WHERE account_no = :h_wacct AND status = 'FROZEN'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "동결 해제 처리 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "계좌 상태 동시성 충돌", 0L); + FAIL(b); + } + EXEC SQL UPDATE py_payout SET status = 'A', reject_reason = '', + updated_at = now() + WHERE account_no = :h_wacct AND status IN ('H') AND :h_thr > 0; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 51); + Bchg(b, T_MSG, 0, "미지급건 연쇄 처리 실패", 0L); + FAIL(b); + } + h_wcas = sqlca.sqlerrd[2]; + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s acct=%s %s->%s 연쇄=%ld 금액=%ld", "PAY_ACCTREOPEN", h_wacct, "FROZEN", "ACTIVE", h_wcas, h_wsum); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_ACCTREOPEN', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 계좌 상태 전이 후 잔여 미지급 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE account_no = :h_wacct AND status = 'H'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "계좌 잔여 미지급 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE account_no = :h_wacct AND status IN ('R','A'); + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 연쇄 처리 후 잔여 존재: 기준=%ld 실적=%ld", "PAY_ACCTREOPEN", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - setl(b, T_COUNT, h_rel); Bchg(b, T_STATUS, 0, "ACTIVE", 0L); - userlog("PAY_ACCTREOPEN acct=%s released=%ld", h_acct, h_rel); + setl(b, T_COUNT, h_wcas); + Bchg(b, T_MSG, 0, "동결 해제 및 미지급건 연쇄처리 완료", 0L); + userlog("PAY_ACCTREOPEN 완료: acct=%s 연쇄처리=%ld건 보류금액=%ld", + h_wacct, h_wcas, h_wsum); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_ACCTREOPEN", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_ACCTVERIFY.pgc b/app/src/py/svc/PAY_ACCTVERIFY.pgc index b2ce53e..031f1bf 100644 --- a/app/src/py/svc/PAY_ACCTVERIFY.pgc +++ b/app/src/py/svc/PAY_ACCTVERIFY.pgc @@ -1,7 +1,9 @@ /* - * PAY_ACCTVERIFY.pgc - py 모듈 서비스 PAY_ACCTVERIFY (one service = one file). + * PAY_ACCTVERIFY.pgc - py (지급) 모듈 온라인 서비스 PAY_ACCTVERIFY. * 지급계좌 검증: 계좌의 상태/예금주/잔액을 조회하고, 요청에 예금주가 주어지면 * 일치 여부까지 확인한다. 미등록/불일치는 사유코드로 구분해 회신한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [acctq]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_ACCTVERIFY.h" @@ -9,26 +11,139 @@ void PAY_ACCTVERIFY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char inh[64]; EXEC SQL BEGIN DECLARE SECTION; - char h_acct[32], h_st[16], h_holder[64]; - long h_bal; + char h_key1[64]; + char h_str1[64]; + char h_wast[16]; + long h_wbal; + char h_wholder[64]; + char h_wmerch[64]; + char h_wbank[8]; + long h_wcnt; + long h_wsum; + long h_waff; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, h_acct, sizeof(h_acct)); - gets_(b, T_STR1, inh, sizeof(inh)); - EXEC SQL SELECT status, holder, balance INTO :h_st, :h_holder, :h_bal - FROM py_account WHERE account_no = :h_acct; - if (sqlca.sqlcode == 100) { setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "미등록 계좌", 0L); FAIL(b); } - if (sqlca.sqlcode < 0) FAIL(b); - if (inh[0] != 0 && strcmp(inh, h_holder) != 0) { - setl(b, T_RC, 1); - Bchg(b, T_MSG, 0, "예금주 불일치", 0L); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + gets_(b, T_KEY1, h_key1, sizeof(h_key1)); + if (h_key1[0] == 0 || strlen(h_key1) < 3) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "계좌번호/키 값이 유효하지 않음", 0L); + FAIL(b); + } + gets_(b, T_STR1, h_str1, sizeof(h_str1)); + if (h_str1[0] == 0) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "필수 문자열 항목 누락", 0L); + FAIL(b); + } + /* 2) 마스터 조회부 - 계좌 상태/잔액/예금주/은행 */ + EXEC SQL SELECT status, balance, holder, merchant_id, bank_code + INTO :h_wast, :h_wbal, :h_wholder, :h_wmerch, :h_wbank + FROM py_account WHERE account_no = :h_key1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "조회 대상 지급계좌 없음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급계좌 조회 오류", 0L); + FAIL(b); + } + /* 해당 계좌 지급 실적 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wcnt, :h_wsum + FROM py_payout WHERE account_no = :h_key1; + if (sqlca.sqlcode < 0) { h_wcnt = 0; h_wsum = 0; } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + userlog("PAY_ACCTVERIFY 진입: acct=%s bank=%s status=%s", h_key1, h_wbank, h_wast); + /* 3) 업무규칙 분기부 - 계좌 사용 가능성 판정 (3분기) */ + if (h_str1[0] != 0 && strcmp(h_str1, h_wholder) != 0) { + setl(b, T_RC, 45); + Bchg(b, T_MSG, 0, "요청 예금주와 등록 예금주가 불일치", 0L); + FAIL(b); + } + if (strcmp(h_wast, "SEIZED") == 0 || strcmp(h_wast, "GARNISH") == 0) { + setl(b, T_RC, 6); Bchg(b, T_STATUS, 0, h_wast, 0L); + Bchg(b, T_MSG, 0, "압류/가압류 계좌 - 지급 불가", 0L); FAIL(b); + } + if (strcmp(h_wast, "ACTIVE") != 0) { + setl(b, T_RC, 1); Bchg(b, T_STATUS, 0, h_wast, 0L); + Bchg(b, T_MSG, 0, "정상(ACTIVE) 상태 계좌가 아님", 0L); + } else { + setl(b, T_RC, 0); + Bchg(b, T_MSG, 0, "정상 지급 가능 계좌", 0L); + } + strcpy(h_pval, "1000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_acctverify_min_bal'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 1000; + if (h_wbal < h_thr) { + userlog("PAY_ACCTVERIFY 잔액 부족 경고: acct=%s bal=%ld thr=%ld", h_key1, h_wbal, h_thr); + } + /* 4) 본처리부 - 조회 서비스이므로 갱신 없이 검증 결과만 확정 */ + h_waff = h_wcnt; + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s acct=%s bal=%ld 지급이력=%ld건", "PAY_ACCTVERIFY", h_key1, h_wbal, h_wcnt); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_ACCTVERIFY', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 가맹점 지급계좌 보유 현황 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(balance),0) INTO :h_wa1, :h_wa2 + FROM py_account WHERE merchant_id = :h_wmerch; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "가맹점 계좌 현황 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_account + WHERE merchant_id = :h_wmerch AND status <> 'ACTIVE'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 비정상 계좌 비중 높음: 기준=%ld 실적=%ld", "PAY_ACCTVERIFY", h_wa1, h_wa3); + } + /* 8) 응답부 */ + setl(b, T_AMT1, h_wbal); + Bchg(b, T_STR2, 0, h_wholder, 0L); + Bchg(b, T_STATUS, 0, h_wast, 0L); + userlog("PAY_ACCTVERIFY 완료: acct=%s 실적=%ld건 %ld원", h_key1, h_wcnt, h_wsum); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_ACCTVERIFY", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); FAIL(b); } - setl(b, T_RC, 0); - setl(b, T_AMT1, h_bal); - Bchg(b, T_STATUS, 0, h_st, 0L); - Bchg(b, T_STR2, 0, h_holder, 0L); OK(b); } diff --git a/app/src/py/svc/PAY_ADJUST.pgc b/app/src/py/svc/PAY_ADJUST.pgc index ce633d1..19792b6 100644 --- a/app/src/py/svc/PAY_ADJUST.pgc +++ b/app/src/py/svc/PAY_ADJUST.pgc @@ -1,29 +1,185 @@ /* - * PAY_ADJUST.pgc - py 모듈 서비스 PAY_ADJUST (one service = one file). + * PAY_ADJUST.pgc - py (지급) 모듈 온라인 서비스 PAY_ADJUST. * 지급 조정: py_adjust 에 증감(delta)을 기록하고 지급건 net 에 즉시 반영. - * Moved verbatim from the former inline py_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by py_svr at tpsvrinit. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [fee]. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_ADJUST.h" void PAY_ADJUST(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], reason[128]; - long payout_id, delta, adj; EXEC SQL BEGIN DECLARE SECTION; - long e_pay, e_delta; + char h_bizdate[64]; + long h_deltain; + long h_id1; + char h_reason[64]; + char h_wst[8]; + long h_wamt; + long h_wfee; + long h_wnet; + char h_wmerch[64]; + char h_wacct[64]; + long h_wmdr; + long h_wnew; + long h_wdelta; + long h_waff; + long h_wadj; + char h_wdt[16]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - payout_id = getl(b, T_ID1); - delta = getl(b, T_DELTA); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "ADJ"); - adj = pydb_insert_adjust(payout_id, delta, reason, bizdate); - if (adj < 0) FAIL(b); - e_pay = payout_id; e_delta = delta; - EXEC SQL UPDATE py_payout SET net = net + :e_delta, updated_at = now() - WHERE payout_id = :e_pay; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - setl(b, T_ADJ, adj); + + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + h_deltain = getl(b, T_DELTA); + if (h_deltain == 0) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "조정 증감액이 0 이면 처리 불가", 0L); + FAIL(b); + } + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 13); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + gets_(b, T_REASON, h_reason, sizeof(h_reason)); + if (h_reason[0] == 0) strncpy(h_reason, "사유미기재", sizeof(h_reason)-1); + /* 2) 마스터 조회부 - 지급건 + 가맹점 계약요율 (LEFT JOIN) */ + EXEC SQL SELECT p.status, p.amount, p.fee, p.net, p.merchant_id, + p.account_no, p.biz_date, coalesce(m.mdr_bps, 25) + INTO :h_wst, :h_wamt, :h_wfee, :h_wnet, :h_wmerch, + :h_wacct, :h_wdt, :h_wmdr + FROM py_payout p + LEFT JOIN merchant m ON m.merchant_id = p.merchant_id + WHERE p.payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 지급건 없음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급건/가맹점 조인 조회 오류", 0L); + FAIL(b); + } + /* 기 적재된 조정 합계 확인 */ + h_wadj = pydb_adjust_delta_sum(h_id1); + if (h_wadj < 0) h_wadj = 0; + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + userlog("PAY_ADJUST 진입: payout=%ld status=%s fee=%ld net=%ld 조정누계=%ld", + h_id1, h_wst, h_wfee, h_wnet, h_wadj); + /* 3) 업무규칙 분기부 - 수수료/순액 재산정 (3분기) */ + if (strcmp(h_wst, "P") == 0 || strcmp(h_wst, "X") == 0 || + strcmp(h_wst, "C") == 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "종결된 지급건은 수수료/순액 조정 불가", 0L); + FAIL(b); + } + if (h_deltain > 0) { + h_wdelta = h_deltain; + } else if (h_wnet + h_deltain < 0) { + setl(b, T_RC, 13); + Bchg(b, T_MSG, 0, "조정 후 순액이 음수가 됨", 0L); + FAIL(b); + } else { + h_wdelta = h_deltain; + } + h_wnew = h_wfee; + strcpy(h_pval, "1000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_adjust_bps_cap'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 1000; + if (h_wnew > acq_fee(h_wamt, h_thr)) { + userlog("PAY_ADJUST 수수료 상한정책 초과: fee=%ld cap_bps=%ld", h_wnew, h_thr); + } + /* 4) 본처리부 - fee/net 재계산 및 갱신 */ + h_wnet = h_wnet + h_wdelta; + if (h_wnet < 0) h_wnet = 0; + EXEC SQL UPDATE py_payout SET net = :h_wnet, updated_at = now() + WHERE payout_id = :h_id1 AND status IN ('R','A','H'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "수수료/순액 갱신 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "갱신 대상 없음 - 처리 도중 상태 변동", 0L); + FAIL(b); + } + /* 5) 이력 적재부 - 조정 원장(py_adjust) 기록 */ + if (pydb_insert_adjust(h_id1, h_wdelta, h_reason, h_wdt) < 0) { + setl(b, T_RC, 55); + Bchg(b, T_MSG, 0, "조정 원장(py_adjust) 기록 실패", 0L); + FAIL(b); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s payout=%ld fee %ld->%ld net=%ld delta=%ld", "PAY_ADJUST", h_id1, h_wfee, h_wnew, h_wnet, h_wdelta); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_ADJUST', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 가맹점 당일 수수료/조정 누계 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(fee),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_wmerch AND biz_date = :h_wdt; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "수수료/조정 누계 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_adjust + WHERE payout_id = :h_id1; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 조정 이력 과다: 기준=%ld 실적=%ld", "PAY_ADJUST", h_wa1, h_wa3); + } + /* 8) 응답부 */ + setl(b, T_RC, 0); + setl(b, T_ADJ, h_wdelta); + Bchg(b, T_MSG, 0, "수수료/순액 조정 완료", 0L); + userlog("PAY_ADJUST 완료: payout=%ld fee=%ld net=%ld", h_id1, h_wnew, h_wnet); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_ADJUST", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_ADVANCE.pgc b/app/src/py/svc/PAY_ADVANCE.pgc index 6e308b9..d97d797 100644 --- a/app/src/py/svc/PAY_ADVANCE.pgc +++ b/app/src/py/svc/PAY_ADVANCE.pgc @@ -1,27 +1,219 @@ /* - * PAY_ADVANCE.pgc - py 모듈 서비스 PAY_ADVANCE (one service = one file). + * PAY_ADVANCE.pgc - py (지급) 모듈 온라인 서비스 PAY_ADVANCE. * 선지급(advance): 선지급 수수료(1%)를 공제한 지급건(ADVANCE 채널)을 생성. - * Moved verbatim from the former inline py_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by py_svr at tpsvrinit. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [mkpay]. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_ADVANCE.h" void PAY_ADVANCE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], acct[32], bizdate[16]; - long amount, fee, net, payout_id; - gets_(b, T_MERCHANT, merch, sizeof(merch)); - gets_(b, T_KEY1, acct, sizeof(acct)); - amount = getl(b, T_AMOUNT); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - fee = acq_fee(amount, 100); /* 선지급 수수료 1% */ - net = amount - fee; - payout_id = pydb_next_payout_id(); - if (payout_id < 0) FAIL(b); - if (pydb_insert_payout(payout_id, merch, acct, amount, fee, net, "A", "ADVANCE", bizdate) < 0) FAIL(b); - setl(b, T_ID1, payout_id); - setl(b, T_FEE, fee); - setl(b, T_NET, net); + int gi; + EXEC SQL BEGIN DECLARE SECTION; + long h_amount; + char h_bizdate[64]; + char h_key1[64]; + char h_merchant[64]; + long h_wmdr; + char h_wmst[16]; + long h_wlim; + char h_wast[16]; + long h_wbal; + long h_wfee; + long h_wnet; + long h_wpart; + long h_wnum; + long h_wid; + long h_wlast; + long h_wused; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; + EXEC SQL END DECLARE SECTION; + + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_amount = getl(b, T_AMOUNT); + if (h_amount < 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "금액 항목은 음수가 될 수 없음", 0L); + FAIL(b); + } + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + gets_(b, T_KEY1, h_key1, sizeof(h_key1)); + if (h_key1[0] == 0 || strlen(h_key1) < 3) { + setl(b, T_RC, 13); + Bchg(b, T_MSG, 0, "계좌번호/키 값이 유효하지 않음", 0L); + FAIL(b); + } + gets_(b, T_MERCHANT, h_merchant, sizeof(h_merchant)); + if (h_merchant[0] == 0) { + setl(b, T_RC, 14); + Bchg(b, T_MSG, 0, "가맹점 ID는 필수 입력", 0L); + FAIL(b); + } + /* 2) 마스터 조회부 - 가맹점 계약요율/상태/일한도 */ + EXEC SQL SELECT mdr_bps, status, daily_limit INTO :h_wmdr, :h_wmst, :h_wlim + FROM merchant WHERE merchant_id = :h_merchant; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "등록되지 않은 가맹점", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); + FAIL(b); + } + if (strcmp(h_wmst, "ACTIVE") != 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "비활성/폐업 가맹점은 지급 생성 불가", 0L); + FAIL(b); + } + /* 지급계좌 상태/잔액 확인 */ + EXEC SQL SELECT status, balance INTO :h_wast, :h_wbal + FROM py_account WHERE account_no = :h_key1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 43); + Bchg(b, T_MSG, 0, "지급계좌 미등록", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 44); + Bchg(b, T_MSG, 0, "지급계좌 조회 오류", 0L); + FAIL(b); + } + if (strcmp(h_wast, "ACTIVE") != 0) { + setl(b, T_RC, 45); + Bchg(b, T_MSG, 0, "정상 상태 계좌가 아니어서 지급 불가", 0L); + FAIL(b); + } + /* 당일 기 사용액 조회 - 일한도 통제용 */ + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_wused FROM py_payout + WHERE merchant_id = :h_merchant AND biz_date = :h_bizdate + AND status IN ('A','F','P'); + if (sqlca.sqlcode < 0) h_wused = 0; + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + userlog("PAY_ADVANCE 진입: merch=%s amt=%ld mdr=%ldbps 기사용=%ld", + h_merchant, h_amount, h_wmdr, h_wused); + /* 3) 업무규칙 분기부 - 금액 구간별 수수료 산정 (3분기) */ + if (h_amount < 100000) { + h_wfee = acq_fee(h_amount, 100); + } else if (h_amount < 10000000) { + h_wfee = acq_fee(h_amount, 100) + acq_vat(acq_fee(h_amount, 100)); + } else { + h_wfee = acq_fee(h_amount, 100 + 20) + acq_vat(acq_fee(h_amount, 100)); + } + if (h_wfee < 0) h_wfee = 0; + h_wnet = h_amount - h_wfee; + if (h_wnet < 0) h_wnet = 0; + /* 일한도 통제 */ + if (h_wlim > 0 && h_wused + h_wnet > h_wlim) { + setl(b, T_RC, 8); + Bchg(b, T_MSG, 0, "가맹점 일 지급한도 초과", 0L); + FAIL(b); + } + strcpy(h_pval, "500000000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_advance_single_max'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 500000000; + if (h_amount > h_thr) { + setl(b, T_RC, 9); + Bchg(b, T_MSG, 0, "1건 지급 상한 정책 초과", 0L); + FAIL(b); + } + h_wnum = 1; + h_wpart = h_amount; + /* 4) 본처리부 - 지급건 생성 (분할 시 반복 채번) */ + h_wlast = 0; + for (gi = 0; gi < (int)h_wnum; gi++) { + EXEC SQL SELECT nextval('py_payout_seq') INTO :h_wid; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); Bchg(b, T_MSG, 0, "지급 일련번호 채번 실패", 0L); FAIL(b); + } + EXEC SQL INSERT INTO py_payout + (payout_id, merchant_id, account_no, amount, fee, net, + status, channel, biz_date) + VALUES (:h_wid, :h_merchant, :h_key1, :h_wpart, :h_wfee, + :h_wpart, 'R', 'ADVANCE', :h_bizdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 51); Bchg(b, T_MSG, 0, "지급건 생성 INSERT 실패", 0L); FAIL(b); + } + h_wlast = h_wid; + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s merch=%s 생성=%ld건 총액=%ld 수수료=%ld", "PAY_ADVANCE", h_merchant, h_wnum, h_amount, h_wfee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_ADVANCE', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계 갱신부 - 가맹점 일별 지급 집계 upsert */ + if (pydb_upsert_summary(h_merchant, h_bizdate, h_wnum, h_amount) < 0) { + setl(b, T_RC, 60); + Bchg(b, T_MSG, 0, "가맹점 지급집계(py_summary) 갱신 실패", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 당일 채널별 지급 생성 누계 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_merchant + AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "당일 지급 생성 누계 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE merchant_id = :h_merchant AND biz_date = :h_bizdate + AND status = 'R'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 요청 잔량 누적: 기준=%ld 실적=%ld", "PAY_ADVANCE", h_wa1, h_wa3); + } + /* 8) 응답부 */ + setl(b, T_RC, 0); + setl(b, T_ID1, h_wlast); + setl(b, T_FEE, h_wfee); + setl(b, T_NET, h_wnet); + Bchg(b, T_MSG, 0, "지급건 생성 완료", 0L); + userlog("PAY_ADVANCE 완료: last=%ld 건수=%ld 순액=%ld", h_wlast, h_wnum, h_wnet); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_ADVANCE", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_ADVCALC.pgc b/app/src/py/svc/PAY_ADVCALC.pgc index c0fb89d..adc677c 100644 --- a/app/src/py/svc/PAY_ADVCALC.pgc +++ b/app/src/py/svc/PAY_ADVCALC.pgc @@ -1,7 +1,9 @@ /* - * PAY_ADVCALC.pgc - py 모듈 서비스 PAY_ADVCALC (one service = one file). + * PAY_ADVCALC.pgc - py (지급) 모듈 온라인 서비스 PAY_ADVCALC. * 선지급 가능액 산정: 가맹점 미지급(R/A) 잔액에 지급률(bps)을 곱한 한도에서 * 이미 나간 선지급(ADVANCE, 미상환) 잔액을 차감해 추가 가능액을 회신한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [calc]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_ADVCALC.h" @@ -9,31 +11,126 @@ void PAY_ADVCALC(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long bps, avail; EXEC SQL BEGIN DECLARE SECTION; - char h_merch[64]; - long h_pend, h_out; + long h_arg1; + char h_merchant[64]; + long h_wcnt; + long h_wsum; + long h_wnet; + long h_wlim; + long h_wused; + long h_waff; + long h_wval; + long h_wrate; + char h_wst[8]; + char h_wmerch[64]; + char h_wacct[64]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); - bps = getl(b, T_ARG1); - if (bps <= 0 || bps > 10000) bps = 8000; /* 기본 지급률 80% */ - - EXEC SQL SELECT coalesce(sum(net),0) INTO :h_pend - FROM py_payout WHERE merchant_id = :h_merch AND status IN ('R','A'); - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT coalesce(sum(net),0) INTO :h_out - FROM py_payout - WHERE merchant_id = :h_merch AND channel = 'ADVANCE' - AND status NOT IN ('C','V','X'); - if (sqlca.sqlcode < 0) FAIL(b); - - avail = h_pend * bps / 10000 - h_out; - if (avail < 0) avail = 0; - setl(b, T_RC, 0); - setl(b, T_GROSS, h_pend); - setl(b, T_AMT1, avail); - setl(b, T_AMT2, h_out); - setl(b, T_ARG1, bps); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_arg1 = getl(b, T_ARG1); + if (h_arg1 < 0) h_arg1 = 0; + gets_(b, T_MERCHANT, h_merchant, sizeof(h_merchant)); + if (h_merchant[0] == 0) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "가맹점 ID는 필수 입력", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + /* 2) 마스터 조회부 - 가맹점 상태/한도 및 미지급 잔액 */ + EXEC SQL SELECT status, daily_limit INTO :h_wst, :h_wlim + FROM merchant WHERE merchant_id = :h_merchant; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "가맹점 미등록", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "가맹점 조회 오류", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wcnt, :h_wsum + FROM py_payout WHERE merchant_id = :h_merchant AND status IN ('R','A'); + if (sqlca.sqlcode < 0) { h_wcnt = 0; h_wsum = 0; } + /* 기 실행된 선지급 잔액 */ + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_wused FROM py_payout + WHERE merchant_id = :h_merchant AND channel = 'ADVANCE' + AND status IN ('A','F','P'); + if (sqlca.sqlcode < 0) h_wused = 0; + strcpy(h_pval, "7000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_adv_default_bps'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 7000; + /* 3) 업무규칙 분기부 - 지급률 구간별 선지급 가능액 (3분기) */ + if (strcmp(h_wst, "ACTIVE") != 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "비활성 가맹점은 선지급 산정 불가", 0L); + FAIL(b); + } + if (h_arg1 == 0) { + h_wrate = h_thr; + } else if (h_arg1 > 10000) { + h_wrate = 10000; + } else { + h_wrate = h_arg1; + } + h_wval = h_wsum * h_wrate / 10000; + h_waff = h_wval - h_wused; + if (h_waff < 0) h_waff = 0; + if (h_wlim > 0 && h_waff > h_wlim) h_waff = h_wlim; + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s merch=%s 미지급=%ld 가능액=%ld 기실행=%ld", "PAY_ADVCALC", h_merchant, h_wsum, h_waff, h_wused); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_ADVCALC', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 8) 응답부 */ + if (h_wcnt == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "선지급 대상 미지급건 없음", 0L); + } else if (h_waff == 0) { + setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "선지급 가용 한도 소진", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "선지급 가능", 0L); + } + setl(b, T_AMT1, h_wval); + setl(b, T_AMT2, h_wused); + setl(b, T_ARG1, h_wrate); + setl(b, T_GROSS, h_waff); + userlog("PAY_ADVCALC 완료: merch=%s 가능액=%ld", h_merchant, h_waff); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_ADVCALC", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_ADVGRANT.pgc b/app/src/py/svc/PAY_ADVGRANT.pgc index fc383a1..e76a4af 100644 --- a/app/src/py/svc/PAY_ADVGRANT.pgc +++ b/app/src/py/svc/PAY_ADVGRANT.pgc @@ -1,7 +1,9 @@ /* - * PAY_ADVGRANT.pgc - py 모듈 서비스 PAY_ADVGRANT (one service = one file). + * PAY_ADVGRANT.pgc - py (지급) 모듈 온라인 서비스 PAY_ADVGRANT. * 선지급 승인: 선지급(ADVANCE) 채널의 요청(R) 건만 승인(A)으로 전이한다. * 채널/상태를 함께 가드하며, 대상이 아니면 사유코드로 구분해 회신한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [txn]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_ADVGRANT.h" @@ -10,23 +12,144 @@ void PAY_ADVGRANT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_amt; - char h_st[4], h_chan[16]; + long h_id1; + char h_wst[8]; + long h_wamt; + long h_wnet; + char h_wchan[16]; + long h_wretry; + long h_waff; + char h_wmerch[64]; + char h_wacct[64]; + long h_wcnt; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_ID1); - EXEC SQL SELECT status, channel, amount INTO :h_st, :h_chan, :h_amt - FROM py_payout WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - if (strcmp(h_chan, "ADVANCE") != 0) { setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "선지급 건 아님", 0L); FAIL(b); } - if (h_st[0] != 'R') { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "요청 상태만 승인 가능", 0L); FAIL(b); } - - EXEC SQL UPDATE py_payout SET status = 'A', updated_at = now() WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + /* 2) 마스터 조회부 - 지급건 현재 상태/금액/채널/재시도 이력 */ + EXEC SQL SELECT status, amount, net, channel, retry_count, merchant_id, account_no + INTO :h_wst, :h_wamt, :h_wnet, :h_wchan, :h_wretry, :h_wmerch, :h_wacct + FROM py_payout WHERE payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 지급건이 존재하지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급건 조회 중 DB 오류", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + userlog("PAY_ADVGRANT 진입: payout=%ld status=%s amt=%ld chan=%s", + h_id1, h_wst, h_wamt, h_wchan); + /* 3) 업무규칙 분기부 - 상태 전이 가능성/한도/재시도 판정 */ + if (strcmp(h_wst, "P") == 0 || strcmp(h_wst, "X") == 0) { + setl(b, T_RC, 2); Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "이미 종결(지급완료/마감)된 건은 선지급 승인 불가", 0L); FAIL(b); + } + if (strcmp(h_wst, "R") != 0) { + setl(b, T_RC, 3); Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "현재 상태에서는 선지급 승인 전이가 허용되지 않음", 0L); FAIL(b); + } + if (strcmp(h_wchan, "ADVANCE") == 0 && h_wnet > 50000000) { + userlog("PAY_ADVGRANT 고액 선지급 건 주의: payout=%ld net=%ld", h_id1, h_wnet); + } + strcpy(h_pval, "100000000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_advgrant_amt_max'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 100000000; + if (h_wamt > h_thr) { + userlog("PAY_ADVGRANT 정책 상한 초과: amt=%ld thr=%ld", h_wamt, h_thr); + } + /* 4) 본처리부 - 가드 조건부 상태 전이 UPDATE */ + EXEC SQL UPDATE py_payout SET status = 'A', updated_at = now() + WHERE payout_id = :h_id1 AND status = 'R'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "선지급 승인 상태 갱신 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "동시성 충돌 - 전이 대상 건이 없음", 0L); + FAIL(b); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s payout=%ld %s->%s amt=%ld aff=%ld", "PAY_ADVGRANT", h_id1, "R", "A", h_wamt, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_ADVGRANT', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계 재산정부 - 당일 동일 상태 건수 재집계 */ + EXEC SQL SELECT count(*) INTO :h_wcnt FROM py_payout + WHERE biz_date = :h_wdate AND status = 'A'; + if (sqlca.sqlcode < 0) h_wcnt = 0; + /* 6) 집계/요약 갱신부 - 가맹점 당일 확정성 지급 실적 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_wmerch + AND biz_date = :h_wdate AND status IN ('A','F','P'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "가맹점 지급 실적 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE merchant_id = :h_wmerch AND biz_date = :h_wdate + AND status IN ('R','H'); + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 미지급 잔량 과다: 기준=%ld 실적=%ld", "PAY_ADVGRANT", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - setl(b, T_AMOUNT, h_amt); Bchg(b, T_STATUS, 0, "A", 0L); - userlog("PAY_ADVGRANT payout=%ld amt=%ld", h_pid, h_amt); + setl(b, T_AMOUNT, h_wamt); + Bchg(b, T_MSG, 0, "선지급 승인 정상 처리", 0L); + userlog("PAY_ADVGRANT 완료: payout=%ld -> %s (당일 %s상태 %ld건)", + h_id1, "A", "A", h_wcnt); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_ADVGRANT", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_ADVREPAY.pgc b/app/src/py/svc/PAY_ADVREPAY.pgc index 64850e2..8910d4f 100644 --- a/app/src/py/svc/PAY_ADVREPAY.pgc +++ b/app/src/py/svc/PAY_ADVREPAY.pgc @@ -1,7 +1,9 @@ /* - * PAY_ADVREPAY.pgc - py 모듈 서비스 PAY_ADVREPAY (one service = one file). + * PAY_ADVREPAY.pgc - py (지급) 모듈 온라인 서비스 PAY_ADVREPAY. * 선지급 상환: ADVANCE 채널 지급건의 잔여 net 에서 상환액을 차감하고 * py_adjust 에 음수 조정으로 남긴다. 완납되면 마감(X) 전이. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [calc]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_ADVREPAY.h" @@ -9,39 +11,154 @@ void PAY_ADVREPAY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long repay; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_net, h_new; - char h_chan[16]; + long h_amount; + char h_bizdate[64]; + long h_id1; + long h_wcnt; + long h_wsum; + long h_wnet; + long h_wlim; + long h_wused; + long h_waff; + long h_wval; + long h_wrate; + char h_wst[8]; + char h_wmerch[64]; + char h_wacct[64]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_ID1); - repay = getl(b, T_AMOUNT); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - - EXEC SQL SELECT channel, net INTO :h_chan, :h_net - FROM py_payout WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - if (strcmp(h_chan, "ADVANCE") != 0) { - setl(b, T_RC, 1); - Bchg(b, T_MSG, 0, "선지급 건만 상환 가능", 0L); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_amount = getl(b, T_AMOUNT); + if (h_amount < 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "금액 항목은 음수가 될 수 없음", 0L); FAIL(b); } - if (repay <= 0 || repay > h_net) repay = h_net; /* 초과 상환은 잔액 전액으로 절사 */ - if (pydb_insert_adjust(h_pid, -repay, "선지급상환", bizdate) < 0) FAIL(b); - h_new = h_net - repay; - EXEC SQL UPDATE py_payout - SET net = :h_new, - status = CASE WHEN :h_new <= 0 THEN 'X' ELSE status END, - updated_at = now() - WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 13); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + /* 2) 마스터 조회부 - 선지급 건 잔여 순액 */ + EXEC SQL SELECT status, net, merchant_id, account_no + INTO :h_wst, :h_wnet, :h_wmerch, :h_wacct + FROM py_payout WHERE payout_id = :h_id1 AND channel = 'ADVANCE'; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "선지급(ADVANCE) 건이 아니거나 존재하지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "선지급건 조회 오류", 0L); + FAIL(b); + } + h_wused = pydb_adjust_delta_sum(h_id1); + if (h_wused < 0) h_wused = 0; + /* 3) 업무규칙 분기부 - 상환 가능 범위 판정 (3분기) */ + if (strcmp(h_wst, "C") == 0 || strcmp(h_wst, "X") == 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "종결된 선지급건은 상환 불가", 0L); + FAIL(b); + } + if (h_amount > h_wnet) { + h_wval = h_wnet; + } else if (h_amount <= 0) { + setl(b, T_RC, 43); + Bchg(b, T_MSG, 0, "상환액은 1 이상이어야 함", 0L); + FAIL(b); + } else { + h_wval = h_amount; + } + h_wnet = h_wnet - h_wval; + if (h_wnet < 0) h_wnet = 0; + strcpy(h_pval, "1"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_advrepay_min'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 1; + if (h_wval < h_thr) { + setl(b, T_RC, 22); + Bchg(b, T_MSG, 0, "최소 상환액 미만", 0L); + FAIL(b); + } + /* 4) 본처리부 - 잔여 순액 차감 및 완제 시 종결 */ + EXEC SQL UPDATE py_payout SET net = :h_wnet, updated_at = now() + WHERE payout_id = :h_id1; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "선지급 잔액 차감 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_wnet == 0) { + EXEC SQL UPDATE py_payout SET status = 'X', updated_at = now() + WHERE payout_id = :h_id1; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 51); + Bchg(b, T_MSG, 0, "선지급 완제 종결 처리 실패", 0L); + FAIL(b); + } + strcpy(h_wst, "X"); + } + if (pydb_insert_adjust(h_id1, -h_wval, "선지급 상환", h_bizdate) < 0) { + setl(b, T_RC, 55); + Bchg(b, T_MSG, 0, "상환 조정원장 기록 실패", 0L); + FAIL(b); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s payout=%ld 상환=%ld 잔여=%ld", "PAY_ADVREPAY", h_id1, h_wval, h_wnet); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_ADVREPAY', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - setl(b, T_AMOUNT, repay); - setl(b, T_NET, h_new); - Bchg(b, T_STATUS, 0, h_new <= 0 ? "X" : "A", 0L); - userlog("PAY_ADVREPAY id=%ld repay=%ld remain=%ld", h_pid, repay, h_new); + setl(b, T_AMOUNT, h_wval); + setl(b, T_NET, h_wnet); + Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "선지급 상환 처리 완료", 0L); + userlog("PAY_ADVREPAY 완료: payout=%ld 상환=%ld 잔여=%ld", h_id1, h_wval, h_wnet); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_ADVREPAY", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_APPRBULK.pgc b/app/src/py/svc/PAY_APPRBULK.pgc index 043b2a5..7d97242 100644 --- a/app/src/py/svc/PAY_APPRBULK.pgc +++ b/app/src/py/svc/PAY_APPRBULK.pgc @@ -1,7 +1,9 @@ /* - * PAY_APPRBULK.pgc - py 모듈 서비스 PAY_APPRBULK (one service = one file). + * PAY_APPRBULK.pgc - py (지급) 모듈 온라인 서비스 PAY_APPRBULK. * 소액 일괄 승인: 당일 요청(R)건 중 net 이 승인한도 이하인 건을 한 번의 * UPDATE 로 승인(A)하고, 남은 고액 대기 건수를 함께 회신한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [calc]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_APPRBULK.h" @@ -10,26 +12,116 @@ void PAY_APPRBULK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_cap, h_appr, h_remain; + long h_amt1; + char h_bizdate[64]; + long h_wcnt; + long h_wsum; + long h_wnet; + long h_wlim; + long h_wused; + long h_waff; + long h_wval; + long h_wrate; + char h_wst[8]; + char h_wmerch[64]; + char h_wacct[64]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_amt1 = getl(b, T_AMT1); + if (h_amt1 < 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "금액 항목은 음수가 될 수 없음", 0L); + FAIL(b); + } gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - h_cap = getl(b, T_AMT1); - if (h_cap <= 0) h_cap = 500000; /* 기본 자동승인 한도 50만원 */ - + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + /* 2) 마스터 조회부 - 당일 소액 요청건 규모 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wcnt, :h_wsum + FROM py_payout WHERE biz_date = :h_bizdate AND status = 'R' + AND net <= :h_amt1; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "일괄 승인 대상 집계 오류", 0L); + FAIL(b); + } + strcpy(h_pval, "5000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_apprbulk_max_cnt'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 5000; + /* 3) 업무규칙 분기부 - 일괄 승인 가능 여부 (3분기) */ + if (h_wcnt == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "일괄 승인 대상 없음", 0L); + setl(b, T_COUNT, 0); OK(b); + } + if (h_wcnt > h_thr) { + setl(b, T_RC, 24); + Bchg(b, T_MSG, 0, "일괄 승인 건수 상한 초과 - 분할 실행 필요", 0L); + FAIL(b); + } + if (h_amt1 <= 0) { + setl(b, T_RC, 25); + Bchg(b, T_MSG, 0, "승인 한도 금액이 지정되지 않음", 0L); + FAIL(b); + } + /* 4) 본처리부 - 한 번의 UPDATE 로 일괄 승인 */ EXEC SQL UPDATE py_payout SET status = 'A', updated_at = now() - WHERE biz_date = :h_bizdate AND status = 'R' AND net <= :h_cap; - if (sqlca.sqlcode < 0) FAIL(b); - h_appr = sqlca.sqlerrd[2]; - EXEC SQL SELECT count(*) INTO :h_remain - FROM py_payout WHERE biz_date = :h_bizdate AND status = 'R'; - if (sqlca.sqlcode < 0) FAIL(b); - + WHERE biz_date = :h_bizdate AND status = 'R' AND net <= :h_amt1; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "일괄 승인 처리 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s 대상=%ld 승인=%ld 금액=%ld", "PAY_APPRBULK", h_wcnt, h_waff, h_wsum); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_APPRBULK', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - setl(b, T_COUNT, h_appr); - setl(b, T_ARG1, h_remain); - setl(b, T_AMT1, h_cap); - userlog("PAY_APPRBULK date=%s cap=%ld approved=%ld remain=%ld", h_bizdate, h_cap, h_appr, h_remain); + setl(b, T_COUNT, h_waff); + setl(b, T_AMT1, h_wsum); + setl(b, T_ARG1, h_wcnt); + userlog("PAY_APPRBULK 완료: 승인=%ld건 금액=%ld", h_waff, h_wsum); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_APPRBULK", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_APPRCANCEL.pgc b/app/src/py/svc/PAY_APPRCANCEL.pgc index c29b772..5c66bea 100644 --- a/app/src/py/svc/PAY_APPRCANCEL.pgc +++ b/app/src/py/svc/PAY_APPRCANCEL.pgc @@ -1,7 +1,9 @@ /* - * PAY_APPRCANCEL.pgc - py 모듈 서비스 PAY_APPRCANCEL (one service = one file). + * PAY_APPRCANCEL.pgc - py (지급) 모듈 온라인 서비스 PAY_APPRCANCEL. * 지급 승인 취소: 아직 파일에 편성되지 않은 승인(A) 건을 요청(R)으로 되돌린다. * 파일편성(F)/지급완료(P) 이후에는 승인 취소가 불가하다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [txn]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_APPRCANCEL.h" @@ -9,20 +11,147 @@ void PAY_APPRCANCEL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long reverted; EXEC SQL BEGIN DECLARE SECTION; - long h_pid; + long h_id1; + char h_wst[8]; + long h_wamt; + long h_wnet; + char h_wchan[16]; + long h_wretry; + long h_waff; + char h_wmerch[64]; + char h_wacct[64]; + long h_wcnt; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_ID1); - EXEC SQL UPDATE py_payout SET status = 'R', updated_at = now() - WHERE payout_id = :h_pid AND status = 'A'; - if (sqlca.sqlcode < 0) FAIL(b); - reverted = sqlca.sqlerrd[2]; - if (reverted == 0) { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "승인 상태 건만 취소 가능", 0L); FAIL(b); } - + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + /* 2) 마스터 조회부 - 지급건 현재 상태/금액/채널/재시도 이력 */ + EXEC SQL SELECT status, amount, net, channel, retry_count, merchant_id, account_no + INTO :h_wst, :h_wamt, :h_wnet, :h_wchan, :h_wretry, :h_wmerch, :h_wacct + FROM py_payout WHERE payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 지급건이 존재하지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급건 조회 중 DB 오류", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + userlog("PAY_APPRCANCEL 진입: payout=%ld status=%s amt=%ld chan=%s", + h_id1, h_wst, h_wamt, h_wchan); + /* 3) 업무규칙 분기부 - 상태 전이 가능성/한도/재시도 판정 */ + if (strcmp(h_wst, "P") == 0 || strcmp(h_wst, "X") == 0) { + setl(b, T_RC, 2); Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "이미 종결(지급완료/마감)된 건은 승인 취소 불가", 0L); FAIL(b); + } + if (strcmp(h_wst, "A") != 0) { + setl(b, T_RC, 3); Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "현재 상태에서는 승인 취소 전이가 허용되지 않음", 0L); FAIL(b); + } + if (h_wretry >= 3) { + setl(b, T_RC, 5); + Bchg(b, T_MSG, 0, "재시도 한도(3회) 초과 - 수동 처리 필요", 0L); + FAIL(b); + } + strcpy(h_pval, "100000000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_apprcancel_amt_max'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 100000000; + if (h_wamt > h_thr) { + userlog("PAY_APPRCANCEL 정책 상한 초과: amt=%ld thr=%ld", h_wamt, h_thr); + } + /* 4) 본처리부 - 가드 조건부 상태 전이 UPDATE */ + EXEC SQL UPDATE py_payout SET status = 'R', retry_count = retry_count + 1, + updated_at = now() + WHERE payout_id = :h_id1 AND status = 'A'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "승인 취소 상태 갱신 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "동시성 충돌 - 전이 대상 건이 없음", 0L); + FAIL(b); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s payout=%ld %s->%s amt=%ld aff=%ld", "PAY_APPRCANCEL", h_id1, "A", "R", h_wamt, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_APPRCANCEL', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계 재산정부 - 당일 동일 상태 건수 재집계 */ + EXEC SQL SELECT count(*) INTO :h_wcnt FROM py_payout + WHERE biz_date = :h_wdate AND status = 'R'; + if (sqlca.sqlcode < 0) h_wcnt = 0; + /* 6) 집계/요약 갱신부 - 가맹점 당일 확정성 지급 실적 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_wmerch + AND biz_date = :h_wdate AND status IN ('A','F','P'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "가맹점 지급 실적 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE merchant_id = :h_wmerch AND biz_date = :h_wdate + AND status IN ('R','H'); + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 미지급 잔량 과다: 기준=%ld 실적=%ld", "PAY_APPRCANCEL", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); Bchg(b, T_STATUS, 0, "R", 0L); - userlog("PAY_APPRCANCEL payout=%ld -> R", h_pid); + Bchg(b, T_MSG, 0, "승인 취소 정상 처리", 0L); + userlog("PAY_APPRCANCEL 완료: payout=%ld -> %s (당일 %s상태 %ld건)", + h_id1, "R", "R", h_wcnt); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_APPRCANCEL", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_APPRHIST.pgc b/app/src/py/svc/PAY_APPRHIST.pgc index a28cb5a..94adeef 100644 --- a/app/src/py/svc/PAY_APPRHIST.pgc +++ b/app/src/py/svc/PAY_APPRHIST.pgc @@ -1,7 +1,9 @@ /* - * PAY_APPRHIST.pgc - py 모듈 서비스 PAY_APPRHIST (one service = one file). + * PAY_APPRHIST.pgc - py (지급) 모듈 온라인 서비스 PAY_APPRHIST. * 지급 승인/원장 이력: 지정 지급건의 py_ledger 기표를 시간순 커서로 훑어 * 기표유형/금액/일자를 occurrence 로 회신한다 (승인·이체·환입 흐름 추적). + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [cursor]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_APPRHIST.h" @@ -9,33 +11,128 @@ void PAY_APPRHIST(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - int i = 0; - long rows = 0; + int gi = 0; + long tot_cnt = 0, tot_sum = 0, tot_fee = 0, top_cnt = 0, top_sum = 0; + char top_grp[64]; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_amt; - char h_type[16], h_date[16]; + long h_id1; + char h_wgrp[64]; + long h_wgcnt; + long h_wgsum; + long h_wgfee; + long h_wtot; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_ID1); - EXEC SQL DECLARE cur_apprhist CURSOR FOR - SELECT entry_type, amount, CAST(biz_date AS text) - FROM py_ledger WHERE payout_id = :h_pid ORDER BY ledger_id; - EXEC SQL OPEN cur_apprhist; - if (sqlca.sqlcode < 0) FAIL(b); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + top_grp[0] = 0; + /* 2) 사전 규모 확인 - 커서 오픈 전 총건수 파악 */ + EXEC SQL SELECT count(*) INTO :h_wtot FROM py_ledger l WHERE l.payout_id = :h_id1; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "집계 사전 조회 실패", 0L); + FAIL(b); + } + userlog("PAY_APPRHIST 집계 시작: 기준일=%s 대상=%ld건 (기표이력별)", h_wdate, h_wtot); + /* 커서 정의: 기표이력별 건수/금액 집계 */ + EXEC SQL DECLARE cur_pay_apprhist CURSOR FOR + SELECT l.entry_type, count(*), coalesce(sum(l.amount),0), 0 + FROM py_ledger l WHERE l.payout_id = :h_id1 + GROUP BY l.entry_type ORDER BY 2 DESC, 1; + EXEC SQL OPEN cur_pay_apprhist; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "집계 커서 OPEN 실패", 0L); + FAIL(b); + } + /* 3) 본처리부 - 커서 순회하며 그룹별 집계/상위 추출 */ for (;;) { - EXEC SQL FETCH cur_apprhist INTO :h_type, :h_amt, :h_date; + EXEC SQL FETCH cur_pay_apprhist INTO :h_wgrp, :h_wgcnt, :h_wgsum, :h_wgfee; if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_apprhist; FAIL(b); } - rows++; - if (i < 12) { - Bchg(b, T_STR3, i, h_type, 0L); - Bchg(b, T_AMT1, i, (char *)&h_amt, 0L); - Bchg(b, T_STR4, i, h_date, 0L); - i++; + if (sqlca.sqlcode < 0) { + EXEC SQL CLOSE cur_pay_apprhist; + setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "집계 FETCH 오류", 0L); FAIL(b); + } + tot_cnt += h_wgcnt; + tot_sum += h_wgsum; + tot_fee += h_wgfee; + if (h_wgcnt > top_cnt) { + top_cnt = h_wgcnt; + top_sum = h_wgsum; + strncpy(top_grp, h_wgrp, sizeof(top_grp)-1); + top_grp[sizeof(top_grp)-1] = 0; + } + if (gi < 10) { + Bchg(b, T_STR1, gi, h_wgrp, 0L); + Bchg(b, T_AMT1, gi, (char *)&h_wgsum, 0L); + Bchg(b, T_ARG1, gi, (char *)&h_wgcnt, 0L); + gi++; } } - EXEC SQL CLOSE cur_apprhist; - setl(b, T_COUNT, rows); - setl(b, T_RC, 0); + EXEC SQL CLOSE cur_pay_apprhist; + strcpy(h_pval, "5000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_apprhist_concentration'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 5000; + /* 업무규칙 분기부 - 분포 집중도 판정 (3분기) */ + if (gi == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "기표이력별 집계 대상 없음", 0L); + } else if (tot_cnt > 0 && top_cnt * 10000 / tot_cnt > h_thr) { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "특정 기표이력 집중 - 확인 필요", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "기표이력별 정상 분포", 0L); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s %s별 그룹=%d 건수=%ld 금액=%ld 최다=%s", "PAY_APPRHIST", "기표이력", gi, tot_cnt, tot_sum, top_grp); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_APPRHIST', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 8) 응답부 */ + setl(b, T_COUNT, tot_cnt); + Bchg(b, T_STR3, 0, top_grp, 0L); + Bchg(b, T_STR4, 0, top_grp, 0L); + userlog("PAY_APPRHIST 완료: 그룹=%d 총건=%ld 총액=%ld 최다=%s", + gi, tot_cnt, tot_sum, top_grp); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_APPRHIST", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_APPRLIMIT.pgc b/app/src/py/svc/PAY_APPRLIMIT.pgc index 51a3426..5e90ff3 100644 --- a/app/src/py/svc/PAY_APPRLIMIT.pgc +++ b/app/src/py/svc/PAY_APPRLIMIT.pgc @@ -1,7 +1,9 @@ /* - * PAY_APPRLIMIT.pgc - py 모듈 서비스 PAY_APPRLIMIT (one service = one file). + * PAY_APPRLIMIT.pgc - py (지급) 모듈 온라인 서비스 PAY_APPRLIMIT. * 고액 승인 통제: 한도 초과 건은 승인자 지정 없이는 거절(2). 승인 시 * py_ledger 에 APPROVE 기표를 남겨 누가/얼마를 승인했는지 증적화한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [calc]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_APPRLIMIT.h" @@ -9,40 +11,145 @@ void PAY_APPRLIMIT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char approver[32]; - long cap; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_net; - char h_st[4], h_bizdate[16]; + long h_amt1; + long h_id1; + char h_key2[64]; + long h_wcnt; + long h_wsum; + long h_wnet; + long h_wlim; + long h_wused; + long h_waff; + long h_wval; + long h_wrate; + char h_wst[8]; + char h_wmerch[64]; + char h_wacct[64]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_ID1); - gets_(b, T_KEY2, approver, sizeof(approver)); - cap = getl(b, T_AMT1); - if (cap <= 0) cap = 10000000; /* 기본 고액 기준 1천만원 */ - - EXEC SQL SELECT net, status, CAST(biz_date AS text) INTO :h_net, :h_st, :h_bizdate - FROM py_payout WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - if (h_st[0] != 'R') { - setl(b, T_RC, 1); - Bchg(b, T_MSG, 0, "요청 상태 건만 승인 가능", 0L); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_amt1 = getl(b, T_AMT1); + if (h_amt1 < 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "금액 항목은 음수가 될 수 없음", 0L); FAIL(b); } - if (h_net > cap && approver[0] == 0) { - setl(b, T_RC, 2); - Bchg(b, T_MSG, 0, "고액 지급은 승인자 지정 필수", 0L); + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); FAIL(b); } - EXEC SQL UPDATE py_payout SET status = 'A', updated_at = now() WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL INSERT INTO py_ledger (ledger_id, payout_id, entry_type, amount, biz_date) - VALUES (nextval('py_ledger_seq'), :h_pid, 'APPROVE', :h_net, :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); - + gets_(b, T_KEY2, h_key2, sizeof(h_key2)); + if (h_key2[0] == 0 || strlen(h_key2) < 3) { + setl(b, T_RC, 13); + Bchg(b, T_MSG, 0, "계좌번호/키 값이 유효하지 않음", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + /* 2) 마스터 조회부 - 승인 대상 건 및 가맹점 한도 */ + EXEC SQL SELECT p.status, p.net, p.merchant_id, coalesce(m.daily_limit,0) + INTO :h_wst, :h_wnet, :h_wmerch, :h_wlim + FROM py_payout p + LEFT JOIN merchant m ON m.merchant_id = p.merchant_id + WHERE p.payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "승인 대상 지급건 없음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급건 조회 오류", 0L); + FAIL(b); + } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_wused FROM py_payout + WHERE merchant_id = :h_wmerch AND biz_date = :h_wdate + AND status IN ('A','F','P'); + if (sqlca.sqlcode < 0) h_wused = 0; + /* 3) 업무규칙 분기부 - 고액 승인 통제 (3분기) */ + if (strcmp(h_wst, "R") != 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "요청(R) 상태 건만 승인 가능", 0L); + FAIL(b); + } + if (h_wnet > h_amt1 && h_key2[0] == 0) { + setl(b, T_RC, 2); Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "한도 초과 건은 승인자 지정이 필요", 0L); FAIL(b); + } else if (h_wlim > 0 && h_wused + h_wnet > h_wlim) { + setl(b, T_RC, 8); + Bchg(b, T_MSG, 0, "가맹점 일한도 초과로 승인 불가", 0L); + FAIL(b); + } + strcpy(h_pval, "1000000000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_apprlimit_hard'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 1000000000; + if (h_wnet > h_thr) { + setl(b, T_RC, 23); + Bchg(b, T_MSG, 0, "단건 승인 절대상한 초과", 0L); + FAIL(b); + } + /* 4) 본처리부 - 승인 전이 */ + EXEC SQL UPDATE py_payout SET status = 'A', updated_at = now() + WHERE payout_id = :h_id1 AND status = 'R'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "승인 처리 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "동시성 충돌 - 승인 대상 없음", 0L); + FAIL(b); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s payout=%ld net=%ld 승인자=%s 사용=%ld", "PAY_APPRLIMIT", h_id1, h_wnet, h_key2, h_wused); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_APPRLIMIT', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - setl(b, T_NET, h_net); + setl(b, T_NET, h_wnet); Bchg(b, T_STATUS, 0, "A", 0L); - userlog("PAY_APPRLIMIT id=%ld net=%ld approver=%s", h_pid, h_net, approver[0] ? approver : "-"); + Bchg(b, T_MSG, 0, "고액 승인 통제 통과 - 승인 완료", 0L); + userlog("PAY_APPRLIMIT 완료: payout=%ld net=%ld", h_id1, h_wnet); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_APPRLIMIT", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_APPROVE.pgc b/app/src/py/svc/PAY_APPROVE.pgc index bee7843..d69c787 100644 --- a/app/src/py/svc/PAY_APPROVE.pgc +++ b/app/src/py/svc/PAY_APPROVE.pgc @@ -1,7 +1,9 @@ /* - * PAY_APPROVE.pgc - py 모듈 서비스 PAY_APPROVE (one service = one file). + * PAY_APPROVE.pgc - py (지급) 모듈 온라인 서비스 PAY_APPROVE. * 지급 승인: 금액을 먼저 조회한 뒤 요청(R) 상태만 승인(A)으로 전이하고 금액을 * 함께 회신. SELECT amount + guarded UPDATE(status='R') + affected-count. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [txn]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_APPROVE.h" @@ -10,18 +12,144 @@ void PAY_APPROVE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - long h_pay, h_amt, h_aff; + long h_id1; + char h_wst[8]; + long h_wamt; + long h_wnet; + char h_wchan[16]; + long h_wretry; + long h_waff; + char h_wmerch[64]; + char h_wacct[64]; + long h_wcnt; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pay = getl(b, T_ID1); - EXEC SQL SELECT coalesce(amount,0) INTO :h_amt FROM py_payout WHERE payout_id = :h_pay; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + /* 2) 마스터 조회부 - 지급건 현재 상태/금액/채널/재시도 이력 */ + EXEC SQL SELECT status, amount, net, channel, retry_count, merchant_id, account_no + INTO :h_wst, :h_wamt, :h_wnet, :h_wchan, :h_wretry, :h_wmerch, :h_wacct + FROM py_payout WHERE payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 지급건이 존재하지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급건 조회 중 DB 오류", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + userlog("PAY_APPROVE 진입: payout=%ld status=%s amt=%ld chan=%s", + h_id1, h_wst, h_wamt, h_wchan); + /* 3) 업무규칙 분기부 - 상태 전이 가능성/한도/재시도 판정 */ + if (strcmp(h_wst, "P") == 0 || strcmp(h_wst, "X") == 0) { + setl(b, T_RC, 2); Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "이미 종결(지급완료/마감)된 건은 지급 승인 불가", 0L); FAIL(b); + } + if (strcmp(h_wst, "R") != 0) { + setl(b, T_RC, 3); Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "현재 상태에서는 지급 승인 전이가 허용되지 않음", 0L); FAIL(b); + } + if (strcmp(h_wchan, "ADVANCE") == 0 && h_wnet > 50000000) { + userlog("PAY_APPROVE 고액 선지급 건 주의: payout=%ld net=%ld", h_id1, h_wnet); + } + strcpy(h_pval, "100000000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_approve_amt_max'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 100000000; + if (h_wamt > h_thr) { + userlog("PAY_APPROVE 정책 상한 초과: amt=%ld thr=%ld", h_wamt, h_thr); + } + /* 4) 본처리부 - 가드 조건부 상태 전이 UPDATE */ EXEC SQL UPDATE py_payout SET status = 'A', updated_at = now() - WHERE payout_id = :h_pay AND status = 'R'; - if (sqlca.sqlcode < 0) FAIL(b); - h_aff = sqlca.sqlerrd[2]; - setl(b, T_AMOUNT, h_amt); - if (h_aff > 0) Bchg(b, T_STATUS, 0, "A", 0L); - setl(b, T_RC, h_aff > 0 ? 0 : 1); + WHERE payout_id = :h_id1 AND status = 'R'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "지급 승인 상태 갱신 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "동시성 충돌 - 전이 대상 건이 없음", 0L); + FAIL(b); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s payout=%ld %s->%s amt=%ld aff=%ld", "PAY_APPROVE", h_id1, "R", "A", h_wamt, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_APPROVE', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계 재산정부 - 당일 동일 상태 건수 재집계 */ + EXEC SQL SELECT count(*) INTO :h_wcnt FROM py_payout + WHERE biz_date = :h_wdate AND status = 'A'; + if (sqlca.sqlcode < 0) h_wcnt = 0; + /* 6) 집계/요약 갱신부 - 가맹점 당일 확정성 지급 실적 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_wmerch + AND biz_date = :h_wdate AND status IN ('A','F','P'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "가맹점 지급 실적 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE merchant_id = :h_wmerch AND biz_date = :h_wdate + AND status IN ('R','H'); + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 미지급 잔량 과다: 기준=%ld 실적=%ld", "PAY_APPROVE", h_wa1, h_wa3); + } + /* 8) 응답부 */ + setl(b, T_RC, 0); + Bchg(b, T_STATUS, 0, "A", 0L); + setl(b, T_AMOUNT, h_wamt); + Bchg(b, T_MSG, 0, "지급 승인 정상 처리", 0L); + userlog("PAY_APPROVE 완료: payout=%ld -> %s (당일 %s상태 %ld건)", + h_id1, "A", "A", h_wcnt); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_APPROVE", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_APPRPEND.pgc b/app/src/py/svc/PAY_APPRPEND.pgc index 307bb5b..77d345a 100644 --- a/app/src/py/svc/PAY_APPRPEND.pgc +++ b/app/src/py/svc/PAY_APPRPEND.pgc @@ -1,7 +1,9 @@ /* - * PAY_APPRPEND.pgc - py 모듈 서비스 PAY_APPRPEND (one service = one file). + * PAY_APPRPEND.pgc - py (지급) 모듈 온라인 서비스 PAY_APPRPEND. * 승인 대기 집계: 아직 승인 전인 요청(R) 지급건을 가맹점별로 묶어 대기 건수/ * 요청금액 합을 건수 내림차순 occurrence 로 회신한다 (승인 워크로드 파악). + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [cursor]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_APPRPEND.h" @@ -9,35 +11,123 @@ void PAY_APPRPEND(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - int i = 0; - long grp = 0, waiting = 0; + int gi = 0; + long tot_cnt = 0, tot_sum = 0, tot_fee = 0, top_cnt = 0, top_sum = 0; + char top_grp[64]; EXEC SQL BEGIN DECLARE SECTION; - char h_merch[64]; - long h_cnt, h_amt; + char h_wgrp[64]; + long h_wgcnt; + long h_wgsum; + long h_wgfee; + long h_wtot; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - EXEC SQL DECLARE cur_apprpend CURSOR FOR - SELECT merchant_id, count(*), coalesce(sum(amount),0) - FROM py_payout WHERE status = 'R' - GROUP BY merchant_id ORDER BY 2 DESC, 1; - EXEC SQL OPEN cur_apprpend; - if (sqlca.sqlcode < 0) FAIL(b); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + /* 입력 파라미터가 없는 전역 집계 서비스 - 검증 대상 없음 */ + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + top_grp[0] = 0; + /* 2) 사전 규모 확인 - 커서 오픈 전 총건수 파악 */ + EXEC SQL SELECT count(*) INTO :h_wtot FROM py_payout p WHERE p.status = 'R'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "집계 사전 조회 실패", 0L); + FAIL(b); + } + userlog("PAY_APPRPEND 집계 시작: 기준일=%s 대상=%ld건 (가맹점별)", h_wdate, h_wtot); + /* 커서 정의: 가맹점별 건수/금액 집계 */ + EXEC SQL DECLARE cur_pay_apprpend CURSOR FOR + SELECT p.merchant_id, count(*), coalesce(sum(p.net),0), coalesce(sum(p.fee),0) + FROM py_payout p WHERE p.status = 'R' + GROUP BY p.merchant_id ORDER BY 2 DESC, 1; + EXEC SQL OPEN cur_pay_apprpend; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "집계 커서 OPEN 실패", 0L); + FAIL(b); + } + /* 3) 본처리부 - 커서 순회하며 그룹별 집계/상위 추출 */ for (;;) { - EXEC SQL FETCH cur_apprpend INTO :h_merch, :h_cnt, :h_amt; + EXEC SQL FETCH cur_pay_apprpend INTO :h_wgrp, :h_wgcnt, :h_wgsum, :h_wgfee; if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_apprpend; FAIL(b); } - grp++; - waiting += h_cnt; - if (i < 10) { - Bchg(b, T_STR2, i, h_merch, 0L); - Bchg(b, T_ARG3, i, (char *)&h_cnt, 0L); - Bchg(b, T_AMT1, i, (char *)&h_amt, 0L); - i++; + if (sqlca.sqlcode < 0) { + EXEC SQL CLOSE cur_pay_apprpend; + setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "집계 FETCH 오류", 0L); FAIL(b); + } + tot_cnt += h_wgcnt; + tot_sum += h_wgsum; + tot_fee += h_wgfee; + if (h_wgcnt > top_cnt) { + top_cnt = h_wgcnt; + top_sum = h_wgsum; + strncpy(top_grp, h_wgrp, sizeof(top_grp)-1); + top_grp[sizeof(top_grp)-1] = 0; + } + if (gi < 10) { + Bchg(b, T_STR1, gi, h_wgrp, 0L); + Bchg(b, T_AMT1, gi, (char *)&h_wgsum, 0L); + Bchg(b, T_ARG1, gi, (char *)&h_wgcnt, 0L); + gi++; } } - EXEC SQL CLOSE cur_apprpend; - setl(b, T_COUNT, grp); - setl(b, T_ARG2, waiting); - setl(b, T_RC, 0); + EXEC SQL CLOSE cur_pay_apprpend; + strcpy(h_pval, "5000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_apprpend_concentration'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 5000; + /* 업무규칙 분기부 - 분포 집중도 판정 (3분기) */ + if (gi == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "가맹점별 집계 대상 없음", 0L); + } else if (tot_cnt > 0 && top_cnt * 10000 / tot_cnt > h_thr) { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "특정 가맹점 집중 - 확인 필요", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "가맹점별 정상 분포", 0L); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s %s별 그룹=%d 건수=%ld 금액=%ld 최다=%s", "PAY_APPRPEND", "가맹점", gi, tot_cnt, tot_sum, top_grp); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_APPRPEND', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 8) 응답부 */ + setl(b, T_COUNT, tot_cnt); + setl(b, T_ARG2, (long)gi); + setl(b, T_ARG3, (long)gi); + Bchg(b, T_STR2, 0, top_grp, 0L); + userlog("PAY_APPRPEND 완료: 그룹=%d 총건=%ld 총액=%ld 최다=%s", + gi, tot_cnt, tot_sum, top_grp); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_APPRPEND", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_APPRREQ.pgc b/app/src/py/svc/PAY_APPRREQ.pgc index 3513037..47fc678 100644 --- a/app/src/py/svc/PAY_APPRREQ.pgc +++ b/app/src/py/svc/PAY_APPRREQ.pgc @@ -1,7 +1,9 @@ /* - * PAY_APPRREQ.pgc - py 모듈 서비스 PAY_APPRREQ (one service = one file). + * PAY_APPRREQ.pgc - py (지급) 모듈 온라인 서비스 PAY_APPRREQ. * 승인 대기 큐 조회: 당일 요청(R)건을 금액 내림차순 상위 N건 커서로 훑어 * occurrence(T_ID2[i]/T_STR1[i]/T_AMT1[i]) 로 회신한다 (승인 워크플로 1단계). + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [cursor]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_APPRREQ.h" @@ -9,34 +11,128 @@ void PAY_APPRREQ(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - int i = 0; + int gi = 0; + long tot_cnt = 0, tot_sum = 0, tot_fee = 0, top_cnt = 0, top_sum = 0; + char top_grp[64]; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16], h_merch[64]; - long h_lim, h_pid, h_net; + long h_arg1; + char h_bizdate[64]; + char h_wgrp[64]; + long h_wgcnt; + long h_wgsum; + long h_wgfee; + long h_wtot; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_arg1 = getl(b, T_ARG1); + if (h_arg1 < 0) h_arg1 = 0; gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - h_lim = getl(b, T_ARG1); - if (h_lim <= 0 || h_lim > 10) h_lim = 5; - - EXEC SQL DECLARE cur_apprreq CURSOR FOR - SELECT payout_id, merchant_id, net FROM py_payout - WHERE biz_date = :h_bizdate AND status = 'R' - ORDER BY net DESC, payout_id - LIMIT :h_lim; - EXEC SQL OPEN cur_apprreq; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH cur_apprreq INTO :h_pid, :h_merch, :h_net; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_apprreq; FAIL(b); } - Bchg(b, T_ID2, i, (char *)&h_pid, 0L); - Bchg(b, T_STR1, i, h_merch, 0L); - Bchg(b, T_AMT1, i, (char *)&h_net, 0L); - i++; + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + top_grp[0] = 0; + /* 2) 사전 규모 확인 - 커서 오픈 전 총건수 파악 */ + EXEC SQL SELECT count(*) INTO :h_wtot FROM py_payout p WHERE p.biz_date = :h_bizdate AND p.status = 'R'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "집계 사전 조회 실패", 0L); + FAIL(b); + } + userlog("PAY_APPRREQ 집계 시작: 기준일=%s 대상=%ld건 (승인대기별)", h_wdate, h_wtot); + /* 커서 정의: 승인대기별 건수/금액 집계 */ + EXEC SQL DECLARE cur_pay_apprreq CURSOR FOR + SELECT p.merchant_id, count(*), coalesce(sum(p.net),0), coalesce(sum(p.fee),0) + FROM py_payout p WHERE p.biz_date = :h_bizdate AND p.status = 'R' + GROUP BY p.merchant_id ORDER BY 2 DESC, 1; + EXEC SQL OPEN cur_pay_apprreq; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "집계 커서 OPEN 실패", 0L); + FAIL(b); + } + /* 3) 본처리부 - 커서 순회하며 그룹별 집계/상위 추출 */ + for (;;) { + EXEC SQL FETCH cur_pay_apprreq INTO :h_wgrp, :h_wgcnt, :h_wgsum, :h_wgfee; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { + EXEC SQL CLOSE cur_pay_apprreq; + setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "집계 FETCH 오류", 0L); FAIL(b); + } + tot_cnt += h_wgcnt; + tot_sum += h_wgsum; + tot_fee += h_wgfee; + if (h_wgcnt > top_cnt) { + top_cnt = h_wgcnt; + top_sum = h_wgsum; + strncpy(top_grp, h_wgrp, sizeof(top_grp)-1); + top_grp[sizeof(top_grp)-1] = 0; + } + if (gi < 10) { + Bchg(b, T_STR1, gi, h_wgrp, 0L); + Bchg(b, T_AMT1, gi, (char *)&h_wgsum, 0L); + Bchg(b, T_ARG1, gi, (char *)&h_wgcnt, 0L); + gi++; + } + } + EXEC SQL CLOSE cur_pay_apprreq; + strcpy(h_pval, "5000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_apprreq_concentration'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 5000; + /* 업무규칙 분기부 - 분포 집중도 판정 (3분기) */ + if (gi == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "승인대기별 집계 대상 없음", 0L); + } else if (tot_cnt > 0 && top_cnt * 10000 / tot_cnt > h_thr) { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "특정 승인대기 집중 - 확인 필요", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "승인대기별 정상 분포", 0L); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s %s별 그룹=%d 건수=%ld 금액=%ld 최다=%s", "PAY_APPRREQ", "승인대기", gi, tot_cnt, tot_sum, top_grp); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_APPRREQ', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 8) 응답부 */ + setl(b, T_COUNT, tot_cnt); + userlog("PAY_APPRREQ 완료: 그룹=%d 총건=%ld 총액=%ld 최다=%s", + gi, tot_cnt, tot_sum, top_grp); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_APPRREQ", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); } - EXEC SQL CLOSE cur_apprreq; - setl(b, T_COUNT, (long)i); - setl(b, T_RC, 0); OK(b); } diff --git a/app/src/py/svc/PAY_BANKFILE.pgc b/app/src/py/svc/PAY_BANKFILE.pgc index 5d237ab..1099d99 100644 --- a/app/src/py/svc/PAY_BANKFILE.pgc +++ b/app/src/py/svc/PAY_BANKFILE.pgc @@ -1,7 +1,9 @@ /* - * PAY_BANKFILE.pgc - py 모듈 서비스 PAY_BANKFILE (one service = one file). + * PAY_BANKFILE.pgc - py (지급) 모듈 온라인 서비스 PAY_BANKFILE. * 은행별 지급파일 생성: 지정 은행코드의 승인(A) 지급건만 py_account 조인으로 * 골라 집계 -> py_file 생성 -> 대상 건을 파일편성(F) + file_id 로 일괄 전이. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [file]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_BANKFILE.h" @@ -9,40 +11,160 @@ void PAY_BANKFILE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char fname[64]; EXEC SQL BEGIN DECLARE SECTION; - long h_fid, h_cnt, h_sum; - char h_bank[8], h_bizdate[16], h_fname[64]; + char h_bizdate[64]; + char h_key1[64]; + char h_wfst[8]; + long h_whcnt; + long h_whamt; + long h_wrcnt; + long h_wramt; + long h_waff; + long h_wnew; + char h_wfname[64]; + char h_wfdate[16]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, h_bank, sizeof(h_bank)); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - if (h_bank[0] == 0) { userlog("PAY_BANKFILE: 은행코드 누락"); FAIL(b); } - - EXEC SQL SELECT count(*), coalesce(sum(p.net),0) INTO :h_cnt, :h_sum - FROM py_payout p JOIN py_account a ON p.account_no = a.account_no - WHERE p.biz_date = :h_bizdate AND p.status = 'A' AND a.bank_code = :h_bank; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_cnt == 0) { setl(b, T_RC, 1); setl(b, T_COUNT, 0); OK(b); } - - snprintf(fname, sizeof(fname), "PAYOUT_%s_%s.dat", h_bank, h_bizdate); - strncpy(h_fname, fname, sizeof(h_fname)-1); h_fname[sizeof(h_fname)-1] = 0; - EXEC SQL SELECT nextval('py_file_seq') INTO :h_fid; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL INSERT INTO py_file (file_id, biz_date, file_name, record_count, total_amount, status) - VALUES (:h_fid, :h_bizdate, :h_fname, :h_cnt, :h_sum, 'G'); - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL UPDATE py_payout SET status = 'F', file_id = :h_fid, updated_at = now() - FROM py_account a - WHERE py_payout.account_no = a.account_no AND py_payout.biz_date = :h_bizdate - AND py_payout.status = 'A' AND a.bank_code = :h_bank; - if (sqlca.sqlcode < 0) FAIL(b); - - setl(b, T_RC, 0); - setl(b, T_ID2, h_fid); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_sum); - Bchg(b, T_STR1, 0, fname, 0L); - userlog("PAY_BANKFILE bank=%s file_id=%ld recs=%ld amt=%ld", h_bank, h_fid, h_cnt, h_sum); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + gets_(b, T_KEY1, h_key1, sizeof(h_key1)); + if (h_key1[0] == 0 || strlen(h_key1) < 3) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "계좌번호/키 값이 유효하지 않음", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + /* 2) 마스터 조회부 - 당일 승인(A) 편성 대상 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(p.net),0) + INTO :h_wrcnt, :h_wramt + FROM py_payout p JOIN py_account a ON a.account_no = p.account_no + WHERE p.biz_date = :h_bizdate AND p.status = 'A' + AND a.bank_code = :h_key1; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "편성 대상 집계 오류", 0L); + FAIL(b); + } + h_whcnt = h_wrcnt; h_whamt = h_wramt; + strcpy(h_wfst, "G"); + strncpy(h_wfdate, h_bizdate, sizeof(h_wfdate)-1); + h_wfdate[sizeof(h_wfdate)-1] = 0; + userlog("PAY_BANKFILE 진입: 기준일=%s 편성대상=%ld건 %ld원", + h_bizdate, h_wrcnt, h_wramt); + /* 3) 업무규칙 분기부 - 파일 상태/정합 판정 (3분기) */ + if (h_wrcnt == 0) { + setl(b, T_RC, 43); + Bchg(b, T_MSG, 0, "편성 대상 승인건이 없음", 0L); + FAIL(b); + } else if (h_wramt <= 0) { + setl(b, T_RC, 44); + Bchg(b, T_MSG, 0, "편성 총액이 비정상", 0L); + FAIL(b); + } + strcpy(h_pval, "100000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_bankfile_max_rec'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 100000; + if (h_wrcnt > h_thr) { + setl(b, T_RC, 29); + Bchg(b, T_MSG, 0, "파일 최대 레코드 수 초과 - 분할 편성 필요", 0L); + FAIL(b); + } + /* 4) 본처리부 - 파일 채번/헤더 생성 후 대상건 편성 */ + EXEC SQL SELECT nextval('py_file_seq') INTO :h_wnew; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "지급파일 채번 실패", 0L); + FAIL(b); + } + snprintf(h_wfname, sizeof(h_wfname), "BANK_%.10s_%ld.dat", + h_bizdate, h_wnew); + EXEC SQL INSERT INTO py_file + (file_id, biz_date, file_name, record_count, total_amount, status) + VALUES (:h_wnew, :h_bizdate, :h_wfname, :h_wrcnt, :h_wramt, 'G'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 51); + Bchg(b, T_MSG, 0, "지급파일 헤더 생성 실패", 0L); + FAIL(b); + } + EXEC SQL UPDATE py_payout SET file_id = :h_wnew, status = 'F', + updated_at = now() + WHERE biz_date = :h_bizdate AND status = 'A' + AND account_no IN (SELECT account_no FROM py_account + WHERE bank_code = :h_key1); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 52); + Bchg(b, T_MSG, 0, "지급건 파일 편성 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s file=%ld 상세=%ld건 %ld원 반영=%ld", "PAY_BANKFILE", h_wnew, h_wrcnt, h_wramt, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_BANKFILE', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 당일 지급파일 전체 현황 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(total_amount),0) INTO :h_wa1, :h_wa2 + FROM py_file WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "당일 파일 현황 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE biz_date = :h_wdate AND status = 'F'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 편성 건수 초과: 기준=%ld 실적=%ld", "PAY_BANKFILE", h_wa1, h_wa3); + } + /* 8) 응답부 */ + setl(b, T_ID2, h_wnew); + setl(b, T_COUNT, h_wrcnt); + setl(b, T_GROSS, h_wramt); + Bchg(b, T_STR1, 0, h_wfname, 0L); + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + userlog("PAY_BANKFILE 완료: file=%ld 상세=%ld건 %ld원", h_wnew, h_wrcnt, h_wramt); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_BANKFILE", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_BANKVERIFY.pgc b/app/src/py/svc/PAY_BANKVERIFY.pgc index 8463d85..6b30f5b 100644 --- a/app/src/py/svc/PAY_BANKVERIFY.pgc +++ b/app/src/py/svc/PAY_BANKVERIFY.pgc @@ -1,7 +1,9 @@ /* - * PAY_BANKVERIFY.pgc - py 모듈 서비스 PAY_BANKVERIFY (one service = one file). + * PAY_BANKVERIFY.pgc - py (지급) 모듈 온라인 서비스 PAY_BANKVERIFY. * 지급파일 검증: 파일 헤더(record_count/total_amount)와 실제 편성 지급건의 * 건수/net 합을 대사해 일치 여부와 금액 차액(T_DELTA)을 회신한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [file]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_BANKVERIFY.h" @@ -10,28 +12,117 @@ void PAY_BANKVERIFY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - long h_fid, h_hcnt, h_hsum, h_acnt, h_asum; + long h_id2; + char h_wfst[8]; + long h_whcnt; + long h_whamt; + long h_wrcnt; + long h_wramt; + long h_waff; + long h_wnew; + char h_wfname[64]; + char h_wfdate[16]; + char h_wdate[16]; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_fid = getl(b, T_ID2); - EXEC SQL SELECT record_count, total_amount INTO :h_hcnt, :h_hsum - FROM py_file WHERE file_id = :h_fid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_acnt, :h_asum - FROM py_payout WHERE file_id = :h_fid; - if (sqlca.sqlcode < 0) FAIL(b); - - setl(b, T_AMT1, h_hsum); - setl(b, T_AMT2, h_asum); - setl(b, T_COUNT, h_acnt); - setl(b, T_DELTA, h_hsum - h_asum); - if (h_hcnt == h_acnt && h_hsum == h_asum) { - setl(b, T_RC, 0); - Bchg(b, T_MSG, 0, "파일 대사 일치", 0L); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id2 = getl(b, T_ID2); + if (h_id2 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + /* 2) 마스터 조회부 - 지급파일 헤더 + 실제 편성 상세 대사 */ + EXEC SQL SELECT status, record_count, total_amount, + to_char(biz_date, 'YYYY-MM-DD'), file_name + INTO :h_wfst, :h_whcnt, :h_whamt, :h_wfdate, :h_wfname + FROM py_file WHERE file_id = :h_id2; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "지급파일 헤더가 존재하지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급파일 헤더 조회 오류", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wrcnt, :h_wramt + FROM py_payout WHERE file_id = :h_id2; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "편성 상세 집계 오류", 0L); + FAIL(b); + } + userlog("PAY_BANKVERIFY 진입: file=%ld status=%s 헤더=%ld/%ld 상세=%ld/%ld", + h_id2, h_wfst, h_whcnt, h_whamt, h_wrcnt, h_wramt); + /* 3) 업무규칙 분기부 - 파일 상태/정합 판정 (3분기) */ + if (h_wrcnt == 0) { + setl(b, T_RC, 43); + Bchg(b, T_MSG, 0, "파일에 편성된 지급 상세가 없음", 0L); + FAIL(b); + } else if (h_whcnt != h_wrcnt || h_whamt != h_wramt) { + setl(b, T_RC, 5); Bchg(b, T_MSG, 0, "헤더-상세 건수/금액 불일치", 0L); } else { - setl(b, T_RC, 1); - Bchg(b, T_MSG, 0, "파일 대사 불일치", 0L); - userlog("PAY_BANKVERIFY 불일치 file=%ld hcnt=%ld acnt=%ld", h_fid, h_hcnt, h_acnt); + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "헤더-상세 정합 일치", 0L); + } + h_waff = h_wrcnt; + h_wnew = h_id2; + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s file=%ld 상세=%ld건 %ld원 반영=%ld", "PAY_BANKVERIFY", h_wnew, h_wrcnt, h_wramt, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_BANKVERIFY', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 당일 지급파일 전체 현황 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(total_amount),0) INTO :h_wa1, :h_wa2 + FROM py_file WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "당일 파일 현황 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE biz_date = :h_wdate AND status = 'F'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 편성 건수 초과: 기준=%ld 실적=%ld", "PAY_BANKVERIFY", h_wa1, h_wa3); + } + /* 8) 응답부 */ + setl(b, T_COUNT, h_wrcnt); + setl(b, T_AMT1, h_wramt); + setl(b, T_AMT2, h_whamt); + setl(b, T_DELTA, h_whamt - h_wramt); + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + userlog("PAY_BANKVERIFY 완료: file=%ld 상세=%ld건 %ld원", h_wnew, h_wrcnt, h_wramt); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_BANKVERIFY", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); } OK(b); } diff --git a/app/src/py/svc/PAY_BULK.pgc b/app/src/py/svc/PAY_BULK.pgc index 1525c6d..640ebe9 100644 --- a/app/src/py/svc/PAY_BULK.pgc +++ b/app/src/py/svc/PAY_BULK.pgc @@ -1,26 +1,230 @@ /* - * PAY_BULK.pgc - py 모듈 서비스 PAY_BULK (one service = one file). + * PAY_BULK.pgc - py (지급) 모듈 온라인 서비스 PAY_BULK. * 대량 지급 생성: 동일 금액의 지급건(BULK 채널)을 N건 반복 등록. - * Moved verbatim from the former inline py_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by py_svr at tpsvrinit. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [mkpay]. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_BULK.h" void PAY_BULK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], acct[32], bizdate[16]; - long n, amount, i, payout_id; - gets_(b, T_MERCHANT, merch, sizeof(merch)); - gets_(b, T_KEY1, acct, sizeof(acct)); - amount = getl(b, T_AMOUNT); - n = getl(b, T_COUNT); if (n < 1) n = 1; - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - for (i = 0; i < n; i++) { - payout_id = pydb_next_payout_id(); - if (payout_id < 0) FAIL(b); - if (pydb_insert_payout(payout_id, merch, acct, amount, 0, amount, "R", "BULK", bizdate) < 0) FAIL(b); + int gi; + EXEC SQL BEGIN DECLARE SECTION; + long h_amount; + char h_bizdate[64]; + long h_count; + char h_key1[64]; + char h_merchant[64]; + long h_wmdr; + char h_wmst[16]; + long h_wlim; + char h_wast[16]; + long h_wbal; + long h_wfee; + long h_wnet; + long h_wpart; + long h_wnum; + long h_wid; + long h_wlast; + long h_wused; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; + EXEC SQL END DECLARE SECTION; + + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_amount = getl(b, T_AMOUNT); + if (h_amount < 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "금액 항목은 음수가 될 수 없음", 0L); + FAIL(b); + } + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + h_count = getl(b, T_COUNT); + if (h_count < 1) h_count = 1; + if (h_count > 999) { + setl(b, T_RC, 13); + Bchg(b, T_MSG, 0, "분할/반복 건수 상한(999) 초과", 0L); + FAIL(b); + } + gets_(b, T_KEY1, h_key1, sizeof(h_key1)); + if (h_key1[0] == 0 || strlen(h_key1) < 3) { + setl(b, T_RC, 14); + Bchg(b, T_MSG, 0, "계좌번호/키 값이 유효하지 않음", 0L); + FAIL(b); + } + gets_(b, T_MERCHANT, h_merchant, sizeof(h_merchant)); + if (h_merchant[0] == 0) { + setl(b, T_RC, 15); + Bchg(b, T_MSG, 0, "가맹점 ID는 필수 입력", 0L); + FAIL(b); + } + /* 2) 마스터 조회부 - 가맹점 계약요율/상태/일한도 */ + EXEC SQL SELECT mdr_bps, status, daily_limit INTO :h_wmdr, :h_wmst, :h_wlim + FROM merchant WHERE merchant_id = :h_merchant; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "등록되지 않은 가맹점", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); + FAIL(b); + } + if (strcmp(h_wmst, "ACTIVE") != 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "비활성/폐업 가맹점은 지급 생성 불가", 0L); + FAIL(b); + } + /* 지급계좌 상태/잔액 확인 */ + EXEC SQL SELECT status, balance INTO :h_wast, :h_wbal + FROM py_account WHERE account_no = :h_key1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 43); + Bchg(b, T_MSG, 0, "지급계좌 미등록", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 44); + Bchg(b, T_MSG, 0, "지급계좌 조회 오류", 0L); + FAIL(b); + } + if (strcmp(h_wast, "ACTIVE") != 0) { + setl(b, T_RC, 45); + Bchg(b, T_MSG, 0, "정상 상태 계좌가 아니어서 지급 불가", 0L); + FAIL(b); + } + /* 당일 기 사용액 조회 - 일한도 통제용 */ + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_wused FROM py_payout + WHERE merchant_id = :h_merchant AND biz_date = :h_bizdate + AND status IN ('A','F','P'); + if (sqlca.sqlcode < 0) h_wused = 0; + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + userlog("PAY_BULK 진입: merch=%s amt=%ld mdr=%ldbps 기사용=%ld", + h_merchant, h_amount, h_wmdr, h_wused); + /* 3) 업무규칙 분기부 - 금액 구간별 수수료 산정 (3분기) */ + if (h_amount < 100000) { + h_wfee = acq_fee(h_amount, h_wmdr); + } else if (h_amount < 10000000) { + h_wfee = acq_fee(h_amount, h_wmdr) + acq_vat(acq_fee(h_amount, h_wmdr)); + } else { + h_wfee = acq_fee(h_amount, h_wmdr) - acq_wht_amt(acq_fee(h_amount, h_wmdr), 330); + } + if (h_wfee < 0) h_wfee = 0; + h_wnet = h_amount - h_wfee; + if (h_wnet < 0) h_wnet = 0; + /* 일한도 통제 */ + if (h_wlim > 0 && h_wused + h_wnet > h_wlim) { + setl(b, T_RC, 8); + Bchg(b, T_MSG, 0, "가맹점 일 지급한도 초과", 0L); + FAIL(b); + } + strcpy(h_pval, "500000000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_bulk_single_max'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 500000000; + if (h_amount > h_thr) { + setl(b, T_RC, 9); + Bchg(b, T_MSG, 0, "1건 지급 상한 정책 초과", 0L); + FAIL(b); + } + h_wnum = h_count; + h_wpart = h_amount / h_wnum; + if (h_wpart <= 0) { + setl(b, T_RC, 10); + Bchg(b, T_MSG, 0, "분할 후 건당 금액이 0 - 분할 건수 과다", 0L); + FAIL(b); + } + /* 4) 본처리부 - 지급건 생성 (분할 시 반복 채번) */ + h_wlast = 0; + for (gi = 0; gi < (int)h_wnum; gi++) { + EXEC SQL SELECT nextval('py_payout_seq') INTO :h_wid; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); Bchg(b, T_MSG, 0, "지급 일련번호 채번 실패", 0L); FAIL(b); + } + EXEC SQL INSERT INTO py_payout + (payout_id, merchant_id, account_no, amount, fee, net, + status, channel, biz_date) + VALUES (:h_wid, :h_merchant, :h_key1, :h_wpart, :h_wfee, + :h_wpart, 'R', 'BULK', :h_bizdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 51); Bchg(b, T_MSG, 0, "지급건 생성 INSERT 실패", 0L); FAIL(b); + } + h_wlast = h_wid; + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s merch=%s 생성=%ld건 총액=%ld 수수료=%ld", "PAY_BULK", h_merchant, h_wnum, h_amount, h_wfee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_BULK', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계 갱신부 - 가맹점 일별 지급 집계 upsert */ + if (pydb_upsert_summary(h_merchant, h_bizdate, h_wnum, h_amount) < 0) { + setl(b, T_RC, 60); + Bchg(b, T_MSG, 0, "가맹점 지급집계(py_summary) 갱신 실패", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 당일 채널별 지급 생성 누계 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_merchant + AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "당일 지급 생성 누계 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE merchant_id = :h_merchant AND biz_date = :h_bizdate + AND status = 'R'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 요청 잔량 누적: 기준=%ld 실적=%ld", "PAY_BULK", h_wa1, h_wa3); + } + /* 8) 응답부 */ + setl(b, T_RC, 0); + setl(b, T_COUNT, h_wnum); + Bchg(b, T_MSG, 0, "지급건 생성 완료", 0L); + userlog("PAY_BULK 완료: last=%ld 건수=%ld 순액=%ld", h_wlast, h_wnum, h_wnet); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_BULK", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); } - setl(b, T_COUNT, n); OK(b); } diff --git a/app/src/py/svc/PAY_CANCEL.pgc b/app/src/py/svc/PAY_CANCEL.pgc index a15feb8..b59c995 100644 --- a/app/src/py/svc/PAY_CANCEL.pgc +++ b/app/src/py/svc/PAY_CANCEL.pgc @@ -1,24 +1,177 @@ /* - * PAY_CANCEL.pgc - py 모듈 서비스 PAY_CANCEL (one service = one file). + * PAY_CANCEL.pgc - py (지급) 모듈 온라인 서비스 PAY_CANCEL. * 지급 취소: 취소 조정(-amount)을 py_adjust 에 기록하고 지급건 상태 C 로 전이. - * Moved verbatim from the former inline py_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by py_svr at tpsvrinit. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [txn]. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_CANCEL.h" void PAY_CANCEL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], reason[128]; - long payout_id, amt, adj; - payout_id = getl(b, T_ID1); - amt = getl(b, T_AMOUNT); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "CANCEL"); - adj = pydb_insert_adjust(payout_id, -amt, reason, bizdate); - if (adj < 0) FAIL(b); - if (pydb_update_status(payout_id, "C") < 0) FAIL(b); - setl(b, T_ADJ, adj); + EXEC SQL BEGIN DECLARE SECTION; + long h_amount; + char h_bizdate[64]; + long h_id1; + char h_reason[64]; + char h_wst[8]; + long h_wamt; + long h_wnet; + char h_wchan[16]; + long h_wretry; + long h_waff; + char h_wmerch[64]; + char h_wacct[64]; + long h_wcnt; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; + EXEC SQL END DECLARE SECTION; + + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_amount = getl(b, T_AMOUNT); + if (h_amount < 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "금액 항목은 음수가 될 수 없음", 0L); + FAIL(b); + } + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 13); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + gets_(b, T_REASON, h_reason, sizeof(h_reason)); + if (h_reason[0] == 0) strncpy(h_reason, "사유미기재", sizeof(h_reason)-1); + /* 2) 마스터 조회부 - 지급건 현재 상태/금액/채널/재시도 이력 */ + EXEC SQL SELECT status, amount, net, channel, retry_count, merchant_id, account_no + INTO :h_wst, :h_wamt, :h_wnet, :h_wchan, :h_wretry, :h_wmerch, :h_wacct + FROM py_payout WHERE payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 지급건이 존재하지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급건 조회 중 DB 오류", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + userlog("PAY_CANCEL 진입: payout=%ld status=%s amt=%ld chan=%s", + h_id1, h_wst, h_wamt, h_wchan); + /* 3) 업무규칙 분기부 - 상태 전이 가능성/한도/재시도 판정 */ + if (strcmp(h_wst, "P") == 0 || strcmp(h_wst, "X") == 0) { + setl(b, T_RC, 2); Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "이미 종결(지급완료/마감)된 건은 지급 취소 불가", 0L); FAIL(b); + } + if (strcmp(h_wst, "R") != 0) { + setl(b, T_RC, 3); Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "현재 상태에서는 지급 취소 전이가 허용되지 않음", 0L); FAIL(b); + } + if (strcmp(h_wchan, "ADVANCE") == 0 && h_wnet > 50000000) { + userlog("PAY_CANCEL 고액 선지급 건 주의: payout=%ld net=%ld", h_id1, h_wnet); + } + strcpy(h_pval, "100000000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_cancel_amt_max'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 100000000; + if (h_wamt > h_thr) { + userlog("PAY_CANCEL 정책 상한 초과: amt=%ld thr=%ld", h_wamt, h_thr); + } + /* 4) 본처리부 - 가드 조건부 상태 전이 UPDATE */ + EXEC SQL UPDATE py_payout SET status = 'C', updated_at = now() + WHERE payout_id = :h_id1 AND status = 'R'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "지급 취소 상태 갱신 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "동시성 충돌 - 전이 대상 건이 없음", 0L); + FAIL(b); + } + EXEC SQL UPDATE py_payout SET reject_reason = :h_reason, updated_at = now() + WHERE payout_id = :h_id1; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 51); + Bchg(b, T_MSG, 0, "처리 사유 기록 실패", 0L); + FAIL(b); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s payout=%ld %s->%s amt=%ld aff=%ld", "PAY_CANCEL", h_id1, "R", "C", h_wamt, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_CANCEL', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계 재산정부 - 당일 동일 상태 건수 재집계 */ + EXEC SQL SELECT count(*) INTO :h_wcnt FROM py_payout + WHERE biz_date = :h_wdate AND status = 'C'; + if (sqlca.sqlcode < 0) h_wcnt = 0; + /* 6) 집계/요약 갱신부 - 가맹점 당일 확정성 지급 실적 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_wmerch + AND biz_date = :h_wdate AND status IN ('A','F','P'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "가맹점 지급 실적 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE merchant_id = :h_wmerch AND biz_date = :h_wdate + AND status IN ('R','H'); + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 미지급 잔량 과다: 기준=%ld 실적=%ld", "PAY_CANCEL", h_wa1, h_wa3); + } + /* 8) 응답부 */ + setl(b, T_RC, 0); Bchg(b, T_STATUS, 0, "C", 0L); + setl(b, T_ADJ, h_waff); + Bchg(b, T_MSG, 0, "지급 취소 정상 처리", 0L); + userlog("PAY_CANCEL 완료: payout=%ld -> %s (당일 %s상태 %ld건)", + h_id1, "C", "C", h_wcnt); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_CANCEL", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_CANCELHIST.pgc b/app/src/py/svc/PAY_CANCELHIST.pgc index b8fa8f9..bccc93f 100644 --- a/app/src/py/svc/PAY_CANCELHIST.pgc +++ b/app/src/py/svc/PAY_CANCELHIST.pgc @@ -1,7 +1,9 @@ /* - * PAY_CANCELHIST.pgc - py 모듈 서비스 PAY_CANCELHIST (one service = one file). + * PAY_CANCELHIST.pgc - py (지급) 모듈 온라인 서비스 PAY_CANCELHIST. * 취소/환입 이력 집계: 가맹점 당일 취소(C)와 역분개/환입(V)을 각각 * 건수/금액으로 나눠 한 번의 CASE 집계로 회신한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [cursor]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_CANCELHIST.h" @@ -9,27 +11,134 @@ void PAY_CANCELHIST(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + int gi = 0; + long tot_cnt = 0, tot_sum = 0, tot_fee = 0, top_cnt = 0, top_sum = 0; + char top_grp[64]; EXEC SQL BEGIN DECLARE SECTION; - char h_merch[64], h_bizdate[16]; - long h_ccnt, h_camt, h_vcnt, h_vamt; + char h_bizdate[64]; + char h_merchant[64]; + char h_wgrp[64]; + long h_wgcnt; + long h_wgsum; + long h_wgfee; + long h_wtot; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT coalesce(sum(CASE WHEN status = 'C' THEN 1 ELSE 0 END),0), - coalesce(sum(CASE WHEN status = 'C' THEN net ELSE 0 END),0), - coalesce(sum(CASE WHEN status = 'V' THEN 1 ELSE 0 END),0), - coalesce(sum(CASE WHEN status = 'V' THEN net ELSE 0 END),0) - INTO :h_ccnt, :h_camt, :h_vcnt, :h_vamt - FROM py_payout - WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - - setl(b, T_ARG1, h_ccnt); - setl(b, T_AMT1, h_camt); - setl(b, T_ARG2, h_vcnt); - setl(b, T_AMT2, h_vamt); - setl(b, T_COUNT, h_ccnt + h_vcnt); - setl(b, T_RC, 0); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + gets_(b, T_MERCHANT, h_merchant, sizeof(h_merchant)); + if (h_merchant[0] == 0) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "가맹점 ID는 필수 입력", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + top_grp[0] = 0; + /* 2) 사전 규모 확인 - 커서 오픈 전 총건수 파악 */ + EXEC SQL SELECT count(*) INTO :h_wtot FROM py_payout p WHERE p.merchant_id = :h_merchant AND p.biz_date = :h_bizdate AND p.status IN ('C','V'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "집계 사전 조회 실패", 0L); + FAIL(b); + } + userlog("PAY_CANCELHIST 집계 시작: 기준일=%s 대상=%ld건 (취소구분별)", h_wdate, h_wtot); + /* 커서 정의: 취소구분별 건수/금액 집계 */ + EXEC SQL DECLARE cur_pay_cancelhist CURSOR FOR + SELECT p.status, count(*), coalesce(sum(p.net),0), coalesce(sum(p.fee),0) + FROM py_payout p WHERE p.merchant_id = :h_merchant AND p.biz_date = :h_bizdate AND p.status IN ('C','V') + GROUP BY p.status ORDER BY 2 DESC, 1; + EXEC SQL OPEN cur_pay_cancelhist; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "집계 커서 OPEN 실패", 0L); + FAIL(b); + } + /* 3) 본처리부 - 커서 순회하며 그룹별 집계/상위 추출 */ + for (;;) { + EXEC SQL FETCH cur_pay_cancelhist INTO :h_wgrp, :h_wgcnt, :h_wgsum, :h_wgfee; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { + EXEC SQL CLOSE cur_pay_cancelhist; + setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "집계 FETCH 오류", 0L); FAIL(b); + } + tot_cnt += h_wgcnt; + tot_sum += h_wgsum; + tot_fee += h_wgfee; + if (h_wgcnt > top_cnt) { + top_cnt = h_wgcnt; + top_sum = h_wgsum; + strncpy(top_grp, h_wgrp, sizeof(top_grp)-1); + top_grp[sizeof(top_grp)-1] = 0; + } + if (gi < 10) { + Bchg(b, T_STR1, gi, h_wgrp, 0L); + Bchg(b, T_AMT1, gi, (char *)&h_wgsum, 0L); + Bchg(b, T_ARG1, gi, (char *)&h_wgcnt, 0L); + gi++; + } + } + EXEC SQL CLOSE cur_pay_cancelhist; + strcpy(h_pval, "5000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_cancelhist_concentration'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 5000; + /* 업무규칙 분기부 - 분포 집중도 판정 (3분기) */ + if (gi == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "취소구분별 집계 대상 없음", 0L); + } else if (tot_cnt > 0 && top_cnt * 10000 / tot_cnt > h_thr) { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "특정 취소구분 집중 - 확인 필요", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "취소구분별 정상 분포", 0L); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s %s별 그룹=%d 건수=%ld 금액=%ld 최다=%s", "PAY_CANCELHIST", "취소구분", gi, tot_cnt, tot_sum, top_grp); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_CANCELHIST', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 8) 응답부 */ + setl(b, T_COUNT, tot_cnt); + setl(b, T_AMT2, top_sum); + setl(b, T_ARG2, (long)gi); + userlog("PAY_CANCELHIST 완료: 그룹=%d 총건=%ld 총액=%ld 최다=%s", + gi, tot_cnt, tot_sum, top_grp); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_CANCELHIST", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_CARRYOVER.pgc b/app/src/py/svc/PAY_CARRYOVER.pgc index 7b5575d..5a59b49 100644 --- a/app/src/py/svc/PAY_CARRYOVER.pgc +++ b/app/src/py/svc/PAY_CARRYOVER.pgc @@ -1,7 +1,9 @@ /* - * PAY_CARRYOVER.pgc - py 모듈 서비스 PAY_CARRYOVER (one service = one file). + * PAY_CARRYOVER.pgc - py (지급) 모듈 온라인 서비스 PAY_CARRYOVER. * 지급 이월(단건): 미지급(R/A) 건의 지급기준일(biz_date)을 지정 차기일자로 * 이월하고, 이월 사유를 py_adjust 에 0원 조정으로 증적으로 남긴다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [batch]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_CARRYOVER.h" @@ -9,32 +11,155 @@ void PAY_CARRYOVER(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char nextdate[16], reason[96]; - long adj; EXEC SQL BEGIN DECLARE SECTION; - long h_pid; - char h_st[4], h_next[16]; + long h_id1; + char h_key2[64]; + char h_reason[64]; + long h_wcnt; + long h_wsum; + long h_waff; + long h_wmax; + char h_wnext[16]; + char h_wsrc[16]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_ID1); - gets_(b, T_KEY2, nextdate, sizeof(nextdate)); - gets_(b, T_REASON, reason, sizeof(reason)); - if (nextdate[0] == 0) { setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "이월일자 미지정", 0L); FAIL(b); } - if (reason[0] == 0) strcpy(reason, "지급이월"); - strncpy(h_next, nextdate, sizeof(h_next)-1); h_next[sizeof(h_next)-1] = 0; - - EXEC SQL SELECT status INTO :h_st FROM py_payout WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - if (h_st[0] != 'R' && h_st[0] != 'A') { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "미지급 건만 이월 가능", 0L); FAIL(b); } - - EXEC SQL UPDATE py_payout SET biz_date = :h_next, updated_at = now() WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - adj = pydb_insert_adjust(h_pid, 0, reason, nextdate); - if (adj < 0) FAIL(b); - + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + gets_(b, T_KEY2, h_key2, sizeof(h_key2)); + if (h_key2[0] == 0 || strlen(h_key2) < 3) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "계좌번호/키 값이 유효하지 않음", 0L); + FAIL(b); + } + gets_(b, T_REASON, h_reason, sizeof(h_reason)); + if (h_reason[0] == 0) strncpy(h_reason, "사유미기재", sizeof(h_reason)-1); + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + /* 2) 마스터 조회부 - 일괄 처리 대상 규모 사전 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wcnt, :h_wsum + FROM py_payout WHERE payout_id = :h_id1 AND status IN ('R','A'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "일괄 처리 대상 집계 오류", 0L); + FAIL(b); + } + /* 당일 전체 지급건 대비 비중 확인 */ + EXEC SQL SELECT count(*) INTO :h_wmax FROM py_payout + WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) h_wmax = 0; + userlog("PAY_CARRYOVER 진입: 지급 이월 대상=%ld건 금액=%ld (전체=%ld)", + h_wcnt, h_wsum, h_wmax); + /* 3) 업무규칙 분기부 - 처리 대상/규모 판정 (3분기) */ + if (h_wcnt == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "지급 이월 대상이 없음", 0L); + setl(b, T_COUNT, 0); + OK(b); + } + strcpy(h_pval, "100000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_carryover_max_rows'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 100000; + if (h_wcnt > h_thr) { + setl(b, T_RC, 30); + Bchg(b, T_MSG, 0, "일괄 처리 건수 상한 초과 - 분할 실행 필요", 0L); + FAIL(b); + } + if (h_wmax > 0 && h_wcnt * 2 > h_wmax) { + userlog("PAY_CARRYOVER 대상 비중 과다 경고: %ld/%ld", h_wcnt, h_wmax); + } + /* 4) 본처리부 - 일괄 상태 전이 */ + EXEC SQL SELECT to_char(biz_date, 'YYYY-MM-DD') INTO :h_wsrc + FROM py_payout WHERE payout_id = :h_id1; + if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) strcpy(h_wsrc, h_wdate); + strncpy(h_wnext, h_key2, sizeof(h_wnext)-1); + h_wnext[sizeof(h_wnext)-1] = 0; + if (strlen(h_wnext) != 10) { + if (acq_next_bizday(h_wsrc, h_wnext) < 0) { + setl(b, T_RC, 31); + Bchg(b, T_MSG, 0, "차기 영업일 산출 실패", 0L); + FAIL(b); + } + } + EXEC SQL UPDATE py_payout SET biz_date = :h_wnext, updated_at = now() + WHERE payout_id = :h_id1 AND status IN ('R','A'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "지급 이월 일괄 전이 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "일괄 처리 반영 건수가 0 - 선행 상태 변동", 0L); + FAIL(b); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s %s 대상=%ld 반영=%ld 금액=%ld", "PAY_CARRYOVER", "지급 이월", h_wcnt, h_waff, h_wsum); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_CARRYOVER', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 일괄 처리 후 잔여 대상 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE biz_date = :h_wdate AND status IN ('R','A'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "잔여 대상 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE biz_date = :h_wdate AND status = 'H'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 보류 잔량 증가: 기준=%ld 실적=%ld", "PAY_CARRYOVER", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - setl(b, T_ADJ, adj); - Bchg(b, T_STR1, 0, nextdate, 0L); - userlog("PAY_CARRYOVER id=%ld to=%s", h_pid, nextdate); + setl(b, T_COUNT, h_waff); + setl(b, T_ADJ, h_waff); + Bchg(b, T_STR1, 0, h_wnext, 0L); + Bchg(b, T_MSG, 0, "지급 이월 일괄 처리 완료", 0L); + userlog("PAY_CARRYOVER 완료: 반영=%ld건 금액=%ld", h_waff, h_wsum); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_CARRYOVER", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_CHARGEBACK.pgc b/app/src/py/svc/PAY_CHARGEBACK.pgc index 63d0735..92776d4 100644 --- a/app/src/py/svc/PAY_CHARGEBACK.pgc +++ b/app/src/py/svc/PAY_CHARGEBACK.pgc @@ -1,7 +1,9 @@ /* - * PAY_CHARGEBACK.pgc - py 모듈 서비스 PAY_CHARGEBACK (one service = one file). + * PAY_CHARGEBACK.pgc - py (지급) 모듈 온라인 서비스 PAY_CHARGEBACK. * 지급 되물림: 분쟁 등으로 지급완료(P) 건을 되물릴 때, 원건은 보존한 채 * 음수 금액의 상계 지급건(CHGBK 채널)을 신규 생성해 상쇄 처리한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [derive]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_CHARGEBACK.h" @@ -9,28 +11,172 @@ void PAY_CHARGEBACK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long newid, neg; + int gi; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_net; - char h_merch[64], h_acct[32], h_st[4]; + char h_bizdate[64]; + long h_id1; + char h_wst[8]; + long h_wamt; + long h_wfee; + long h_wnet; + char h_wmerch[64]; + char h_wacct[64]; + long h_wid; + long h_wlast; + long h_wcnt; + long h_wsum; + long h_waff; + long h_wpart; + long h_wrem; + char h_wdt[16]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_ID1); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - EXEC SQL SELECT merchant_id, account_no, net, status - INTO :h_merch, :h_acct, :h_net, :h_st FROM py_payout WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - if (h_st[0] != 'P') { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "지급완료 건만 되물림 가능", 0L); FAIL(b); } - - neg = -h_net; - newid = pydb_next_payout_id(); - if (newid < 0) FAIL(b); - if (pydb_insert_payout(newid, h_merch, h_acct, neg, 0, neg, "R", "CHGBK", bizdate) < 0) FAIL(b); - + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + /* 2) 마스터 조회부 - 원천 지급건 상세 */ + EXEC SQL SELECT status, amount, fee, net, merchant_id, account_no, biz_date + INTO :h_wst, :h_wamt, :h_wfee, :h_wnet, :h_wmerch, :h_wacct, :h_wdt + FROM py_payout WHERE payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "원천 지급건이 존재하지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "원천 지급건 조회 오류", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + userlog("PAY_CHARGEBACK 진입: merch=%s amt=%ld net=%ld", h_wmerch, h_wamt, h_wnet); + /* 3) 업무규칙 분기부 - 파생 생성 적법성 판정 (3분기) */ + if (strcmp(h_wst, "P") != 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "지급완료(P) 건만 되물림 가능", 0L); + FAIL(b); + } + if (h_wnet <= 0) { + setl(b, T_RC, 43); + Bchg(b, T_MSG, 0, "되물림 대상 금액 없음", 0L); + FAIL(b); + } + h_wcnt = 1; + h_wpart = -h_wnet; + h_wrem = 0; + if (h_wpart >= 0) { + setl(b, T_RC, 44); + Bchg(b, T_MSG, 0, "되물림 금액 산정 오류", 0L); + FAIL(b); + } + strcpy(h_pval, "999"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_chargeback_max_child'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 999; + if (h_wcnt > h_thr) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "생성 건수 정책 상한 초과", 0L); + FAIL(b); + } + /* 4) 본처리부 - 파생 지급건 생성 */ + h_wlast = 0; + EXEC SQL SELECT nextval('py_payout_seq') INTO :h_wid; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "되물림건 채번 실패", 0L); + FAIL(b); + } + EXEC SQL INSERT INTO py_payout + (payout_id, merchant_id, account_no, amount, fee, net, + status, channel, biz_date, reject_reason) + VALUES (:h_wid, :h_wmerch, :h_wacct, :h_wpart, 0, + :h_wpart, 'R', 'STD', :h_bizdate, '되물림'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 51); + Bchg(b, T_MSG, 0, "되물림 지급건 생성 실패", 0L); + FAIL(b); + } + h_wlast = h_wid; + /* 되물림 원장 기표 */ + EXEC SQL INSERT INTO py_ledger (ledger_id, payout_id, entry_type, amount, biz_date) + VALUES (nextval('py_ledger_seq'), :h_id1, 'CHARGEBACK', :h_wpart, :h_bizdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 52); + Bchg(b, T_MSG, 0, "되물림 원장 기표 실패", 0L); + FAIL(b); + } + h_waff = 1; + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s 원천=%ld 생성=%ld건 last=%ld 대체=%ld", "PAY_CHARGEBACK", (long)(h_wcnt), h_wcnt, h_wlast, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_CHARGEBACK', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 파생 생성 후 가맹점 당일 지급 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_wmerch AND status = 'R'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "파생 생성 후 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE merchant_id = :h_wmerch AND status = 'C'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 취소 대체 건 누적: 기준=%ld 실적=%ld", "PAY_CHARGEBACK", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - setl(b, T_ID2, newid); - setl(b, T_NET, neg); - userlog("PAY_CHARGEBACK src=%ld offset=%ld net=%ld", h_pid, newid, neg); + setl(b, T_ID2, h_wlast); + setl(b, T_NET, h_wpart); + Bchg(b, T_MSG, 0, "파생 지급건 생성 완료", 0L); + userlog("PAY_CHARGEBACK 완료: 생성=%ld건 last=%ld 대체=%ld건", h_wcnt, h_wlast, h_waff); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_CHARGEBACK", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_CLOSE.pgc b/app/src/py/svc/PAY_CLOSE.pgc index 1f57f10..5d60f17 100644 --- a/app/src/py/svc/PAY_CLOSE.pgc +++ b/app/src/py/svc/PAY_CLOSE.pgc @@ -1,25 +1,139 @@ /* - * PAY_CLOSE.pgc - py 모듈 서비스 PAY_CLOSE (one service = one file). + * PAY_CLOSE.pgc - py (지급) 모듈 온라인 서비스 PAY_CLOSE. * 일마감: 당일 지급완료(P)건을 마감(X)으로 일괄 전환하고 전환 건수를 회신. - * Moved verbatim from the former inline py_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by py_svr at tpsvrinit. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [batch]. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_CLOSE.h" void PAY_CLOSE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_cnt; + char h_bizdate[64]; + long h_wcnt; + long h_wsum; + long h_waff; + long h_wmax; + char h_wnext[16]; + char h_wsrc[16]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; 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; + + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + /* 2) 마스터 조회부 - 일괄 처리 대상 규모 사전 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wcnt, :h_wsum + FROM py_payout WHERE biz_date = :h_bizdate AND status = 'P'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "일괄 처리 대상 집계 오류", 0L); + FAIL(b); + } + /* 당일 전체 지급건 대비 비중 확인 */ + EXEC SQL SELECT count(*) INTO :h_wmax FROM py_payout + WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) h_wmax = 0; + userlog("PAY_CLOSE 진입: 일마감 대상=%ld건 금액=%ld (전체=%ld)", + h_wcnt, h_wsum, h_wmax); + /* 3) 업무규칙 분기부 - 처리 대상/규모 판정 (3분기) */ + if (h_wcnt == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "일마감 대상이 없음", 0L); + setl(b, T_COUNT, 0); + OK(b); + } + strcpy(h_pval, "100000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_close_max_rows'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 100000; + if (h_wcnt > h_thr) { + setl(b, T_RC, 30); + Bchg(b, T_MSG, 0, "일괄 처리 건수 상한 초과 - 분할 실행 필요", 0L); + FAIL(b); + } + if (h_wmax > 0 && h_wcnt * 2 > h_wmax) { + userlog("PAY_CLOSE 대상 비중 과다 경고: %ld/%ld", h_wcnt, h_wmax); + } + /* 4) 본처리부 - 일괄 상태 전이 */ EXEC SQL UPDATE py_payout SET status = 'X', updated_at = now() WHERE biz_date = :h_bizdate AND status = 'P'; - if (sqlca.sqlcode < 0) FAIL(b); - h_cnt = sqlca.sqlerrd[2]; - setl(b, T_COUNT, h_cnt); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "일마감 일괄 전이 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "일괄 처리 반영 건수가 0 - 선행 상태 변동", 0L); + FAIL(b); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s %s 대상=%ld 반영=%ld 금액=%ld", "PAY_CLOSE", "일마감", h_wcnt, h_waff, h_wsum); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_CLOSE', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 일괄 처리 후 잔여 대상 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE biz_date = :h_wdate AND status IN ('R','A'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "잔여 대상 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE biz_date = :h_wdate AND status = 'H'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 보류 잔량 증가: 기준=%ld 실적=%ld", "PAY_CLOSE", h_wa1, h_wa3); + } + /* 8) 응답부 */ + setl(b, T_RC, 0); + setl(b, T_COUNT, h_waff); + Bchg(b, T_MSG, 0, "일마감 일괄 처리 완료", 0L); + userlog("PAY_CLOSE 완료: 반영=%ld건 금액=%ld", h_waff, h_wsum); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_CLOSE", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_CLOSEDSTOP.pgc b/app/src/py/svc/PAY_CLOSEDSTOP.pgc index b7faeed..1b63e33 100644 --- a/app/src/py/svc/PAY_CLOSEDSTOP.pgc +++ b/app/src/py/svc/PAY_CLOSEDSTOP.pgc @@ -1,7 +1,9 @@ /* - * PAY_CLOSEDSTOP.pgc - py 모듈 서비스 PAY_CLOSEDSTOP (one service = one file). + * PAY_CLOSEDSTOP.pgc - py (지급) 모듈 온라인 서비스 PAY_CLOSEDSTOP. * 폐업 가맹점 지급정지: 가맹점 상태가 CLOSED 인 가맹점의 미지급(R/A) 건을 * 모두 보류(H)+'폐업정지' 사유로 전이한다. 지급망 유출을 사전 차단한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [batch]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_CLOSEDSTOP.h" @@ -9,24 +11,134 @@ void PAY_CLOSEDSTOP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long stopped; EXEC SQL BEGIN DECLARE SECTION; - char h_merch[64]; - char h_mst[16]; + char h_merchant[64]; + long h_wcnt; + long h_wsum; + long h_waff; + long h_wmax; + char h_wnext[16]; + char h_wsrc[16]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); - EXEC SQL SELECT status INTO :h_mst FROM merchant WHERE merchant_id = :h_merch; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - if (strcmp(h_mst, "CLOSED") != 0) { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "폐업 가맹점 아님", 0L); FAIL(b); } - - EXEC SQL UPDATE py_payout SET status = 'H', reject_reason = '폐업정지', updated_at = now() - WHERE merchant_id = :h_merch AND status IN ('R','A'); - if (sqlca.sqlcode < 0) FAIL(b); - stopped = sqlca.sqlerrd[2]; - + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + gets_(b, T_MERCHANT, h_merchant, sizeof(h_merchant)); + if (h_merchant[0] == 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "가맹점 ID는 필수 입력", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + /* 2) 마스터 조회부 - 일괄 처리 대상 규모 사전 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wcnt, :h_wsum + FROM py_payout WHERE merchant_id = :h_merchant AND status IN ('R','A'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "일괄 처리 대상 집계 오류", 0L); + FAIL(b); + } + /* 당일 전체 지급건 대비 비중 확인 */ + EXEC SQL SELECT count(*) INTO :h_wmax FROM py_payout + WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) h_wmax = 0; + userlog("PAY_CLOSEDSTOP 진입: 폐업 지급정지 대상=%ld건 금액=%ld (전체=%ld)", + h_wcnt, h_wsum, h_wmax); + /* 3) 업무규칙 분기부 - 처리 대상/규모 판정 (3분기) */ + if (h_wcnt == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "폐업 지급정지 대상이 없음", 0L); + setl(b, T_COUNT, 0); + OK(b); + } + strcpy(h_pval, "100000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_closedstop_max_rows'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 100000; + if (h_wcnt > h_thr) { + setl(b, T_RC, 30); + Bchg(b, T_MSG, 0, "일괄 처리 건수 상한 초과 - 분할 실행 필요", 0L); + FAIL(b); + } + if (h_wmax > 0 && h_wcnt * 2 > h_wmax) { + userlog("PAY_CLOSEDSTOP 대상 비중 과다 경고: %ld/%ld", h_wcnt, h_wmax); + } + /* 4) 본처리부 - 일괄 상태 전이 */ + EXEC SQL UPDATE py_payout SET status = 'H', + reject_reason = '폐업정지', updated_at = now() + WHERE merchant_id = :h_merchant AND status IN ('R','A') + AND merchant_id IN (SELECT merchant_id FROM merchant + WHERE status = 'CLOSED'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "폐업 지급정지 일괄 전이 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "일괄 처리 반영 건수가 0 - 선행 상태 변동", 0L); + FAIL(b); + } + strcpy(h_lvl, "W"); + snprintf(h_amsg, sizeof(h_amsg), "%s %s 대상=%ld 반영=%ld 금액=%ld", "PAY_CLOSEDSTOP", "폐업 지급정지", h_wcnt, h_waff, h_wsum); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_CLOSEDSTOP', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 일괄 처리 후 잔여 대상 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE biz_date = :h_wdate AND status IN ('R','A'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "잔여 대상 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE biz_date = :h_wdate AND status = 'H'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 보류 잔량 증가: 기준=%ld 실적=%ld", "PAY_CLOSEDSTOP", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - setl(b, T_COUNT, stopped); - userlog("PAY_CLOSEDSTOP merch=%s stopped=%ld", h_merch, stopped); + setl(b, T_COUNT, h_waff); + Bchg(b, T_MSG, 0, "폐업 지급정지 일괄 처리 완료", 0L); + userlog("PAY_CLOSEDSTOP 완료: 반영=%ld건 금액=%ld", h_waff, h_wsum); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_CLOSEDSTOP", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_COMBINE.pgc b/app/src/py/svc/PAY_COMBINE.pgc index c1950ff..dd832d5 100644 --- a/app/src/py/svc/PAY_COMBINE.pgc +++ b/app/src/py/svc/PAY_COMBINE.pgc @@ -1,7 +1,9 @@ /* - * PAY_COMBINE.pgc - py 모듈 서비스 PAY_COMBINE (one service = one file). + * PAY_COMBINE.pgc - py (지급) 모듈 온라인 서비스 PAY_COMBINE. * 지급 합산: 같은 계좌의 당일 요청(R) 건들을 하나로 합산 지급한다. 합계 * 금액으로 신규 합산건(BULK)을 만들고 원건들은 취소(C) 처리한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [derive]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_COMBINE.h" @@ -9,34 +11,174 @@ void PAY_COMBINE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long newid; + int gi; EXEC SQL BEGIN DECLARE SECTION; - char h_acct[32], h_bizdate[16], h_merch[64]; - long h_cnt, h_sum; + char h_bizdate[64]; + char h_key1[64]; + char h_wst[8]; + long h_wamt; + long h_wfee; + long h_wnet; + char h_wmerch[64]; + char h_wacct[64]; + long h_wid; + long h_wlast; + long h_wcnt; + long h_wsum; + long h_waff; + long h_wpart; + long h_wrem; + char h_wdt[16]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, h_acct, sizeof(h_acct)); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - - EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(max(merchant_id),'') - INTO :h_cnt, :h_sum, :h_merch - FROM py_payout WHERE account_no = :h_acct AND biz_date = :h_bizdate AND status = 'R'; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_cnt < 2) { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "합산 대상(2건 이상) 없음", 0L); FAIL(b); } - - EXEC SQL UPDATE py_payout SET status = 'C', updated_at = now() - WHERE account_no = :h_acct AND biz_date = :h_bizdate AND status = 'R'; - if (sqlca.sqlcode < 0) FAIL(b); - newid = pydb_next_payout_id(); - if (newid < 0) FAIL(b); - if (pydb_insert_payout(newid, h_merch, h_acct, h_sum, 0, h_sum, "R", "BULK", bizdate) < 0) FAIL(b); - + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + gets_(b, T_KEY1, h_key1, sizeof(h_key1)); + if (h_key1[0] == 0 || strlen(h_key1) < 3) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "계좌번호/키 값이 유효하지 않음", 0L); + FAIL(b); + } + /* 2) 마스터 조회부 - 합산 대상 지급건 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0) + INTO :h_wcnt, :h_wamt, :h_wnet FROM py_payout + WHERE account_no = :h_key1 AND biz_date = :h_bizdate AND status = 'R'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "합산 대상 집계 오류", 0L); + FAIL(b); + } + strcpy(h_wst, "R"); + EXEC SQL SELECT coalesce(max(merchant_id),'') INTO :h_wmerch FROM py_payout + WHERE account_no = :h_key1 AND biz_date = :h_bizdate AND status = 'R'; + if (sqlca.sqlcode < 0) strcpy(h_wmerch, ""); + strncpy(h_wacct, h_key1, sizeof(h_wacct)-1); h_wacct[sizeof(h_wacct)-1] = 0; + strncpy(h_wdt, h_bizdate, sizeof(h_wdt)-1); h_wdt[sizeof(h_wdt)-1] = 0; + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + userlog("PAY_COMBINE 진입: merch=%s amt=%ld net=%ld", h_wmerch, h_wamt, h_wnet); + /* 3) 업무규칙 분기부 - 파생 생성 적법성 판정 (3분기) */ + if (h_wcnt == 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "합산 대상 지급건이 없음", 0L); + FAIL(b); + } + if (h_wcnt < 2) { + setl(b, T_RC, 43); + Bchg(b, T_MSG, 0, "합산은 2건 이상일 때만 의미가 있음", 0L); + FAIL(b); + } + h_wpart = h_wnet; + h_wrem = 0; + if (h_wpart <= 0) { + setl(b, T_RC, 44); + Bchg(b, T_MSG, 0, "합산 순액이 0 이하", 0L); + FAIL(b); + } + strcpy(h_pval, "999"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_combine_max_child'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 999; + if (h_wcnt > h_thr) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "생성 건수 정책 상한 초과", 0L); + FAIL(b); + } + /* 4) 본처리부 - 파생 지급건 생성 */ + h_wlast = 0; + EXEC SQL SELECT nextval('py_payout_seq') INTO :h_wid; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "합산건 채번 실패", 0L); + FAIL(b); + } + EXEC SQL INSERT INTO py_payout + (payout_id, merchant_id, account_no, amount, fee, net, + status, channel, biz_date) + VALUES (:h_wid, :h_wmerch, :h_wacct, :h_wamt, 0, + :h_wpart, 'R', 'STD', :h_bizdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 51); + Bchg(b, T_MSG, 0, "합산 지급건 생성 실패", 0L); + FAIL(b); + } + h_wlast = h_wid; + EXEC SQL UPDATE py_payout SET status = 'C', reject_reason = '합산대체', + updated_at = now() + WHERE account_no = :h_key1 AND biz_date = :h_bizdate + AND status = 'R' AND payout_id <> :h_wid; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 52); + Bchg(b, T_MSG, 0, "원 지급건 합산대체 처리 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s 원천=%ld 생성=%ld건 last=%ld 대체=%ld", "PAY_COMBINE", (long)(h_wcnt), h_wcnt, h_wlast, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_COMBINE', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 파생 생성 후 가맹점 당일 지급 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_wmerch AND status = 'R'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "파생 생성 후 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE merchant_id = :h_wmerch AND status = 'C'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 취소 대체 건 누적: 기준=%ld 실적=%ld", "PAY_COMBINE", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - setl(b, T_ID2, newid); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_sum); - userlog("PAY_COMBINE acct=%s merged=%ld sum=%ld new=%ld", h_acct, h_cnt, h_sum, newid); + setl(b, T_ID2, h_wlast); + setl(b, T_COUNT, h_wcnt); + setl(b, T_GROSS, h_wamt); + Bchg(b, T_MSG, 0, "파생 지급건 생성 완료", 0L); + userlog("PAY_COMBINE 완료: 생성=%ld건 last=%ld 대체=%ld건", h_wcnt, h_wlast, h_waff); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_COMBINE", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_CONFIRM.pgc b/app/src/py/svc/PAY_CONFIRM.pgc index d403992..574b289 100644 --- a/app/src/py/svc/PAY_CONFIRM.pgc +++ b/app/src/py/svc/PAY_CONFIRM.pgc @@ -1,8 +1,9 @@ /* - * PAY_CONFIRM.pgc - py 모듈 서비스 PAY_CONFIRM (one service = one file). + * PAY_CONFIRM.pgc - py (지급) 모듈 온라인 서비스 PAY_CONFIRM. * 지급 확정(confirm): 지급 파일 상태를 확정(C)으로 전이하고 확정 시각 기록. - * Moved verbatim from the former inline py_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by py_svr at tpsvrinit. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [file]. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_CONFIRM.h" @@ -10,10 +11,138 @@ void PAY_CONFIRM(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - long h_file; + long h_id2; + char h_wfst[8]; + long h_whcnt; + long h_whamt; + long h_wrcnt; + long h_wramt; + long h_waff; + long h_wnew; + char h_wfname[64]; + char h_wfdate[16]; + char h_wdate[16]; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_file = getl(b, T_ID2); - EXEC SQL UPDATE py_file SET status = 'C', confirmed_at = now() WHERE file_id = :h_file; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); + + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id2 = getl(b, T_ID2); + if (h_id2 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + /* 2) 마스터 조회부 - 지급파일 헤더 + 실제 편성 상세 대사 */ + EXEC SQL SELECT status, record_count, total_amount, + to_char(biz_date, 'YYYY-MM-DD'), file_name + INTO :h_wfst, :h_whcnt, :h_whamt, :h_wfdate, :h_wfname + FROM py_file WHERE file_id = :h_id2; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "지급파일 헤더가 존재하지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급파일 헤더 조회 오류", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wrcnt, :h_wramt + FROM py_payout WHERE file_id = :h_id2; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "편성 상세 집계 오류", 0L); + FAIL(b); + } + userlog("PAY_CONFIRM 진입: file=%ld status=%s 헤더=%ld/%ld 상세=%ld/%ld", + h_id2, h_wfst, h_whcnt, h_whamt, h_wrcnt, h_wramt); + /* 3) 업무규칙 분기부 - 파일 상태/정합 판정 (3분기) */ + if (strcmp(h_wfst, "C") == 0) { + setl(b, T_RC, 43); + Bchg(b, T_MSG, 0, "이미 확정된 지급파일", 0L); + FAIL(b); + } else if (strcmp(h_wfst, "G") != 0) { + setl(b, T_RC, 44); + Bchg(b, T_MSG, 0, "생성(G) 상태 파일만 확정 가능", 0L); + FAIL(b); + } else if (h_wrcnt == 0) { + setl(b, T_RC, 45); + Bchg(b, T_MSG, 0, "편성 상세가 없어 확정 불가", 0L); + FAIL(b); + } + /* 4) 본처리부 - 파일 확정 + 편성건 F 전이 */ + EXEC SQL UPDATE py_file SET status = 'C', confirmed_at = now() + WHERE file_id = :h_id2 AND status = 'G'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "지급파일 확정 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "확정 대상 파일 없음 - 동시성 충돌", 0L); + FAIL(b); + } + EXEC SQL UPDATE py_payout SET status = 'F', updated_at = now() + WHERE file_id = :h_id2 AND status = 'A'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 51); + Bchg(b, T_MSG, 0, "편성 지급건 확정 표기 실패", 0L); + FAIL(b); + } + strcpy(h_wfst, "C"); + h_wnew = h_id2; + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s file=%ld 상세=%ld건 %ld원 반영=%ld", "PAY_CONFIRM", h_wnew, h_wrcnt, h_wramt, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_CONFIRM', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 당일 지급파일 전체 현황 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(total_amount),0) INTO :h_wa1, :h_wa2 + FROM py_file WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "당일 파일 현황 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE biz_date = :h_wdate AND status = 'F'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 편성 건수 초과: 기준=%ld 실적=%ld", "PAY_CONFIRM", h_wa1, h_wa3); + } + /* 8) 응답부 */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + userlog("PAY_CONFIRM 완료: file=%ld 상세=%ld건 %ld원", h_wnew, h_wrcnt, h_wramt); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_CONFIRM", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_DORMANT.pgc b/app/src/py/svc/PAY_DORMANT.pgc index ef3c435..6a26bdc 100644 --- a/app/src/py/svc/PAY_DORMANT.pgc +++ b/app/src/py/svc/PAY_DORMANT.pgc @@ -1,7 +1,9 @@ /* - * PAY_DORMANT.pgc - py 모듈 서비스 PAY_DORMANT (one service = one file). + * PAY_DORMANT.pgc - py (지급) 모듈 온라인 서비스 PAY_DORMANT. * 휴면 가맹점 검출: 최근 N일간 지급 이력이 전혀 없는 정상(ACTIVE) 가맹점 * 수를 NOT EXISTS 로 세고 대표 1곳(가장 빠른 ID)을 함께 회신한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [scalar]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_DORMANT.h" @@ -10,24 +12,119 @@ void PAY_DORMANT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - long h_days, h_cnt; - char h_first[64]; + long h_arg1; + long h_wcnt; + long h_wsum; + long h_wfee; + long h_wnet; + long h_wrej; + long h_wlim; + long h_wrate; + long h_wpaid; + long h_whold; + char h_wtop[64]; + long h_wdays; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_days = getl(b, T_ARG1); - if (h_days <= 0 || h_days > 730) h_days = 30; - EXEC SQL SELECT count(*), coalesce(min(m.merchant_id),'') - INTO :h_cnt, :h_first - FROM merchant m - WHERE m.status = 'ACTIVE' - AND NOT EXISTS (SELECT 1 FROM py_payout p - WHERE p.merchant_id = m.merchant_id - AND p.created_at > now() - :h_days * interval '1 day'); - if (sqlca.sqlcode < 0) FAIL(b); - - setl(b, T_RC, h_cnt > 0 ? 1 : 0); - setl(b, T_COUNT, h_cnt); - setl(b, T_ARG1, h_days); - Bchg(b, T_MERCHANT, 0, h_first, 0L); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_arg1 = getl(b, T_ARG1); + if (h_arg1 < 0) h_arg1 = 0; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + /* 2) 집계 조회부 - 건수/총액/수수료/순액 스칼라 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) + INTO :h_wcnt, :h_wsum, :h_wfee, :h_wnet + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "지급 집계 조회 오류", 0L); + FAIL(b); + } + /* 상태별 부가 지표 - 반려/지급완료/보류 */ + EXEC SQL SELECT + count(*) FILTER (WHERE status = 'J'), + count(*) FILTER (WHERE status = 'P'), + count(*) FILTER (WHERE status = 'H') + INTO :h_wrej, :h_wpaid, :h_whold + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wrej = 0; h_wpaid = 0; h_whold = 0; } + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :h_wlim FROM merchant; + if (sqlca.sqlcode < 0) h_wlim = 0; + /* 최다 실적 가맹점 1건 추출 */ + EXEC SQL SELECT coalesce(merchant_id,'') INTO :h_wtop + FROM py_payout WHERE biz_date = :h_wdate + GROUP BY merchant_id ORDER BY count(*) DESC LIMIT 1; + if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) strcpy(h_wtop, ""); + h_wdays = h_arg1 > 0 ? h_arg1 : 30; + userlog("PAY_DORMANT 집계: 건수=%ld 총액=%ld 반려=%ld 보류=%ld", + h_wcnt, h_wsum, h_wrej, h_whold); + /* 3) 업무규칙 분기부 - 지표 임계 판정 (3분기) */ + h_wrate = (h_wcnt > 0) ? (h_wrej * 10000 / h_wcnt) : 0; + strcpy(h_pval, "2000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_dormant_fail_bps'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 2000; + if (h_wcnt == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "대상 지급건 없음", 0L); + } else if (h_wlim > 0 && h_wnet > h_wlim) { + setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "일 지급한도 초과 상태", 0L); + } else if (h_wrate > h_thr) { + setl(b, T_RC, 3); Bchg(b, T_MSG, 0, "실패율이 임계치를 초과", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "정상 범위", 0L); + } + if (h_whold > 0 && h_whold * 2 > h_wcnt) { + userlog("PAY_DORMANT 보류 과다 경고: 보류=%ld/전체=%ld", h_whold, h_wcnt); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s 건수=%ld 총액=%ld 반려=%ld 실패율=%ldbps 최다=%s", "PAY_DORMANT", h_wcnt, h_wsum, h_wrej, h_wrate, h_wtop); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_DORMANT', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계 재산정부 - 당일 지급완료 총액 재확인 */ + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_wpaid FROM py_payout + WHERE biz_date = :h_wdate AND status = 'P'; + if (sqlca.sqlcode < 0) h_wpaid = 0; + /* 8) 응답부 */ + setl(b, T_COUNT, h_wcnt); + setl(b, T_ARG1, h_wrej); + Bchg(b, T_MERCHANT, 0, h_wtop, 0L); + userlog("PAY_DORMANT 완료: 건수=%ld 총액=%ld 실패율=%ldbps", + h_wcnt, h_wsum, h_wrate); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_DORMANT", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_DORMLIST.pgc b/app/src/py/svc/PAY_DORMLIST.pgc index 3fe29ae..acec64b 100644 --- a/app/src/py/svc/PAY_DORMLIST.pgc +++ b/app/src/py/svc/PAY_DORMLIST.pgc @@ -1,7 +1,9 @@ /* - * PAY_DORMLIST.pgc - py 모듈 서비스 PAY_DORMLIST (one service = one file). + * PAY_DORMLIST.pgc - py (지급) 모듈 온라인 서비스 PAY_DORMLIST. * 휴면 계좌 목록: 지정 기준일 이후 지급 이력이 전혀 없는 활성(ACTIVE) 계좌를 * NOT EXISTS 커서로 뽑아 계좌/예금주/잔액을 occurrence 로 회신한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [cursor]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_DORMLIST.h" @@ -9,36 +11,127 @@ void PAY_DORMLIST(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - int i = 0; - long n = 0; + int gi = 0; + long tot_cnt = 0, tot_sum = 0, tot_fee = 0, top_cnt = 0, top_sum = 0; + char top_grp[64]; EXEC SQL BEGIN DECLARE SECTION; - char h_since[16], h_acct[32], h_holder[64]; - long h_bal; + char h_bizdate[64]; + char h_wgrp[64]; + long h_wgcnt; + long h_wgsum; + long h_wgfee; + long h_wtot; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, h_since, sizeof(h_since)); - EXEC SQL DECLARE cur_dormlist CURSOR FOR - SELECT a.account_no, a.holder, a.balance FROM py_account a - WHERE a.status = 'ACTIVE' - AND NOT EXISTS (SELECT 1 FROM py_payout p - WHERE p.account_no = a.account_no AND p.biz_date >= :h_since) - ORDER BY a.account_no; - EXEC SQL OPEN cur_dormlist; - if (sqlca.sqlcode < 0) FAIL(b); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + top_grp[0] = 0; + /* 2) 사전 규모 확인 - 커서 오픈 전 총건수 파악 */ + EXEC SQL SELECT count(*) INTO :h_wtot FROM py_account a WHERE a.status = 'ACTIVE'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "집계 사전 조회 실패", 0L); + FAIL(b); + } + userlog("PAY_DORMLIST 집계 시작: 기준일=%s 대상=%ld건 (휴면계좌별)", h_wdate, h_wtot); + /* 커서 정의: 휴면계좌별 건수/금액 집계 */ + EXEC SQL DECLARE cur_pay_dormlist CURSOR FOR + SELECT a.account_no, 1, coalesce(a.balance,0), 0 + FROM py_account a WHERE a.status = 'ACTIVE' + ORDER BY a.account_no; + EXEC SQL OPEN cur_pay_dormlist; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "집계 커서 OPEN 실패", 0L); + FAIL(b); + } + /* 3) 본처리부 - 커서 순회하며 그룹별 집계/상위 추출 */ for (;;) { - EXEC SQL FETCH cur_dormlist INTO :h_acct, :h_holder, :h_bal; + EXEC SQL FETCH cur_pay_dormlist INTO :h_wgrp, :h_wgcnt, :h_wgsum, :h_wgfee; if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_dormlist; FAIL(b); } - n++; - if (i < 10) { - Bchg(b, T_STR5, i, h_acct, 0L); - Bchg(b, T_STR6, i, h_holder, 0L); - Bchg(b, T_AMT1, i, (char *)&h_bal, 0L); - i++; + if (sqlca.sqlcode < 0) { + EXEC SQL CLOSE cur_pay_dormlist; + setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "집계 FETCH 오류", 0L); FAIL(b); + } + tot_cnt += h_wgcnt; + tot_sum += h_wgsum; + tot_fee += h_wgfee; + if (h_wgcnt > top_cnt) { + top_cnt = h_wgcnt; + top_sum = h_wgsum; + strncpy(top_grp, h_wgrp, sizeof(top_grp)-1); + top_grp[sizeof(top_grp)-1] = 0; + } + if (gi < 10) { + Bchg(b, T_STR1, gi, h_wgrp, 0L); + Bchg(b, T_AMT1, gi, (char *)&h_wgsum, 0L); + Bchg(b, T_ARG1, gi, (char *)&h_wgcnt, 0L); + gi++; } } - EXEC SQL CLOSE cur_dormlist; - setl(b, T_COUNT, n); - setl(b, T_RC, 0); + EXEC SQL CLOSE cur_pay_dormlist; + strcpy(h_pval, "5000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_dormlist_concentration'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 5000; + /* 업무규칙 분기부 - 분포 집중도 판정 (3분기) */ + if (gi == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "휴면계좌별 집계 대상 없음", 0L); + } else if (tot_cnt > 0 && top_cnt * 10000 / tot_cnt > h_thr) { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "특정 휴면계좌 집중 - 확인 필요", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "휴면계좌별 정상 분포", 0L); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s %s별 그룹=%d 건수=%ld 금액=%ld 최다=%s", "PAY_DORMLIST", "휴면계좌", gi, tot_cnt, tot_sum, top_grp); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_DORMLIST', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 8) 응답부 */ + setl(b, T_COUNT, tot_cnt); + Bchg(b, T_STR5, 0, top_grp, 0L); + Bchg(b, T_STR6, 0, top_grp, 0L); + userlog("PAY_DORMLIST 완료: 그룹=%d 총건=%ld 총액=%ld 최다=%s", + gi, tot_cnt, tot_sum, top_grp); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_DORMLIST", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_DORMSTOP.pgc b/app/src/py/svc/PAY_DORMSTOP.pgc index 5a23482..cc5115d 100644 --- a/app/src/py/svc/PAY_DORMSTOP.pgc +++ b/app/src/py/svc/PAY_DORMSTOP.pgc @@ -1,7 +1,9 @@ /* - * PAY_DORMSTOP.pgc - py 모듈 서비스 PAY_DORMSTOP (one service = one file). + * PAY_DORMSTOP.pgc - py (지급) 모듈 온라인 서비스 PAY_DORMSTOP. * 휴면 가맹점 지급정지: 대상 가맹점의 정상 계좌를 DORMANT 로 내리고 * 잔여 요청(R) 지급건을 '휴면정지' 사유로 보류(H)한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [acct]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_DORMSTOP.h" @@ -10,30 +12,165 @@ void PAY_DORMSTOP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - char h_merch[64]; - long h_accts, h_held; + char h_merchant[64]; + char h_wast[16]; + long h_wbal; + char h_wholder[64]; + char h_wmerch[64]; + long h_waff; + long h_wcas; + long h_wsum; + long h_wcnt; + char h_wacct[64]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); - if (h_merch[0] == 0) { userlog("PAY_DORMSTOP: 가맹점 누락"); FAIL(b); } - - EXEC SQL UPDATE py_account SET status = 'DORMANT', updated_at = now() - WHERE merchant_id = :h_merch AND status = 'ACTIVE'; - if (sqlca.sqlcode < 0) FAIL(b); - h_accts = sqlca.sqlerrd[2]; - EXEC SQL UPDATE py_payout SET status = 'H', reject_reason = '휴면정지', updated_at = now() - WHERE merchant_id = :h_merch AND status = 'R'; - if (sqlca.sqlcode < 0) FAIL(b); - h_held = sqlca.sqlerrd[2]; - - if (h_accts == 0 && h_held == 0) { - setl(b, T_RC, 1); - Bchg(b, T_MSG, 0, "정지할 계좌/지급건 없음", 0L); - OK(b); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + gets_(b, T_MERCHANT, h_merchant, sizeof(h_merchant)); + if (h_merchant[0] == 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "가맹점 ID는 필수 입력", 0L); + FAIL(b); } + /* 2) 마스터 조회부 - 대상 지급계좌 확정 후 상태/잔액/예금주 조회 */ + EXEC SQL SELECT coalesce(max(account_no),'') INTO :h_wacct + FROM py_account WHERE merchant_id = :h_merchant + AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 39); + Bchg(b, T_MSG, 0, "가맹점 지급계좌 조회 오류", 0L); + FAIL(b); + } + if (h_wacct[0] == 0) { + setl(b, T_RC, 38); + Bchg(b, T_MSG, 0, "가맹점에 정상 지급계좌가 없음", 0L); + FAIL(b); + } + EXEC SQL SELECT status, balance, holder, merchant_id + INTO :h_wast, :h_wbal, :h_wholder, :h_wmerch + FROM py_account WHERE account_no = :h_wacct; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 지급계좌가 등록되어 있지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급계좌 조회 중 DB 오류", 0L); + FAIL(b); + } + /* 해당 계좌의 미지급 잔액 규모 파악 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wcnt, :h_wsum + FROM py_payout WHERE account_no = :h_wacct AND status IN ('R','A','H'); + if (sqlca.sqlcode < 0) { h_wcnt = 0; h_wsum = 0; } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + userlog("PAY_DORMSTOP 진입: acct=%s status=%s bal=%ld 미지급=%ld건", + h_wacct, h_wast, h_wbal, h_wcnt); + /* 3) 업무규칙 분기부 - 계좌 상태 전이 적법성 (3분기) */ + if (strcmp(h_wast, "DORMANT") == 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "이미 휴면 지급정지 상태인 계좌", 0L); + FAIL(b); + } + if (strcmp(h_wast, "ACTIVE") != 0) { + setl(b, T_RC, 43); Bchg(b, T_STATUS, 0, h_wast, 0L); + Bchg(b, T_MSG, 0, "현재 계좌 상태에서는 휴면 지급정지 처리 불가", 0L); FAIL(b); + } + if (h_wbal < 0) { + setl(b, T_RC, 44); + Bchg(b, T_MSG, 0, "잔액이 음수인 비정상 계좌 - 선행 정산 필요", 0L); + FAIL(b); + } + strcpy(h_pval, "1"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_dormstop_cascade'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 1; + /* 4) 본처리부 - 계좌 상태 전이 후 미지급건 연쇄 처리 */ + EXEC SQL UPDATE py_account SET status = 'DORMANT', updated_at = now() + WHERE account_no = :h_wacct AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "휴면 지급정지 처리 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "계좌 상태 동시성 충돌", 0L); + FAIL(b); + } + EXEC SQL UPDATE py_payout SET status = 'H', reject_reason = '휴면', + updated_at = now() + WHERE account_no = :h_wacct AND status IN ('R','A') AND :h_thr > 0; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 51); + Bchg(b, T_MSG, 0, "미지급건 연쇄 처리 실패", 0L); + FAIL(b); + } + h_wcas = sqlca.sqlerrd[2]; + strcpy(h_lvl, "W"); + snprintf(h_amsg, sizeof(h_amsg), "%s acct=%s %s->%s 연쇄=%ld 금액=%ld", "PAY_DORMSTOP", h_wacct, "ACTIVE", "DORMANT", h_wcas, h_wsum); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_DORMSTOP', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 계좌 상태 전이 후 잔여 미지급 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE account_no = :h_wacct AND status = 'H'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "계좌 잔여 미지급 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE account_no = :h_wacct AND status IN ('R','A'); + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 연쇄 처리 후 잔여 존재: 기준=%ld 실적=%ld", "PAY_DORMSTOP", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - setl(b, T_ARG1, h_accts); - setl(b, T_COUNT, h_held); - userlog("PAY_DORMSTOP merch=%s accts=%ld held=%ld", h_merch, h_accts, h_held); + Bchg(b, T_STATUS, 0, "DORMANT", 0L); + setl(b, T_COUNT, h_wcas); + setl(b, T_ARG1, h_wbal); + Bchg(b, T_MSG, 0, "휴면 지급정지 및 미지급건 연쇄처리 완료", 0L); + userlog("PAY_DORMSTOP 완료: acct=%s 연쇄처리=%ld건 보류금액=%ld", + h_wacct, h_wcas, h_wsum); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_DORMSTOP", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_FAILCLEAR.pgc b/app/src/py/svc/PAY_FAILCLEAR.pgc index 869c7cf..f766daa 100644 --- a/app/src/py/svc/PAY_FAILCLEAR.pgc +++ b/app/src/py/svc/PAY_FAILCLEAR.pgc @@ -1,7 +1,9 @@ /* - * PAY_FAILCLEAR.pgc - py 모듈 서비스 PAY_FAILCLEAR (one service = one file). + * PAY_FAILCLEAR.pgc - py (지급) 모듈 온라인 서비스 PAY_FAILCLEAR. * 실패 종결: 재처리 불가한 반려(J) 단건을 취소(C)로 종결하고 종결 사유를 * reject_reason 에 남긴다. 반려 상태가 아니면 거절한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [txn]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_FAILCLEAR.h" @@ -9,26 +11,155 @@ void PAY_FAILCLEAR(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long payout_id = getl(b, T_ID1); - char st[4], reason[128]; + EXEC SQL BEGIN DECLARE SECTION; + long h_id1; + char h_reason[64]; + char h_wst[8]; + long h_wamt; + long h_wnet; + char h_wchan[16]; + long h_wretry; + long h_waff; + char h_wmerch[64]; + char h_wacct[64]; + long h_wcnt; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; + EXEC SQL END DECLARE SECTION; - gets_(b, T_REASON, reason, sizeof(reason)); - if (reason[0] == 0) strcpy(reason, "재처리불가종결"); - if (pydb_get_status(payout_id, st) < 0) FAIL(b); - if (st[0] != 'J') { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "반려 건만 종결 가능", 0L); FAIL(b); } - - { - EXEC SQL BEGIN DECLARE SECTION; - long h_pid = payout_id; - char h_rsn[128]; - EXEC SQL END DECLARE SECTION; - strncpy(h_rsn, reason, sizeof(h_rsn)-1); h_rsn[sizeof(h_rsn)-1] = 0; - EXEC SQL UPDATE py_payout SET status = 'C', reject_reason = :h_rsn, updated_at = now() - WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); } + gets_(b, T_REASON, h_reason, sizeof(h_reason)); + if (h_reason[0] == 0) strncpy(h_reason, "사유미기재", sizeof(h_reason)-1); + /* 2) 마스터 조회부 - 지급건 현재 상태/금액/채널/재시도 이력 */ + EXEC SQL SELECT status, amount, net, channel, retry_count, merchant_id, account_no + INTO :h_wst, :h_wamt, :h_wnet, :h_wchan, :h_wretry, :h_wmerch, :h_wacct + FROM py_payout WHERE payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 지급건이 존재하지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급건 조회 중 DB 오류", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + userlog("PAY_FAILCLEAR 진입: payout=%ld status=%s amt=%ld chan=%s", + h_id1, h_wst, h_wamt, h_wchan); + /* 3) 업무규칙 분기부 - 상태 전이 가능성/한도/재시도 판정 */ + if (strcmp(h_wst, "P") == 0 || strcmp(h_wst, "X") == 0) { + setl(b, T_RC, 2); Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "이미 종결(지급완료/마감)된 건은 실패 종결 불가", 0L); FAIL(b); + } + if (strcmp(h_wst, "J") != 0) { + setl(b, T_RC, 3); Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "현재 상태에서는 실패 종결 전이가 허용되지 않음", 0L); FAIL(b); + } + if (strcmp(h_wchan, "ADVANCE") == 0 && h_wnet > 50000000) { + userlog("PAY_FAILCLEAR 고액 선지급 건 주의: payout=%ld net=%ld", h_id1, h_wnet); + } + strcpy(h_pval, "100000000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_failclear_amt_max'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 100000000; + if (h_wamt > h_thr) { + userlog("PAY_FAILCLEAR 정책 상한 초과: amt=%ld thr=%ld", h_wamt, h_thr); + } + /* 4) 본처리부 - 가드 조건부 상태 전이 UPDATE */ + EXEC SQL UPDATE py_payout SET status = 'C', updated_at = now() + WHERE payout_id = :h_id1 AND status = 'J'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "실패 종결 상태 갱신 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "동시성 충돌 - 전이 대상 건이 없음", 0L); + FAIL(b); + } + EXEC SQL UPDATE py_payout SET reject_reason = :h_reason, updated_at = now() + WHERE payout_id = :h_id1; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 51); + Bchg(b, T_MSG, 0, "처리 사유 기록 실패", 0L); + FAIL(b); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s payout=%ld %s->%s amt=%ld aff=%ld", "PAY_FAILCLEAR", h_id1, "J", "C", h_wamt, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_FAILCLEAR', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계 재산정부 - 당일 동일 상태 건수 재집계 */ + EXEC SQL SELECT count(*) INTO :h_wcnt FROM py_payout + WHERE biz_date = :h_wdate AND status = 'C'; + if (sqlca.sqlcode < 0) h_wcnt = 0; + /* 6) 집계/요약 갱신부 - 가맹점 당일 확정성 지급 실적 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_wmerch + AND biz_date = :h_wdate AND status IN ('A','F','P'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "가맹점 지급 실적 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE merchant_id = :h_wmerch AND biz_date = :h_wdate + AND status IN ('R','H'); + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 미지급 잔량 과다: 기준=%ld 실적=%ld", "PAY_FAILCLEAR", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); Bchg(b, T_STATUS, 0, "C", 0L); - Bchg(b, T_REASON, 0, reason, 0L); + Bchg(b, T_REASON, 0, h_reason, 0L); + Bchg(b, T_MSG, 0, "실패 종결 정상 처리", 0L); + userlog("PAY_FAILCLEAR 완료: payout=%ld -> %s (당일 %s상태 %ld건)", + h_id1, "C", "C", h_wcnt); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_FAILCLEAR", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_FAILRATE.pgc b/app/src/py/svc/PAY_FAILRATE.pgc index 720944e..09bcecd 100644 --- a/app/src/py/svc/PAY_FAILRATE.pgc +++ b/app/src/py/svc/PAY_FAILRATE.pgc @@ -1,7 +1,9 @@ /* - * PAY_FAILRATE.pgc - py 모듈 서비스 PAY_FAILRATE (one service = one file). + * PAY_FAILRATE.pgc - py (지급) 모듈 온라인 서비스 PAY_FAILRATE. * 지급 실패율 산출: 가맹점 당일 전체 지급건 대비 반려(J) 비중을 bps 로 * 환산해 회신한다 (CASE 집계 1회). 대상 없으면 1. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [scalar]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_FAILRATE.h" @@ -9,27 +11,135 @@ void PAY_FAILRATE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long rate; EXEC SQL BEGIN DECLARE SECTION; - char h_merch[64], h_bizdate[16]; - long h_tot, h_fail; + char h_bizdate[64]; + char h_merchant[64]; + long h_wcnt; + long h_wsum; + long h_wfee; + long h_wnet; + long h_wrej; + long h_wlim; + long h_wrate; + long h_wpaid; + long h_whold; + char h_wtop[64]; + long h_wdays; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT count(*), coalesce(sum(CASE WHEN status = 'J' THEN 1 ELSE 0 END),0) - INTO :h_tot, :h_fail - FROM py_payout WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_tot == 0) { - setl(b, T_RC, 1); - Bchg(b, T_MSG, 0, "산출 대상 지급건 없음", 0L); - OK(b); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + gets_(b, T_MERCHANT, h_merchant, sizeof(h_merchant)); + if (h_merchant[0] == 0) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "가맹점 ID는 필수 입력", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + /* 2) 집계 조회부 - 건수/총액/수수료/순액 스칼라 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) + INTO :h_wcnt, :h_wsum, :h_wfee, :h_wnet + FROM py_payout WHERE merchant_id = :h_merchant AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "지급 집계 조회 오류", 0L); + FAIL(b); + } + /* 상태별 부가 지표 - 반려/지급완료/보류 */ + EXEC SQL SELECT + count(*) FILTER (WHERE status = 'J'), + count(*) FILTER (WHERE status = 'P'), + count(*) FILTER (WHERE status = 'H') + INTO :h_wrej, :h_wpaid, :h_whold + FROM py_payout WHERE merchant_id = :h_merchant AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { h_wrej = 0; h_wpaid = 0; h_whold = 0; } + EXEC SQL SELECT coalesce(daily_limit,0) INTO :h_wlim + FROM merchant WHERE merchant_id = :h_merchant; + if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) h_wlim = 0; + /* 최다 실적 가맹점 1건 추출 */ + EXEC SQL SELECT coalesce(merchant_id,'') INTO :h_wtop + FROM py_payout WHERE merchant_id = :h_merchant AND biz_date = :h_bizdate + GROUP BY merchant_id ORDER BY count(*) DESC LIMIT 1; + if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) strcpy(h_wtop, ""); + h_wdays = 30; + userlog("PAY_FAILRATE 집계: 건수=%ld 총액=%ld 반려=%ld 보류=%ld", + h_wcnt, h_wsum, h_wrej, h_whold); + /* 3) 업무규칙 분기부 - 지표 임계 판정 (3분기) */ + h_wrate = (h_wcnt > 0) ? (h_wrej * 10000 / h_wcnt) : 0; + strcpy(h_pval, "2000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_failrate_fail_bps'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 2000; + if (h_wcnt == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "대상 지급건 없음", 0L); + } else if (h_wlim > 0 && h_wnet > h_wlim) { + setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "일 지급한도 초과 상태", 0L); + } else if (h_wrate > h_thr) { + setl(b, T_RC, 3); Bchg(b, T_MSG, 0, "실패율이 임계치를 초과", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "정상 범위", 0L); + } + if (h_whold > 0 && h_whold * 2 > h_wcnt) { + userlog("PAY_FAILRATE 보류 과다 경고: 보류=%ld/전체=%ld", h_whold, h_wcnt); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s 건수=%ld 총액=%ld 반려=%ld 실패율=%ldbps 최다=%s", "PAY_FAILRATE", h_wcnt, h_wsum, h_wrej, h_wrate, h_wtop); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_FAILRATE', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계 갱신부 - py_summary upsert */ + if (h_wcnt > 0) { + if (pydb_upsert_summary(h_merchant, h_wdate, h_wcnt, h_wsum) < 0) { + setl(b, T_RC, 60); + Bchg(b, T_MSG, 0, "가맹점 지급집계 갱신 실패", 0L); + FAIL(b); + } + } + /* 8) 응답부 */ + setl(b, T_COUNT, h_wcnt); + setl(b, T_ARG1, h_wrej); + setl(b, T_ARG2, h_wrate); + userlog("PAY_FAILRATE 완료: 건수=%ld 총액=%ld 실패율=%ldbps", + h_wcnt, h_wsum, h_wrate); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_FAILRATE", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); } - rate = h_fail * 10000 / h_tot; - setl(b, T_RC, 0); - setl(b, T_COUNT, h_tot); - setl(b, T_ARG1, h_fail); - setl(b, T_ARG2, rate); OK(b); } diff --git a/app/src/py/svc/PAY_FAILRSN.pgc b/app/src/py/svc/PAY_FAILRSN.pgc index e3269a3..4a3348a 100644 --- a/app/src/py/svc/PAY_FAILRSN.pgc +++ b/app/src/py/svc/PAY_FAILRSN.pgc @@ -1,7 +1,9 @@ /* - * PAY_FAILRSN.pgc - py 모듈 서비스 PAY_FAILRSN (one service = one file). + * PAY_FAILRSN.pgc - py (지급) 모듈 온라인 서비스 PAY_FAILRSN. * 실패 재시도횟수별 분포: 당일 반려(J) 건을 retry_count 별 커서로 건수/net * 집계해 재시도 횟수 오름차순 occurrence 로 회신한다 (재처리 난이도 파악). + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [cursor]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_FAILRSN.h" @@ -9,34 +11,128 @@ void PAY_FAILRSN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - int i = 0; - long buckets = 0; + int gi = 0; + long tot_cnt = 0, tot_sum = 0, tot_fee = 0, top_cnt = 0, top_sum = 0; + char top_grp[64]; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_rc, h_cnt, h_amt; + char h_bizdate[64]; + char h_wgrp[64]; + long h_wgcnt; + long h_wgsum; + long h_wgfee; + long h_wtot; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL DECLARE cur_failrsn CURSOR FOR - SELECT retry_count, count(*), coalesce(sum(net),0) - FROM py_payout WHERE status = 'J' AND biz_date = :h_bizdate - GROUP BY retry_count ORDER BY retry_count; - EXEC SQL OPEN cur_failrsn; - if (sqlca.sqlcode < 0) FAIL(b); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + top_grp[0] = 0; + /* 2) 사전 규모 확인 - 커서 오픈 전 총건수 파악 */ + EXEC SQL SELECT count(*) INTO :h_wtot FROM py_payout p WHERE p.biz_date = :h_bizdate AND p.status = 'J'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "집계 사전 조회 실패", 0L); + FAIL(b); + } + userlog("PAY_FAILRSN 집계 시작: 기준일=%s 대상=%ld건 (재시도횟수별)", h_wdate, h_wtot); + /* 커서 정의: 재시도횟수별 건수/금액 집계 */ + EXEC SQL DECLARE cur_pay_failrsn CURSOR FOR + SELECT to_char(p.retry_count, '999'), count(*), coalesce(sum(p.net),0), coalesce(sum(p.fee),0) + FROM py_payout p WHERE p.biz_date = :h_bizdate AND p.status = 'J' + GROUP BY to_char(p.retry_count, '999') ORDER BY 2 DESC, 1; + EXEC SQL OPEN cur_pay_failrsn; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "집계 커서 OPEN 실패", 0L); + FAIL(b); + } + /* 3) 본처리부 - 커서 순회하며 그룹별 집계/상위 추출 */ for (;;) { - EXEC SQL FETCH cur_failrsn INTO :h_rc, :h_cnt, :h_amt; + EXEC SQL FETCH cur_pay_failrsn INTO :h_wgrp, :h_wgcnt, :h_wgsum, :h_wgfee; if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_failrsn; FAIL(b); } - buckets++; - if (i < 8) { - Bchg(b, T_ARG6, i, (char *)&h_rc, 0L); - Bchg(b, T_ARG7, i, (char *)&h_cnt, 0L); - Bchg(b, T_AMT2, i, (char *)&h_amt, 0L); - i++; + if (sqlca.sqlcode < 0) { + EXEC SQL CLOSE cur_pay_failrsn; + setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "집계 FETCH 오류", 0L); FAIL(b); + } + tot_cnt += h_wgcnt; + tot_sum += h_wgsum; + tot_fee += h_wgfee; + if (h_wgcnt > top_cnt) { + top_cnt = h_wgcnt; + top_sum = h_wgsum; + strncpy(top_grp, h_wgrp, sizeof(top_grp)-1); + top_grp[sizeof(top_grp)-1] = 0; + } + if (gi < 10) { + Bchg(b, T_STR1, gi, h_wgrp, 0L); + Bchg(b, T_AMT1, gi, (char *)&h_wgsum, 0L); + Bchg(b, T_ARG1, gi, (char *)&h_wgcnt, 0L); + gi++; } } - EXEC SQL CLOSE cur_failrsn; - setl(b, T_COUNT, buckets); - setl(b, T_RC, 0); + EXEC SQL CLOSE cur_pay_failrsn; + strcpy(h_pval, "5000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_failrsn_concentration'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 5000; + /* 업무규칙 분기부 - 분포 집중도 판정 (3분기) */ + if (gi == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "재시도횟수별 집계 대상 없음", 0L); + } else if (tot_cnt > 0 && top_cnt * 10000 / tot_cnt > h_thr) { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "특정 재시도횟수 집중 - 확인 필요", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "재시도횟수별 정상 분포", 0L); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s %s별 그룹=%d 건수=%ld 금액=%ld 최다=%s", "PAY_FAILRSN", "재시도횟수", gi, tot_cnt, tot_sum, top_grp); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_FAILRSN', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 8) 응답부 */ + setl(b, T_COUNT, tot_cnt); + setl(b, T_AMT2, top_sum); + setl(b, T_ARG6, (long)gi); + setl(b, T_ARG7, (long)gi); + userlog("PAY_FAILRSN 완료: 그룹=%d 총건=%ld 총액=%ld 최다=%s", + gi, tot_cnt, tot_sum, top_grp); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_FAILRSN", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_FAILSTAT.pgc b/app/src/py/svc/PAY_FAILSTAT.pgc index f82ab6a..6491965 100644 --- a/app/src/py/svc/PAY_FAILSTAT.pgc +++ b/app/src/py/svc/PAY_FAILSTAT.pgc @@ -1,7 +1,9 @@ /* - * PAY_FAILSTAT.pgc - py 모듈 서비스 PAY_FAILSTAT (one service = one file). + * PAY_FAILSTAT.pgc - py (지급) 모듈 온라인 서비스 PAY_FAILSTAT. * 실패사유별 통계: 당일 반려(J)건을 사유별 커서로 집계해 상위 8개 사유를 * occurrence 로, 총 반려건/총액을 스칼라로 회신한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [cursor]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_FAILSTAT.h" @@ -9,38 +11,127 @@ void PAY_FAILSTAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - int i = 0; - long tot_cnt = 0, tot_amt = 0; + int gi = 0; + long tot_cnt = 0, tot_sum = 0, tot_fee = 0, top_cnt = 0, top_sum = 0; + char top_grp[64]; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16], h_reason[128]; - long h_cnt, h_amt; + char h_bizdate[64]; + char h_wgrp[64]; + long h_wgcnt; + long h_wgsum; + long h_wgfee; + long h_wtot; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL DECLARE cur_failstat CURSOR FOR - SELECT coalesce(nullif(reject_reason,''),'(사유미기재)'), count(*), coalesce(sum(net),0) - FROM py_payout - WHERE biz_date = :h_bizdate AND status = 'J' - GROUP BY 1 ORDER BY 2 DESC, 3 DESC; - EXEC SQL OPEN cur_failstat; - if (sqlca.sqlcode < 0) FAIL(b); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + top_grp[0] = 0; + /* 2) 사전 규모 확인 - 커서 오픈 전 총건수 파악 */ + EXEC SQL SELECT count(*) INTO :h_wtot FROM py_payout p WHERE p.biz_date = :h_bizdate AND p.status = 'J'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "집계 사전 조회 실패", 0L); + FAIL(b); + } + userlog("PAY_FAILSTAT 집계 시작: 기준일=%s 대상=%ld건 (실패사유별)", h_wdate, h_wtot); + /* 커서 정의: 실패사유별 건수/금액 집계 */ + EXEC SQL DECLARE cur_pay_failstat CURSOR FOR + SELECT p.reject_reason, count(*), coalesce(sum(p.net),0), coalesce(sum(p.fee),0) + FROM py_payout p WHERE p.biz_date = :h_bizdate AND p.status = 'J' + GROUP BY p.reject_reason ORDER BY 2 DESC, 1; + EXEC SQL OPEN cur_pay_failstat; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "집계 커서 OPEN 실패", 0L); + FAIL(b); + } + /* 3) 본처리부 - 커서 순회하며 그룹별 집계/상위 추출 */ for (;;) { - EXEC SQL FETCH cur_failstat INTO :h_reason, :h_cnt, :h_amt; + EXEC SQL FETCH cur_pay_failstat INTO :h_wgrp, :h_wgcnt, :h_wgsum, :h_wgfee; if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_failstat; FAIL(b); } - tot_cnt += h_cnt; - tot_amt += h_amt; - if (i < 8) { - Bchg(b, T_STR1, i, h_reason, 0L); - Bchg(b, T_ARG1, i, (char *)&h_cnt, 0L); - Bchg(b, T_AMT1, i, (char *)&h_amt, 0L); - i++; + if (sqlca.sqlcode < 0) { + EXEC SQL CLOSE cur_pay_failstat; + setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "집계 FETCH 오류", 0L); FAIL(b); + } + tot_cnt += h_wgcnt; + tot_sum += h_wgsum; + tot_fee += h_wgfee; + if (h_wgcnt > top_cnt) { + top_cnt = h_wgcnt; + top_sum = h_wgsum; + strncpy(top_grp, h_wgrp, sizeof(top_grp)-1); + top_grp[sizeof(top_grp)-1] = 0; + } + if (gi < 10) { + Bchg(b, T_STR1, gi, h_wgrp, 0L); + Bchg(b, T_AMT1, gi, (char *)&h_wgsum, 0L); + Bchg(b, T_ARG1, gi, (char *)&h_wgcnt, 0L); + gi++; } } - EXEC SQL CLOSE cur_failstat; + EXEC SQL CLOSE cur_pay_failstat; + strcpy(h_pval, "5000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_failstat_concentration'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 5000; + /* 업무규칙 분기부 - 분포 집중도 판정 (3분기) */ + if (gi == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "실패사유별 집계 대상 없음", 0L); + } else if (tot_cnt > 0 && top_cnt * 10000 / tot_cnt > h_thr) { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "특정 실패사유 집중 - 확인 필요", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "실패사유별 정상 분포", 0L); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s %s별 그룹=%d 건수=%ld 금액=%ld 최다=%s", "PAY_FAILSTAT", "실패사유", gi, tot_cnt, tot_sum, top_grp); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_FAILSTAT', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 8) 응답부 */ setl(b, T_COUNT, tot_cnt); - setl(b, T_GROSS, tot_amt); - setl(b, T_ARG2, (long)i); - setl(b, T_RC, 0); + setl(b, T_GROSS, tot_sum); + setl(b, T_ARG2, (long)gi); + userlog("PAY_FAILSTAT 완료: 그룹=%d 총건=%ld 총액=%ld 최다=%s", + gi, tot_cnt, tot_sum, top_grp); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_FAILSTAT", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_FAILTOP.pgc b/app/src/py/svc/PAY_FAILTOP.pgc index 802ec4b..928aa45 100644 --- a/app/src/py/svc/PAY_FAILTOP.pgc +++ b/app/src/py/svc/PAY_FAILTOP.pgc @@ -1,7 +1,9 @@ /* - * PAY_FAILTOP.pgc - py 모듈 서비스 PAY_FAILTOP (one service = one file). + * PAY_FAILTOP.pgc - py (지급) 모듈 온라인 서비스 PAY_FAILTOP. * 최다 실패 가맹점: 당일 반려(J)건이 가장 많은 가맹점 1곳을 ORDER BY + * LIMIT 1 로 뽑아 가맹점/건수/금액을 회신한다. 반려 없으면 1. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [scalar]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_FAILTOP.h" @@ -10,28 +12,122 @@ void PAY_FAILTOP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16], h_merch[64]; - long h_cnt, h_amt; + char h_bizdate[64]; + long h_wcnt; + long h_wsum; + long h_wfee; + long h_wnet; + long h_wrej; + long h_wlim; + long h_wrate; + long h_wpaid; + long h_whold; + char h_wtop[64]; + long h_wdays; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT merchant_id, count(*), coalesce(sum(net),0) - INTO :h_merch, :h_cnt, :h_amt - FROM py_payout - WHERE biz_date = :h_bizdate AND status = 'J' - GROUP BY merchant_id - ORDER BY count(*) DESC, merchant_id - LIMIT 1; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlcode == 100) { - setl(b, T_RC, 1); - setl(b, T_COUNT, 0); - Bchg(b, T_MSG, 0, "당일 반려 건 없음", 0L); - OK(b); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + /* 2) 집계 조회부 - 건수/총액/수수료/순액 스칼라 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) + INTO :h_wcnt, :h_wsum, :h_wfee, :h_wnet + FROM py_payout WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "지급 집계 조회 오류", 0L); + FAIL(b); + } + /* 상태별 부가 지표 - 반려/지급완료/보류 */ + EXEC SQL SELECT + count(*) FILTER (WHERE status = 'J'), + count(*) FILTER (WHERE status = 'P'), + count(*) FILTER (WHERE status = 'H') + INTO :h_wrej, :h_wpaid, :h_whold + FROM py_payout WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { h_wrej = 0; h_wpaid = 0; h_whold = 0; } + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :h_wlim FROM merchant; + if (sqlca.sqlcode < 0) h_wlim = 0; + /* 최다 실적 가맹점 1건 추출 */ + EXEC SQL SELECT coalesce(merchant_id,'') INTO :h_wtop + FROM py_payout WHERE biz_date = :h_bizdate + GROUP BY merchant_id ORDER BY count(*) DESC LIMIT 1; + if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) strcpy(h_wtop, ""); + h_wdays = 30; + userlog("PAY_FAILTOP 집계: 건수=%ld 총액=%ld 반려=%ld 보류=%ld", + h_wcnt, h_wsum, h_wrej, h_whold); + /* 3) 업무규칙 분기부 - 지표 임계 판정 (3분기) */ + h_wrate = (h_wcnt > 0) ? (h_wrej * 10000 / h_wcnt) : 0; + strcpy(h_pval, "2000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_failtop_fail_bps'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 2000; + if (h_wcnt == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "대상 지급건 없음", 0L); + } else if (h_wlim > 0 && h_wnet > h_wlim) { + setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "일 지급한도 초과 상태", 0L); + } else if (h_wrate > h_thr) { + setl(b, T_RC, 3); Bchg(b, T_MSG, 0, "실패율이 임계치를 초과", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "정상 범위", 0L); + } + if (h_whold > 0 && h_whold * 2 > h_wcnt) { + userlog("PAY_FAILTOP 보류 과다 경고: 보류=%ld/전체=%ld", h_whold, h_wcnt); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s 건수=%ld 총액=%ld 반려=%ld 실패율=%ldbps 최다=%s", "PAY_FAILTOP", h_wcnt, h_wsum, h_wrej, h_wrate, h_wtop); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_FAILTOP', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계 재산정부 - 당일 지급완료 총액 재확인 */ + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_wpaid FROM py_payout + WHERE biz_date = :h_wdate AND status = 'P'; + if (sqlca.sqlcode < 0) h_wpaid = 0; + /* 8) 응답부 */ + setl(b, T_COUNT, h_wcnt); + setl(b, T_GROSS, h_wsum); + Bchg(b, T_MERCHANT, 0, h_wtop, 0L); + userlog("PAY_FAILTOP 완료: 건수=%ld 총액=%ld 실패율=%ldbps", + h_wcnt, h_wsum, h_wrate); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_FAILTOP", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); } - setl(b, T_RC, 0); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_amt); - Bchg(b, T_MERCHANT, 0, h_merch, 0L); OK(b); } diff --git a/app/src/py/svc/PAY_FEECAP.pgc b/app/src/py/svc/PAY_FEECAP.pgc index 0ee30d2..c2a5d1f 100644 --- a/app/src/py/svc/PAY_FEECAP.pgc +++ b/app/src/py/svc/PAY_FEECAP.pgc @@ -1,7 +1,9 @@ /* - * PAY_FEECAP.pgc - py 모듈 서비스 PAY_FEECAP (one service = one file). + * PAY_FEECAP.pgc - py (지급) 모듈 온라인 서비스 PAY_FEECAP. * 수수료 상한 적용: 미지급 건 수수료가 상한(T_AMOUNT)을 넘으면 상한까지 * 낮추고 초과분만큼 net 을 늘린다. 초과분은 py_adjust 에 양수로 남긴다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [fee]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_FEECAP.h" @@ -9,37 +11,177 @@ void PAY_FEECAP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char reason[96], bizdate[16]; - long cap, cut, adj; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_fee, h_net, h_newfee, h_newnet; - char h_st[4]; + long h_amount; + char h_bizdate[64]; + long h_id1; + char h_reason[64]; + char h_wst[8]; + long h_wamt; + long h_wfee; + long h_wnet; + char h_wmerch[64]; + char h_wacct[64]; + long h_wmdr; + long h_wnew; + long h_wdelta; + long h_waff; + long h_wadj; + char h_wdt[16]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_ID1); - cap = getl(b, T_AMOUNT); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - gets_(b, T_REASON, reason, sizeof(reason)); - if (reason[0] == 0) strcpy(reason, "수수료상한"); - - EXEC SQL SELECT fee, net, status INTO :h_fee, :h_net, :h_st FROM py_payout WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - if (h_st[0] != 'R' && h_st[0] != 'A') { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "미지급 건만 적용 가능", 0L); FAIL(b); } - if (h_fee <= cap) { setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "수수료가 상한 이내", 0L); FAIL(b); } - - cut = h_fee - cap; - h_newfee = cap; - h_newnet = h_net + cut; - adj = pydb_insert_adjust(h_pid, cut, reason, bizdate); - if (adj < 0) FAIL(b); - EXEC SQL UPDATE py_payout SET fee = :h_newfee, net = :h_newnet, updated_at = now() - WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_amount = getl(b, T_AMOUNT); + if (h_amount < 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "금액 항목은 음수가 될 수 없음", 0L); + FAIL(b); + } + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 13); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + gets_(b, T_REASON, h_reason, sizeof(h_reason)); + if (h_reason[0] == 0) strncpy(h_reason, "사유미기재", sizeof(h_reason)-1); + /* 2) 마스터 조회부 - 지급건 + 가맹점 계약요율 (LEFT JOIN) */ + EXEC SQL SELECT p.status, p.amount, p.fee, p.net, p.merchant_id, + p.account_no, p.biz_date, coalesce(m.mdr_bps, 25) + INTO :h_wst, :h_wamt, :h_wfee, :h_wnet, :h_wmerch, + :h_wacct, :h_wdt, :h_wmdr + FROM py_payout p + LEFT JOIN merchant m ON m.merchant_id = p.merchant_id + WHERE p.payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 지급건 없음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급건/가맹점 조인 조회 오류", 0L); + FAIL(b); + } + /* 기 적재된 조정 합계 확인 */ + h_wadj = pydb_adjust_delta_sum(h_id1); + if (h_wadj < 0) h_wadj = 0; + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + userlog("PAY_FEECAP 진입: payout=%ld status=%s fee=%ld net=%ld 조정누계=%ld", + h_id1, h_wst, h_wfee, h_wnet, h_wadj); + /* 3) 업무규칙 분기부 - 수수료/순액 재산정 (3분기) */ + if (strcmp(h_wst, "P") == 0 || strcmp(h_wst, "X") == 0 || + strcmp(h_wst, "C") == 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "종결된 지급건은 수수료/순액 조정 불가", 0L); + FAIL(b); + } + if (h_wfee <= h_amount) { + h_wnew = h_wfee; + } else if (h_amount <= 0) { + h_wnew = 0; + } else { + h_wnew = h_amount; + } + strcpy(h_pval, "1000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_feecap_bps_cap'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 1000; + if (h_wnew > acq_fee(h_wamt, h_thr)) { + userlog("PAY_FEECAP 수수료 상한정책 초과: fee=%ld cap_bps=%ld", h_wnew, h_thr); + } + /* 4) 본처리부 - fee/net 재계산 및 갱신 */ + h_wdelta = h_wnew - h_wfee; + h_wnet = h_wamt - h_wnew; + if (h_wnet < 0) h_wnet = 0; + EXEC SQL UPDATE py_payout SET fee = :h_wnew, net = :h_wnet, + updated_at = now() + WHERE payout_id = :h_id1 AND status IN ('R','A','H'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "수수료/순액 갱신 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "갱신 대상 없음 - 처리 도중 상태 변동", 0L); + FAIL(b); + } + /* 5) 이력 적재부 - 조정 원장(py_adjust) 기록 */ + if (pydb_insert_adjust(h_id1, h_wdelta, h_reason, h_wdt) < 0) { + setl(b, T_RC, 55); + Bchg(b, T_MSG, 0, "조정 원장(py_adjust) 기록 실패", 0L); + FAIL(b); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s payout=%ld fee %ld->%ld net=%ld delta=%ld", "PAY_FEECAP", h_id1, h_wfee, h_wnew, h_wnet, h_wdelta); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_FEECAP', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 가맹점 당일 수수료/조정 누계 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(fee),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_wmerch AND biz_date = :h_wdt; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "수수료/조정 누계 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_adjust + WHERE payout_id = :h_id1; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 조정 이력 과다: 기준=%ld 실적=%ld", "PAY_FEECAP", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - setl(b, T_ADJ, adj); - setl(b, T_FEE, h_newfee); - setl(b, T_NET, h_newnet); - userlog("PAY_FEECAP id=%ld cap=%ld cut=%ld", h_pid, cap, cut); + setl(b, T_FEE, h_wnew); + setl(b, T_NET, h_wnet); + setl(b, T_ADJ, h_wdelta); + Bchg(b, T_MSG, 0, "수수료/순액 조정 완료", 0L); + userlog("PAY_FEECAP 완료: payout=%ld fee=%ld net=%ld", h_id1, h_wnew, h_wnet); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_FEECAP", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_FEEDED.pgc b/app/src/py/svc/PAY_FEEDED.pgc index 82f56c8..16a5214 100644 --- a/app/src/py/svc/PAY_FEEDED.pgc +++ b/app/src/py/svc/PAY_FEEDED.pgc @@ -1,26 +1,179 @@ /* - * PAY_FEEDED.pgc - py 모듈 서비스 PAY_FEEDED (one service = one file). + * PAY_FEEDED.pgc - py (지급) 모듈 온라인 서비스 PAY_FEEDED. * 수수료 공제(fee deduction): bps 로 수수료를 산출해 fee/net 을 재계산·갱신. - * Moved verbatim from the former inline py_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by py_svr at tpsvrinit. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [fee]. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_FEEDED.h" void PAY_FEEDED(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long amount, bps, fee, net; EXEC SQL BEGIN DECLARE SECTION; - long h_pay, h_fee, h_net; + long h_amount; + long h_arg1; + long h_id1; + char h_wst[8]; + long h_wamt; + long h_wfee; + long h_wnet; + char h_wmerch[64]; + char h_wacct[64]; + long h_wmdr; + long h_wnew; + long h_wdelta; + long h_waff; + long h_wadj; + char h_wdt[16]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pay = getl(b, T_ID1); - amount = getl(b, T_AMOUNT); - bps = getl(b, T_ARG1); if (bps <= 0) bps = 30; - fee = acq_fee(amount, bps); net = amount - fee; - h_fee = fee; h_net = net; - EXEC SQL UPDATE py_payout SET fee = :h_fee, net = :h_net, updated_at = now() - WHERE payout_id = :h_pay; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - setl(b, T_FEE, fee); setl(b, T_NET, net); + + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_amount = getl(b, T_AMOUNT); + if (h_amount < 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "금액 항목은 음수가 될 수 없음", 0L); + FAIL(b); + } + h_arg1 = getl(b, T_ARG1); + if (h_arg1 < 0) h_arg1 = 0; + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 13); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + /* 2) 마스터 조회부 - 지급건 + 가맹점 계약요율 (LEFT JOIN) */ + EXEC SQL SELECT p.status, p.amount, p.fee, p.net, p.merchant_id, + p.account_no, p.biz_date, coalesce(m.mdr_bps, 25) + INTO :h_wst, :h_wamt, :h_wfee, :h_wnet, :h_wmerch, + :h_wacct, :h_wdt, :h_wmdr + FROM py_payout p + LEFT JOIN merchant m ON m.merchant_id = p.merchant_id + WHERE p.payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 지급건 없음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급건/가맹점 조인 조회 오류", 0L); + FAIL(b); + } + /* 기 적재된 조정 합계 확인 */ + h_wadj = pydb_adjust_delta_sum(h_id1); + if (h_wadj < 0) h_wadj = 0; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + userlog("PAY_FEEDED 진입: payout=%ld status=%s fee=%ld net=%ld 조정누계=%ld", + h_id1, h_wst, h_wfee, h_wnet, h_wadj); + /* 3) 업무규칙 분기부 - 수수료/순액 재산정 (3분기) */ + if (strcmp(h_wst, "P") == 0 || strcmp(h_wst, "X") == 0 || + strcmp(h_wst, "C") == 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "종결된 지급건은 수수료/순액 조정 불가", 0L); + FAIL(b); + } + if (h_arg1 <= 0) { + h_wnew = acq_fee(h_wamt, h_wmdr); + } else if (h_arg1 > 1000) { + h_wnew = acq_fee(h_wamt, 1000); + } else { + h_wnew = acq_fee(h_wamt, h_arg1); + } + strcpy(h_pval, "1000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_feeded_bps_cap'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 1000; + if (h_wnew > acq_fee(h_wamt, h_thr)) { + userlog("PAY_FEEDED 수수료 상한정책 초과: fee=%ld cap_bps=%ld", h_wnew, h_thr); + } + /* 4) 본처리부 - fee/net 재계산 및 갱신 */ + h_wdelta = h_wnew - h_wfee; + h_wnet = h_wamt - h_wnew; + if (h_wnet < 0) h_wnet = 0; + EXEC SQL UPDATE py_payout SET fee = :h_wnew, net = :h_wnet, + updated_at = now() + WHERE payout_id = :h_id1 AND status IN ('R','A','H'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "수수료/순액 갱신 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "갱신 대상 없음 - 처리 도중 상태 변동", 0L); + FAIL(b); + } + /* 5) 이력 적재부 - 조정 원장(py_adjust) 기록 */ + if (pydb_insert_adjust(h_id1, h_wdelta, "수수료/순액 조정", h_wdt) < 0) { + setl(b, T_RC, 55); + Bchg(b, T_MSG, 0, "조정 원장(py_adjust) 기록 실패", 0L); + FAIL(b); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s payout=%ld fee %ld->%ld net=%ld delta=%ld", "PAY_FEEDED", h_id1, h_wfee, h_wnew, h_wnet, h_wdelta); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_FEEDED', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 가맹점 당일 수수료/조정 누계 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(fee),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_wmerch AND biz_date = :h_wdt; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "수수료/조정 누계 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_adjust + WHERE payout_id = :h_id1; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 조정 이력 과다: 기준=%ld 실적=%ld", "PAY_FEEDED", h_wa1, h_wa3); + } + /* 8) 응답부 */ + setl(b, T_RC, 0); + setl(b, T_FEE, h_wnew); + setl(b, T_NET, h_wnet); + Bchg(b, T_MSG, 0, "수수료/순액 조정 완료", 0L); + userlog("PAY_FEEDED 완료: payout=%ld fee=%ld net=%ld", h_id1, h_wnew, h_wnet); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_FEEDED", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_FEEFIXED.pgc b/app/src/py/svc/PAY_FEEFIXED.pgc index 6050820..dff9aaa 100644 --- a/app/src/py/svc/PAY_FEEFIXED.pgc +++ b/app/src/py/svc/PAY_FEEFIXED.pgc @@ -1,7 +1,9 @@ /* - * PAY_FEEFIXED.pgc - py 모듈 서비스 PAY_FEEFIXED (one service = one file). + * PAY_FEEFIXED.pgc - py (지급) 모듈 온라인 서비스 PAY_FEEFIXED. * 정액 수수료 적용: 미지급(R/A) 건에 요청 정액 수수료(T_AMOUNT)를 부과하고 * net 을 amount-fee 로 재계산한다. 수수료가 원금을 넘으면 거절한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [fee]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_FEEFIXED.h" @@ -9,28 +11,169 @@ void PAY_FEEFIXED(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long fixed; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_amt, h_fee, h_net; - char h_st[4]; + long h_amount; + long h_id1; + char h_wst[8]; + long h_wamt; + long h_wfee; + long h_wnet; + char h_wmerch[64]; + char h_wacct[64]; + long h_wmdr; + long h_wnew; + long h_wdelta; + long h_waff; + long h_wadj; + char h_wdt[16]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_ID1); - fixed = getl(b, T_AMOUNT); - EXEC SQL SELECT amount, status INTO :h_amt, :h_st FROM py_payout WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - if (h_st[0] != 'R' && h_st[0] != 'A') { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "미지급 건만 적용 가능", 0L); FAIL(b); } - if (fixed > h_amt) { setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "수수료가 원금 초과", 0L); FAIL(b); } - - h_fee = fixed; - h_net = h_amt - fixed; - EXEC SQL UPDATE py_payout SET fee = :h_fee, net = :h_net, updated_at = now() - WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_amount = getl(b, T_AMOUNT); + if (h_amount < 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "금액 항목은 음수가 될 수 없음", 0L); + FAIL(b); + } + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + /* 2) 마스터 조회부 - 지급건 + 가맹점 계약요율 (LEFT JOIN) */ + EXEC SQL SELECT p.status, p.amount, p.fee, p.net, p.merchant_id, + p.account_no, p.biz_date, coalesce(m.mdr_bps, 25) + INTO :h_wst, :h_wamt, :h_wfee, :h_wnet, :h_wmerch, + :h_wacct, :h_wdt, :h_wmdr + FROM py_payout p + LEFT JOIN merchant m ON m.merchant_id = p.merchant_id + WHERE p.payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 지급건 없음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급건/가맹점 조인 조회 오류", 0L); + FAIL(b); + } + /* 기 적재된 조정 합계 확인 */ + h_wadj = pydb_adjust_delta_sum(h_id1); + if (h_wadj < 0) h_wadj = 0; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + userlog("PAY_FEEFIXED 진입: payout=%ld status=%s fee=%ld net=%ld 조정누계=%ld", + h_id1, h_wst, h_wfee, h_wnet, h_wadj); + /* 3) 업무규칙 분기부 - 수수료/순액 재산정 (3분기) */ + if (strcmp(h_wst, "P") == 0 || strcmp(h_wst, "X") == 0 || + strcmp(h_wst, "C") == 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "종결된 지급건은 수수료/순액 조정 불가", 0L); + FAIL(b); + } + if (h_amount <= 0) { + setl(b, T_RC, 13); + Bchg(b, T_MSG, 0, "정액 수수료는 1 이상이어야 함", 0L); + FAIL(b); + } else if (h_amount > h_wamt) { + h_wnew = h_wamt; + } else { + h_wnew = h_amount; + } + strcpy(h_pval, "1000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_feefixed_bps_cap'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 1000; + if (h_wnew > acq_fee(h_wamt, h_thr)) { + userlog("PAY_FEEFIXED 수수료 상한정책 초과: fee=%ld cap_bps=%ld", h_wnew, h_thr); + } + /* 4) 본처리부 - fee/net 재계산 및 갱신 */ + h_wdelta = h_wnew - h_wfee; + h_wnet = h_wamt - h_wnew; + if (h_wnet < 0) h_wnet = 0; + EXEC SQL UPDATE py_payout SET fee = :h_wnew, net = :h_wnet, + updated_at = now() + WHERE payout_id = :h_id1 AND status IN ('R','A','H'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "수수료/순액 갱신 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "갱신 대상 없음 - 처리 도중 상태 변동", 0L); + FAIL(b); + } + /* 5) 이력 적재부 - 조정 원장(py_adjust) 기록 */ + if (pydb_insert_adjust(h_id1, h_wdelta, "수수료/순액 조정", h_wdt) < 0) { + setl(b, T_RC, 55); + Bchg(b, T_MSG, 0, "조정 원장(py_adjust) 기록 실패", 0L); + FAIL(b); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s payout=%ld fee %ld->%ld net=%ld delta=%ld", "PAY_FEEFIXED", h_id1, h_wfee, h_wnew, h_wnet, h_wdelta); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_FEEFIXED', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 가맹점 당일 수수료/조정 누계 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(fee),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_wmerch AND biz_date = :h_wdt; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "수수료/조정 누계 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_adjust + WHERE payout_id = :h_id1; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 조정 이력 과다: 기준=%ld 실적=%ld", "PAY_FEEFIXED", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - setl(b, T_FEE, h_fee); - setl(b, T_NET, h_net); - userlog("PAY_FEEFIXED id=%ld fee=%ld net=%ld", h_pid, h_fee, h_net); + setl(b, T_FEE, h_wnew); + setl(b, T_NET, h_wnet); + Bchg(b, T_MSG, 0, "수수료/순액 조정 완료", 0L); + userlog("PAY_FEEFIXED 완료: payout=%ld fee=%ld net=%ld", h_id1, h_wnew, h_wnet); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_FEEFIXED", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_FEEMIN.pgc b/app/src/py/svc/PAY_FEEMIN.pgc index bc414a7..429a245 100644 --- a/app/src/py/svc/PAY_FEEMIN.pgc +++ b/app/src/py/svc/PAY_FEEMIN.pgc @@ -1,7 +1,9 @@ /* - * PAY_FEEMIN.pgc - py 모듈 서비스 PAY_FEEMIN (one service = one file). + * PAY_FEEMIN.pgc - py (지급) 모듈 온라인 서비스 PAY_FEEMIN. * 최소수수료 보정: 당일 요청(R) 건 중 수수료가 최소수수료 미만인 건을 * 한 번의 가드된 UPDATE 로 최소수수료로 끌어올리고 net 을 재계산한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [scalar]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_FEEMIN.h" @@ -10,24 +12,128 @@ void PAY_FEEMIN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_min, h_aff; + long h_amt1; + char h_bizdate[64]; + long h_wcnt; + long h_wsum; + long h_wfee; + long h_wnet; + long h_wrej; + long h_wlim; + long h_wrate; + long h_wpaid; + long h_whold; + char h_wtop[64]; + long h_wdays; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_amt1 = getl(b, T_AMT1); + if (h_amt1 < 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "금액 항목은 음수가 될 수 없음", 0L); + FAIL(b); + } gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - h_min = getl(b, T_AMT1); - if (h_min <= 0) h_min = 100; /* 기본 최소수수료 100원 */ - - EXEC SQL UPDATE py_payout - SET fee = :h_min, net = amount - :h_min, updated_at = now() - WHERE biz_date = :h_bizdate AND status = 'R' - AND fee < :h_min AND amount > :h_min; - if (sqlca.sqlcode < 0) FAIL(b); - h_aff = sqlca.sqlerrd[2]; - - setl(b, T_RC, 0); - setl(b, T_COUNT, h_aff); - setl(b, T_AMT1, h_min); - userlog("PAY_FEEMIN date=%s min=%ld fixed=%ld", h_bizdate, h_min, h_aff); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + /* 2) 집계 조회부 - 건수/총액/수수료/순액 스칼라 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) + INTO :h_wcnt, :h_wsum, :h_wfee, :h_wnet + FROM py_payout WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "지급 집계 조회 오류", 0L); + FAIL(b); + } + /* 상태별 부가 지표 - 반려/지급완료/보류 */ + EXEC SQL SELECT + count(*) FILTER (WHERE status = 'J'), + count(*) FILTER (WHERE status = 'P'), + count(*) FILTER (WHERE status = 'H') + INTO :h_wrej, :h_wpaid, :h_whold + FROM py_payout WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { h_wrej = 0; h_wpaid = 0; h_whold = 0; } + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :h_wlim FROM merchant; + if (sqlca.sqlcode < 0) h_wlim = 0; + /* 최다 실적 가맹점 1건 추출 */ + EXEC SQL SELECT coalesce(merchant_id,'') INTO :h_wtop + FROM py_payout WHERE biz_date = :h_bizdate + GROUP BY merchant_id ORDER BY count(*) DESC LIMIT 1; + if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) strcpy(h_wtop, ""); + h_wdays = 30; + userlog("PAY_FEEMIN 집계: 건수=%ld 총액=%ld 반려=%ld 보류=%ld", + h_wcnt, h_wsum, h_wrej, h_whold); + /* 3) 업무규칙 분기부 - 지표 임계 판정 (3분기) */ + h_wrate = (h_wcnt > 0) ? (h_wrej * 10000 / h_wcnt) : 0; + strcpy(h_pval, "2000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_feemin_fail_bps'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 2000; + if (h_wcnt == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "대상 지급건 없음", 0L); + } else if (h_wlim > 0 && h_wnet > h_wlim) { + setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "일 지급한도 초과 상태", 0L); + } else if (h_wrate > h_thr) { + setl(b, T_RC, 3); Bchg(b, T_MSG, 0, "실패율이 임계치를 초과", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "정상 범위", 0L); + } + if (h_whold > 0 && h_whold * 2 > h_wcnt) { + userlog("PAY_FEEMIN 보류 과다 경고: 보류=%ld/전체=%ld", h_whold, h_wcnt); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s 건수=%ld 총액=%ld 반려=%ld 실패율=%ldbps 최다=%s", "PAY_FEEMIN", h_wcnt, h_wsum, h_wrej, h_wrate, h_wtop); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_FEEMIN', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계 재산정부 - 당일 지급완료 총액 재확인 */ + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_wpaid FROM py_payout + WHERE biz_date = :h_wdate AND status = 'P'; + if (sqlca.sqlcode < 0) h_wpaid = 0; + /* 8) 응답부 */ + setl(b, T_COUNT, h_wcnt); + setl(b, T_AMT1, h_wsum); + userlog("PAY_FEEMIN 완료: 건수=%ld 총액=%ld 실패율=%ldbps", + h_wcnt, h_wsum, h_wrate); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_FEEMIN", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_FEERATE.pgc b/app/src/py/svc/PAY_FEERATE.pgc index 9f1166b..1e6e561 100644 --- a/app/src/py/svc/PAY_FEERATE.pgc +++ b/app/src/py/svc/PAY_FEERATE.pgc @@ -1,7 +1,9 @@ /* - * PAY_FEERATE.pgc - py 모듈 서비스 PAY_FEERATE (one service = one file). + * PAY_FEERATE.pgc - py (지급) 모듈 온라인 서비스 PAY_FEERATE. * 수수료율 재적용: 가맹점 계약 요율(merchant.mdr_bps)을 조인으로 가져와 * 지급 수수료/순액을 재계산해 미지급(R/A) 건에 반영한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [fee]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_FEERATE.h" @@ -10,29 +12,160 @@ void PAY_FEERATE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_amt, h_bps, h_fee, h_net; - char h_st[4]; + long h_id1; + char h_wst[8]; + long h_wamt; + long h_wfee; + long h_wnet; + char h_wmerch[64]; + char h_wacct[64]; + long h_wmdr; + long h_wnew; + long h_wdelta; + long h_waff; + long h_wadj; + char h_wdt[16]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_ID1); - EXEC SQL SELECT p.amount, p.status, m.mdr_bps INTO :h_amt, :h_st, :h_bps - FROM py_payout p JOIN merchant m ON p.merchant_id = m.merchant_id - WHERE p.payout_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - if (h_st[0] != 'R' && h_st[0] != 'A') { - setl(b, T_RC, 1); - Bchg(b, T_MSG, 0, "미지급 건만 요율 재적용 가능", 0L); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); FAIL(b); } - h_fee = acq_fee(h_amt, h_bps); - h_net = h_amt - h_fee; - EXEC SQL UPDATE py_payout SET fee = :h_fee, net = :h_net, updated_at = now() - WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - + /* 2) 마스터 조회부 - 지급건 + 가맹점 계약요율 (LEFT JOIN) */ + EXEC SQL SELECT p.status, p.amount, p.fee, p.net, p.merchant_id, + p.account_no, p.biz_date, coalesce(m.mdr_bps, 25) + INTO :h_wst, :h_wamt, :h_wfee, :h_wnet, :h_wmerch, + :h_wacct, :h_wdt, :h_wmdr + FROM py_payout p + LEFT JOIN merchant m ON m.merchant_id = p.merchant_id + WHERE p.payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 지급건 없음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급건/가맹점 조인 조회 오류", 0L); + FAIL(b); + } + /* 기 적재된 조정 합계 확인 */ + h_wadj = pydb_adjust_delta_sum(h_id1); + if (h_wadj < 0) h_wadj = 0; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + userlog("PAY_FEERATE 진입: payout=%ld status=%s fee=%ld net=%ld 조정누계=%ld", + h_id1, h_wst, h_wfee, h_wnet, h_wadj); + /* 3) 업무규칙 분기부 - 수수료/순액 재산정 (3분기) */ + if (strcmp(h_wst, "P") == 0 || strcmp(h_wst, "X") == 0 || + strcmp(h_wst, "C") == 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "종결된 지급건은 수수료/순액 조정 불가", 0L); + FAIL(b); + } + if (h_wmdr <= 0) { + h_wnew = h_wfee; + } else if (h_wmdr > 1000) { + h_wnew = acq_fee(h_wamt, 1000); + } else { + h_wnew = acq_fee(h_wamt, h_wmdr); + } + strcpy(h_pval, "1000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_feerate_bps_cap'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 1000; + if (h_wnew > acq_fee(h_wamt, h_thr)) { + userlog("PAY_FEERATE 수수료 상한정책 초과: fee=%ld cap_bps=%ld", h_wnew, h_thr); + } + /* 4) 본처리부 - fee/net 재계산 및 갱신 */ + h_wdelta = h_wnew - h_wfee; + h_wnet = h_wamt - h_wnew; + if (h_wnet < 0) h_wnet = 0; + EXEC SQL UPDATE py_payout SET fee = :h_wnew, net = :h_wnet, + updated_at = now() + WHERE payout_id = :h_id1 AND status IN ('R','A','H'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "수수료/순액 갱신 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "갱신 대상 없음 - 처리 도중 상태 변동", 0L); + FAIL(b); + } + /* 5) 이력 적재부 - 조정 원장(py_adjust) 기록 */ + if (pydb_insert_adjust(h_id1, h_wdelta, "수수료/순액 조정", h_wdt) < 0) { + setl(b, T_RC, 55); + Bchg(b, T_MSG, 0, "조정 원장(py_adjust) 기록 실패", 0L); + FAIL(b); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s payout=%ld fee %ld->%ld net=%ld delta=%ld", "PAY_FEERATE", h_id1, h_wfee, h_wnew, h_wnet, h_wdelta); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_FEERATE', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 가맹점 당일 수수료/조정 누계 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(fee),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_wmerch AND biz_date = :h_wdt; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "수수료/조정 누계 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_adjust + WHERE payout_id = :h_id1; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 조정 이력 과다: 기준=%ld 실적=%ld", "PAY_FEERATE", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - setl(b, T_FEE, h_fee); - setl(b, T_NET, h_net); - setl(b, T_ARG1, h_bps); + setl(b, T_FEE, h_wnew); + setl(b, T_NET, h_wnet); + setl(b, T_ARG1, h_wmdr); + Bchg(b, T_MSG, 0, "수수료/순액 조정 완료", 0L); + userlog("PAY_FEERATE 완료: payout=%ld fee=%ld net=%ld", h_id1, h_wnew, h_wnet); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_FEERATE", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_FEEREFUND.pgc b/app/src/py/svc/PAY_FEEREFUND.pgc index e6e18fe..aa36d3b 100644 --- a/app/src/py/svc/PAY_FEEREFUND.pgc +++ b/app/src/py/svc/PAY_FEEREFUND.pgc @@ -1,7 +1,9 @@ /* - * PAY_FEEREFUND.pgc - py 모듈 서비스 PAY_FEEREFUND (one service = one file). + * PAY_FEEREFUND.pgc - py (지급) 모듈 온라인 서비스 PAY_FEEREFUND. * 수수료 환급: 지급완료(P) 건의 수수료를 지급계좌에 되돌려 입금하고, * py_ledger 에 양수 FEEREFUND 기표를 남긴다. 수수료가 없으면 거절한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [fee]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_FEEREFUND.h" @@ -10,25 +12,169 @@ void PAY_FEEREFUND(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_fee; - char h_acct[32], h_st[4], h_bizdate[16]; + long h_id1; + char h_wst[8]; + long h_wamt; + long h_wfee; + long h_wnet; + char h_wmerch[64]; + char h_wacct[64]; + long h_wmdr; + long h_wnew; + long h_wdelta; + long h_waff; + long h_wadj; + char h_wdt[16]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_ID1); - EXEC SQL SELECT account_no, fee, status, CAST(biz_date AS text) - INTO :h_acct, :h_fee, :h_st, :h_bizdate FROM py_payout WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - if (h_st[0] != 'P') { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "지급완료 건만 환급 가능", 0L); FAIL(b); } - if (h_fee <= 0) { setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "환급할 수수료 없음", 0L); FAIL(b); } - - if (pydb_account_add(h_acct, h_fee) < 0) FAIL(b); - EXEC SQL INSERT INTO py_ledger (ledger_id, payout_id, entry_type, amount, biz_date) - VALUES (nextval('py_ledger_seq'), :h_pid, 'FEEREFUND', :h_fee, :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); - + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + /* 2) 마스터 조회부 - 지급건 + 가맹점 계약요율 (LEFT JOIN) */ + EXEC SQL SELECT p.status, p.amount, p.fee, p.net, p.merchant_id, + p.account_no, p.biz_date, coalesce(m.mdr_bps, 25) + INTO :h_wst, :h_wamt, :h_wfee, :h_wnet, :h_wmerch, + :h_wacct, :h_wdt, :h_wmdr + FROM py_payout p + LEFT JOIN merchant m ON m.merchant_id = p.merchant_id + WHERE p.payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 지급건 없음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급건/가맹점 조인 조회 오류", 0L); + FAIL(b); + } + /* 기 적재된 조정 합계 확인 */ + h_wadj = pydb_adjust_delta_sum(h_id1); + if (h_wadj < 0) h_wadj = 0; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + userlog("PAY_FEEREFUND 진입: payout=%ld status=%s fee=%ld net=%ld 조정누계=%ld", + h_id1, h_wst, h_wfee, h_wnet, h_wadj); + /* 3) 업무규칙 분기부 - 수수료/순액 재산정 (3분기) */ + if (strcmp(h_wst, "P") == 0 || strcmp(h_wst, "X") == 0 || + strcmp(h_wst, "C") == 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "종결된 지급건은 수수료/순액 조정 불가", 0L); + FAIL(b); + } + if (h_wfee <= 0) { + setl(b, T_RC, 13); + Bchg(b, T_MSG, 0, "환급할 수수료가 없음", 0L); + FAIL(b); + } else if (h_wacct[0] == 0) { + setl(b, T_RC, 14); + Bchg(b, T_MSG, 0, "환급 대상 지급계좌 없음", 0L); + FAIL(b); + } else { + h_wnew = 0; + } + strcpy(h_pval, "1000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_feerefund_bps_cap'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 1000; + if (h_wnew > acq_fee(h_wamt, h_thr)) { + userlog("PAY_FEEREFUND 수수료 상한정책 초과: fee=%ld cap_bps=%ld", h_wnew, h_thr); + } + /* 4) 본처리부 - fee/net 재계산 및 갱신 */ + h_wdelta = h_wnew - h_wfee; + h_wnet = h_wamt - h_wnew; + if (h_wnet < 0) h_wnet = 0; + EXEC SQL UPDATE py_payout SET fee = :h_wnew, net = :h_wnet, + updated_at = now() + WHERE payout_id = :h_id1 AND status IN ('R','A','H'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "수수료/순액 갱신 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "갱신 대상 없음 - 처리 도중 상태 변동", 0L); + FAIL(b); + } + /* 환급액을 지급계좌로 입금 */ + if (pydb_account_add(h_wacct, h_wfee) < 0) { + setl(b, T_RC, 53); + Bchg(b, T_MSG, 0, "수수료 환급 입금 실패", 0L); + FAIL(b); + } + /* 5) 이력 적재부 - 조정 원장(py_adjust) 기록 */ + if (pydb_insert_adjust(h_id1, h_wdelta, "수수료/순액 조정", h_wdt) < 0) { + setl(b, T_RC, 55); + Bchg(b, T_MSG, 0, "조정 원장(py_adjust) 기록 실패", 0L); + FAIL(b); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s payout=%ld fee %ld->%ld net=%ld delta=%ld", "PAY_FEEREFUND", h_id1, h_wfee, h_wnew, h_wnet, h_wdelta); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_FEEREFUND', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 가맹점 당일 수수료/조정 누계 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(fee),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_wmerch AND biz_date = :h_wdt; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "수수료/조정 누계 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_adjust + WHERE payout_id = :h_id1; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 조정 이력 과다: 기준=%ld 실적=%ld", "PAY_FEEREFUND", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - setl(b, T_FEE, h_fee); - Bchg(b, T_STR1, 0, h_acct, 0L); - userlog("PAY_FEEREFUND id=%ld fee=%ld acct=%s", h_pid, h_fee, h_acct); + setl(b, T_FEE, h_wnew); + Bchg(b, T_STR1, 0, h_wacct, 0L); + Bchg(b, T_MSG, 0, "수수료/순액 조정 완료", 0L); + userlog("PAY_FEEREFUND 완료: payout=%ld fee=%ld net=%ld", h_id1, h_wnew, h_wnet); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_FEEREFUND", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_FEESUM.pgc b/app/src/py/svc/PAY_FEESUM.pgc index adcd8c2..38b3b8e 100644 --- a/app/src/py/svc/PAY_FEESUM.pgc +++ b/app/src/py/svc/PAY_FEESUM.pgc @@ -1,7 +1,9 @@ /* - * PAY_FEESUM.pgc - py 모듈 서비스 PAY_FEESUM (one service = one file). + * PAY_FEESUM.pgc - py (지급) 모듈 온라인 서비스 PAY_FEESUM. * 수수료 집계: 당일 지급건의 수수료 합/건수와, py_adjust 에 쌓인 조정 합을 * 각각 조회해 순수수료(수수료-조정)를 산출해 회신한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [scalar]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_FEESUM.h" @@ -10,23 +12,123 @@ void PAY_FEESUM(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_cnt, h_fee, h_adj; + char h_bizdate[64]; + long h_wcnt; + long h_wsum; + long h_wfee; + long h_wnet; + long h_wrej; + long h_wlim; + long h_wrate; + long h_wpaid; + long h_whold; + char h_wtop[64]; + long h_wdays; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT count(*), coalesce(sum(fee),0) INTO :h_cnt, :h_fee + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + /* 2) 집계 조회부 - 건수/총액/수수료/순액 스칼라 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) + INTO :h_wcnt, :h_wsum, :h_wfee, :h_wnet FROM py_payout WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT coalesce(sum(delta),0) INTO :h_adj - FROM py_adjust WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - - setl(b, T_RC, 0); - setl(b, T_COUNT, h_cnt); - setl(b, T_FEE, h_fee); - setl(b, T_ADJ, h_adj); - setl(b, T_NET, h_fee - h_adj); - userlog("PAY_FEESUM date=%s fee=%ld adj=%ld", h_bizdate, h_fee, h_adj); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "지급 집계 조회 오류", 0L); + FAIL(b); + } + /* 상태별 부가 지표 - 반려/지급완료/보류 */ + EXEC SQL SELECT + count(*) FILTER (WHERE status = 'J'), + count(*) FILTER (WHERE status = 'P'), + count(*) FILTER (WHERE status = 'H') + INTO :h_wrej, :h_wpaid, :h_whold + FROM py_payout WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { h_wrej = 0; h_wpaid = 0; h_whold = 0; } + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :h_wlim FROM merchant; + if (sqlca.sqlcode < 0) h_wlim = 0; + /* 최다 실적 가맹점 1건 추출 */ + EXEC SQL SELECT coalesce(merchant_id,'') INTO :h_wtop + FROM py_payout WHERE biz_date = :h_bizdate + GROUP BY merchant_id ORDER BY count(*) DESC LIMIT 1; + if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) strcpy(h_wtop, ""); + h_wdays = 30; + userlog("PAY_FEESUM 집계: 건수=%ld 총액=%ld 반려=%ld 보류=%ld", + h_wcnt, h_wsum, h_wrej, h_whold); + /* 3) 업무규칙 분기부 - 지표 임계 판정 (3분기) */ + h_wrate = (h_wcnt > 0) ? (h_wrej * 10000 / h_wcnt) : 0; + strcpy(h_pval, "2000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_feesum_fail_bps'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 2000; + if (h_wcnt == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "대상 지급건 없음", 0L); + } else if (h_wlim > 0 && h_wnet > h_wlim) { + setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "일 지급한도 초과 상태", 0L); + } else if (h_wrate > h_thr) { + setl(b, T_RC, 3); Bchg(b, T_MSG, 0, "실패율이 임계치를 초과", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "정상 범위", 0L); + } + if (h_whold > 0 && h_whold * 2 > h_wcnt) { + userlog("PAY_FEESUM 보류 과다 경고: 보류=%ld/전체=%ld", h_whold, h_wcnt); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s 건수=%ld 총액=%ld 반려=%ld 실패율=%ldbps 최다=%s", "PAY_FEESUM", h_wcnt, h_wsum, h_wrej, h_wrate, h_wtop); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_FEESUM', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계 재산정부 - 당일 지급완료 총액 재확인 */ + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_wpaid FROM py_payout + WHERE biz_date = :h_wdate AND status = 'P'; + if (sqlca.sqlcode < 0) h_wpaid = 0; + /* 8) 응답부 */ + setl(b, T_COUNT, h_wcnt); + setl(b, T_NET, h_wnet); + setl(b, T_FEE, h_wfee); + setl(b, T_ADJ, h_wrej); + userlog("PAY_FEESUM 완료: 건수=%ld 총액=%ld 실패율=%ldbps", + h_wcnt, h_wsum, h_wrate); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_FEESUM", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_FEEWAIVE.pgc b/app/src/py/svc/PAY_FEEWAIVE.pgc index 35bd54c..da3e220 100644 --- a/app/src/py/svc/PAY_FEEWAIVE.pgc +++ b/app/src/py/svc/PAY_FEEWAIVE.pgc @@ -1,7 +1,9 @@ /* - * PAY_FEEWAIVE.pgc - py 모듈 서비스 PAY_FEEWAIVE (one service = one file). + * PAY_FEEWAIVE.pgc - py (지급) 모듈 온라인 서비스 PAY_FEEWAIVE. * 수수료 면제: 미지급(R/A) 건의 수수료를 0 으로 하고 net 을 amount 로 복원. * 면제분은 py_adjust 에 양수 조정으로 증적을 남긴다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [fee]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_FEEWAIVE.h" @@ -9,31 +11,171 @@ void PAY_FEEWAIVE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char reason[96], bizdate[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_fee, h_amt; - char h_st[4]; + char h_bizdate[64]; + long h_id1; + char h_reason[64]; + char h_wst[8]; + long h_wamt; + long h_wfee; + long h_wnet; + char h_wmerch[64]; + char h_wacct[64]; + long h_wmdr; + long h_wnew; + long h_wdelta; + long h_waff; + long h_wadj; + char h_wdt[16]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_ID1); - gets_(b, T_REASON, reason, sizeof(reason)); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (reason[0] == 0) strcpy(reason, "수수료면제"); - - EXEC SQL SELECT fee, amount, status INTO :h_fee, :h_amt, :h_st - FROM py_payout WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - if (h_fee <= 0) { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "면제할 수수료 없음", 0L); FAIL(b); } - if (h_st[0] != 'R' && h_st[0] != 'A') { setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "미지급 건만 면제 가능", 0L); FAIL(b); } - - if (pydb_insert_adjust(h_pid, h_fee, reason, bizdate) < 0) FAIL(b); - EXEC SQL UPDATE py_payout SET fee = 0, net = amount, updated_at = now() - WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + gets_(b, T_REASON, h_reason, sizeof(h_reason)); + if (h_reason[0] == 0) strncpy(h_reason, "사유미기재", sizeof(h_reason)-1); + /* 2) 마스터 조회부 - 지급건 + 가맹점 계약요율 (LEFT JOIN) */ + EXEC SQL SELECT p.status, p.amount, p.fee, p.net, p.merchant_id, + p.account_no, p.biz_date, coalesce(m.mdr_bps, 25) + INTO :h_wst, :h_wamt, :h_wfee, :h_wnet, :h_wmerch, + :h_wacct, :h_wdt, :h_wmdr + FROM py_payout p + LEFT JOIN merchant m ON m.merchant_id = p.merchant_id + WHERE p.payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 지급건 없음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급건/가맹점 조인 조회 오류", 0L); + FAIL(b); + } + /* 기 적재된 조정 합계 확인 */ + h_wadj = pydb_adjust_delta_sum(h_id1); + if (h_wadj < 0) h_wadj = 0; + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + userlog("PAY_FEEWAIVE 진입: payout=%ld status=%s fee=%ld net=%ld 조정누계=%ld", + h_id1, h_wst, h_wfee, h_wnet, h_wadj); + /* 3) 업무규칙 분기부 - 수수료/순액 재산정 (3분기) */ + if (strcmp(h_wst, "P") == 0 || strcmp(h_wst, "X") == 0 || + strcmp(h_wst, "C") == 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "종결된 지급건은 수수료/순액 조정 불가", 0L); + FAIL(b); + } + if (h_wfee == 0) { + setl(b, T_RC, 13); + Bchg(b, T_MSG, 0, "이미 수수료가 면제된 건", 0L); + FAIL(b); + } else if (h_wamt <= 0) { + h_wnew = 0; + } else { + h_wnew = 0; + } + strcpy(h_pval, "1000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_feewaive_bps_cap'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 1000; + if (h_wnew > acq_fee(h_wamt, h_thr)) { + userlog("PAY_FEEWAIVE 수수료 상한정책 초과: fee=%ld cap_bps=%ld", h_wnew, h_thr); + } + /* 4) 본처리부 - fee/net 재계산 및 갱신 */ + h_wdelta = h_wnew - h_wfee; + h_wnet = h_wamt - h_wnew; + if (h_wnet < 0) h_wnet = 0; + EXEC SQL UPDATE py_payout SET fee = :h_wnew, net = :h_wnet, + updated_at = now() + WHERE payout_id = :h_id1 AND status IN ('R','A','H'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "수수료/순액 갱신 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "갱신 대상 없음 - 처리 도중 상태 변동", 0L); + FAIL(b); + } + /* 5) 이력 적재부 - 조정 원장(py_adjust) 기록 */ + if (pydb_insert_adjust(h_id1, h_wdelta, h_reason, h_wdt) < 0) { + setl(b, T_RC, 55); + Bchg(b, T_MSG, 0, "조정 원장(py_adjust) 기록 실패", 0L); + FAIL(b); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s payout=%ld fee %ld->%ld net=%ld delta=%ld", "PAY_FEEWAIVE", h_id1, h_wfee, h_wnew, h_wnet, h_wdelta); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_FEEWAIVE', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 가맹점 당일 수수료/조정 누계 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(fee),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_wmerch AND biz_date = :h_wdt; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "수수료/조정 누계 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_adjust + WHERE payout_id = :h_id1; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 조정 이력 과다: 기준=%ld 실적=%ld", "PAY_FEEWAIVE", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - setl(b, T_FEE, h_fee); - setl(b, T_NET, h_amt); - userlog("PAY_FEEWAIVE id=%ld waived=%ld", h_pid, h_fee); + setl(b, T_FEE, h_wnew); + setl(b, T_NET, h_wnet); + Bchg(b, T_MSG, 0, "수수료/순액 조정 완료", 0L); + userlog("PAY_FEEWAIVE 완료: payout=%ld fee=%ld net=%ld", h_id1, h_wnew, h_wnet); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_FEEWAIVE", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_FILECANCEL.pgc b/app/src/py/svc/PAY_FILECANCEL.pgc index ac7315e..b9d6bff 100644 --- a/app/src/py/svc/PAY_FILECANCEL.pgc +++ b/app/src/py/svc/PAY_FILECANCEL.pgc @@ -1,7 +1,9 @@ /* - * PAY_FILECANCEL.pgc - py 모듈 서비스 PAY_FILECANCEL (one service = one file). + * PAY_FILECANCEL.pgc - py (지급) 모듈 온라인 서비스 PAY_FILECANCEL. * 지급파일 취소: 확정(C) 전 파일만 취소 가능. 편성(F) 건을 승인(A)+file_id=0 * 으로 되돌린 뒤 파일 헤더를 삭제한다. 확정/부재 파일은 사유코드로 거절. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [file]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_FILECANCEL.h" @@ -10,25 +12,128 @@ void PAY_FILECANCEL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - long h_fid, h_back; - char h_st[4]; + long h_id2; + char h_wfst[8]; + long h_whcnt; + long h_whamt; + long h_wrcnt; + long h_wramt; + long h_waff; + long h_wnew; + char h_wfname[64]; + char h_wfdate[16]; + char h_wdate[16]; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_fid = getl(b, T_ID2); - EXEC SQL SELECT status INTO :h_st FROM py_file WHERE file_id = :h_fid; - if (sqlca.sqlcode == 100) { setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "파일 없음", 0L); FAIL(b); } - if (sqlca.sqlcode < 0) FAIL(b); - if (h_st[0] == 'C') { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "확정 파일은 취소 불가", 0L); FAIL(b); } - + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id2 = getl(b, T_ID2); + if (h_id2 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + /* 2) 마스터 조회부 - 지급파일 헤더 + 실제 편성 상세 대사 */ + EXEC SQL SELECT status, record_count, total_amount, + to_char(biz_date, 'YYYY-MM-DD'), file_name + INTO :h_wfst, :h_whcnt, :h_whamt, :h_wfdate, :h_wfname + FROM py_file WHERE file_id = :h_id2; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "지급파일 헤더가 존재하지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급파일 헤더 조회 오류", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wrcnt, :h_wramt + FROM py_payout WHERE file_id = :h_id2; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "편성 상세 집계 오류", 0L); + FAIL(b); + } + userlog("PAY_FILECANCEL 진입: file=%ld status=%s 헤더=%ld/%ld 상세=%ld/%ld", + h_id2, h_wfst, h_whcnt, h_whamt, h_wrcnt, h_wramt); + /* 3) 업무규칙 분기부 - 파일 상태/정합 판정 (3분기) */ + if (strcmp(h_wfst, "C") == 0) { + setl(b, T_RC, 43); + Bchg(b, T_MSG, 0, "확정된 파일은 취소 불가", 0L); + FAIL(b); + } else if (strcmp(h_wfst, "G") != 0) { + setl(b, T_RC, 44); + Bchg(b, T_MSG, 0, "생성(G) 상태 파일만 취소 가능", 0L); + FAIL(b); + } + /* 4) 본처리부 - 편성건 승인 복원 후 헤더 삭제 */ EXEC SQL UPDATE py_payout SET status = 'A', file_id = 0, updated_at = now() - WHERE file_id = :h_fid AND status = 'F'; - if (sqlca.sqlcode < 0) FAIL(b); - h_back = sqlca.sqlerrd[2]; - EXEC SQL DELETE FROM py_file WHERE file_id = :h_fid; - if (sqlca.sqlcode < 0) FAIL(b); - - setl(b, T_RC, 0); - setl(b, T_COUNT, h_back); - userlog("PAY_FILECANCEL file=%ld reverted=%ld", h_fid, h_back); + WHERE file_id = :h_id2 AND status = 'F'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "편성 지급건 승인 복원 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + EXEC SQL DELETE FROM py_file WHERE file_id = :h_id2 AND status = 'G'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 51); + Bchg(b, T_MSG, 0, "지급파일 헤더 삭제 실패", 0L); + FAIL(b); + } + h_wnew = h_id2; + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s file=%ld 상세=%ld건 %ld원 반영=%ld", "PAY_FILECANCEL", h_wnew, h_wrcnt, h_wramt, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_FILECANCEL', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 당일 지급파일 전체 현황 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(total_amount),0) INTO :h_wa1, :h_wa2 + FROM py_file WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "당일 파일 현황 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE biz_date = :h_wdate AND status = 'F'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 편성 건수 초과: 기준=%ld 실적=%ld", "PAY_FILECANCEL", h_wa1, h_wa3); + } + /* 8) 응답부 */ + setl(b, T_COUNT, h_wrcnt); + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + userlog("PAY_FILECANCEL 완료: file=%ld 상세=%ld건 %ld원", h_wnew, h_wrcnt, h_wramt); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_FILECANCEL", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_FILECONF.pgc b/app/src/py/svc/PAY_FILECONF.pgc index d5e50e3..2e88547 100644 --- a/app/src/py/svc/PAY_FILECONF.pgc +++ b/app/src/py/svc/PAY_FILECONF.pgc @@ -1,7 +1,9 @@ /* - * PAY_FILECONF.pgc - py 모듈 서비스 PAY_FILECONF (one service = one file). + * PAY_FILECONF.pgc - py (지급) 모듈 온라인 서비스 PAY_FILECONF. * 지급파일 확정: 생성(G) 파일만 확정(C)+confirmed_at 로 전이하고, 그 파일에 * 편성(F)된 지급건을 지급완료(P)로 확정한다. 이미 확정 파일은 거절. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [file]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_FILECONF.h" @@ -10,25 +12,140 @@ void PAY_FILECONF(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - long h_fid, h_paid; - char h_st[4]; + long h_id2; + char h_wfst[8]; + long h_whcnt; + long h_whamt; + long h_wrcnt; + long h_wramt; + long h_waff; + long h_wnew; + char h_wfname[64]; + char h_wfdate[16]; + char h_wdate[16]; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_fid = getl(b, T_ID2); - EXEC SQL SELECT status INTO :h_st FROM py_file WHERE file_id = :h_fid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - if (h_st[0] == 'C') { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "이미 확정된 파일", 0L); FAIL(b); } - - EXEC SQL UPDATE py_file SET status = 'C', confirmed_at = now() WHERE file_id = :h_fid; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL UPDATE py_payout SET status = 'P', updated_at = now() - WHERE file_id = :h_fid AND status = 'F'; - if (sqlca.sqlcode < 0) FAIL(b); - h_paid = sqlca.sqlerrd[2]; - - setl(b, T_RC, 0); - setl(b, T_COUNT, h_paid); - Bchg(b, T_STATUS, 0, "C", 0L); - userlog("PAY_FILECONF file=%ld paid=%ld", h_fid, h_paid); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id2 = getl(b, T_ID2); + if (h_id2 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + /* 2) 마스터 조회부 - 지급파일 헤더 + 실제 편성 상세 대사 */ + EXEC SQL SELECT status, record_count, total_amount, + to_char(biz_date, 'YYYY-MM-DD'), file_name + INTO :h_wfst, :h_whcnt, :h_whamt, :h_wfdate, :h_wfname + FROM py_file WHERE file_id = :h_id2; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "지급파일 헤더가 존재하지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급파일 헤더 조회 오류", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wrcnt, :h_wramt + FROM py_payout WHERE file_id = :h_id2; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "편성 상세 집계 오류", 0L); + FAIL(b); + } + userlog("PAY_FILECONF 진입: file=%ld status=%s 헤더=%ld/%ld 상세=%ld/%ld", + h_id2, h_wfst, h_whcnt, h_whamt, h_wrcnt, h_wramt); + /* 3) 업무규칙 분기부 - 파일 상태/정합 판정 (3분기) */ + if (strcmp(h_wfst, "C") == 0) { + setl(b, T_RC, 43); + Bchg(b, T_MSG, 0, "이미 확정된 지급파일", 0L); + FAIL(b); + } else if (strcmp(h_wfst, "G") != 0) { + setl(b, T_RC, 44); + Bchg(b, T_MSG, 0, "생성(G) 상태 파일만 확정 가능", 0L); + FAIL(b); + } else if (h_wrcnt == 0) { + setl(b, T_RC, 45); + Bchg(b, T_MSG, 0, "편성 상세가 없어 확정 불가", 0L); + FAIL(b); + } + /* 4) 본처리부 - 파일 확정 + 편성건 F 전이 */ + EXEC SQL UPDATE py_file SET status = 'C', confirmed_at = now() + WHERE file_id = :h_id2 AND status = 'G'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "지급파일 확정 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "확정 대상 파일 없음 - 동시성 충돌", 0L); + FAIL(b); + } + EXEC SQL UPDATE py_payout SET status = 'F', updated_at = now() + WHERE file_id = :h_id2 AND status = 'A'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 51); + Bchg(b, T_MSG, 0, "편성 지급건 확정 표기 실패", 0L); + FAIL(b); + } + strcpy(h_wfst, "C"); + h_wnew = h_id2; + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s file=%ld 상세=%ld건 %ld원 반영=%ld", "PAY_FILECONF", h_wnew, h_wrcnt, h_wramt, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_FILECONF', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 당일 지급파일 전체 현황 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(total_amount),0) INTO :h_wa1, :h_wa2 + FROM py_file WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "당일 파일 현황 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE biz_date = :h_wdate AND status = 'F'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 편성 건수 초과: 기준=%ld 실적=%ld", "PAY_FILECONF", h_wa1, h_wa3); + } + /* 8) 응답부 */ + setl(b, T_COUNT, h_wrcnt); + Bchg(b, T_STATUS, 0, h_wfst, 0L); + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + userlog("PAY_FILECONF 완료: file=%ld 상세=%ld건 %ld원", h_wnew, h_wrcnt, h_wramt); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_FILECONF", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_FILEGEN.pgc b/app/src/py/svc/PAY_FILEGEN.pgc index 3b2e01e..e85d081 100644 --- a/app/src/py/svc/PAY_FILEGEN.pgc +++ b/app/src/py/svc/PAY_FILEGEN.pgc @@ -1,37 +1,158 @@ /* - * PAY_FILEGEN.pgc - py 모듈 서비스 PAY_FILEGEN (one service = one file). + * PAY_FILEGEN.pgc - py (지급) 모듈 온라인 서비스 PAY_FILEGEN. * 지급 파일 헤더 생성: 당일 승인(A) 지급건 집계 -> py_file. - * Moved verbatim from the former inline py_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by py_svr at tpsvrinit. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [file]. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_FILEGEN.h" void PAY_FILEGEN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], fname[64]; EXEC SQL BEGIN DECLARE SECTION; - long h_id, h_cnt, h_sum; - char h_bizdate[16], h_fname[64]; + char h_bizdate[64]; + char h_wfst[8]; + long h_whcnt; + long h_whamt; + long h_wrcnt; + long h_wramt; + long h_waff; + long h_wnew; + char h_wfname[64]; + char h_wfdate[16]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - - EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_cnt, :h_sum + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + /* 2) 마스터 조회부 - 당일 승인(A) 편성 대상 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) + INTO :h_wrcnt, :h_wramt FROM py_payout WHERE biz_date = :h_bizdate AND status = 'A'; - if (sqlca.sqlcode < 0) FAIL(b); - snprintf(fname, sizeof(fname), "PAYOUT_%s.dat", bizdate); - strncpy(h_fname, fname, sizeof(h_fname)-1); h_fname[sizeof(h_fname)-1] = 0; - EXEC SQL SELECT nextval('py_file_seq') INTO :h_id; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL INSERT INTO py_file (file_id, biz_date, file_name, record_count, total_amount, status) - VALUES (:h_id, :h_bizdate, :h_fname, :h_cnt, :h_sum, 'G'); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_ID2, h_id); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_sum); - Bchg(b, T_STR1, 0, fname, 0L); - userlog("PAY_FILEGEN file_id=%ld name=%s recs=%ld amt=%ld", h_id, fname, h_cnt, h_sum); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "편성 대상 집계 오류", 0L); + FAIL(b); + } + h_whcnt = h_wrcnt; h_whamt = h_wramt; + strcpy(h_wfst, "G"); + strncpy(h_wfdate, h_bizdate, sizeof(h_wfdate)-1); + h_wfdate[sizeof(h_wfdate)-1] = 0; + userlog("PAY_FILEGEN 진입: 기준일=%s 편성대상=%ld건 %ld원", + h_bizdate, h_wrcnt, h_wramt); + /* 3) 업무규칙 분기부 - 파일 상태/정합 판정 (3분기) */ + if (h_wrcnt == 0) { + setl(b, T_RC, 43); + Bchg(b, T_MSG, 0, "편성 대상 승인건이 없음", 0L); + FAIL(b); + } else if (h_wramt <= 0) { + setl(b, T_RC, 44); + Bchg(b, T_MSG, 0, "편성 총액이 비정상", 0L); + FAIL(b); + } + strcpy(h_pval, "100000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_filegen_max_rec'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 100000; + if (h_wrcnt > h_thr) { + setl(b, T_RC, 29); + Bchg(b, T_MSG, 0, "파일 최대 레코드 수 초과 - 분할 편성 필요", 0L); + FAIL(b); + } + /* 4) 본처리부 - 파일 채번/헤더 생성 후 대상건 편성 */ + EXEC SQL SELECT nextval('py_file_seq') INTO :h_wnew; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "지급파일 채번 실패", 0L); + FAIL(b); + } + snprintf(h_wfname, sizeof(h_wfname), "PAYOUT_%.10s_%ld.dat", + h_bizdate, h_wnew); + EXEC SQL INSERT INTO py_file + (file_id, biz_date, file_name, record_count, total_amount, status) + VALUES (:h_wnew, :h_bizdate, :h_wfname, :h_wrcnt, :h_wramt, 'G'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 51); + Bchg(b, T_MSG, 0, "지급파일 헤더 생성 실패", 0L); + FAIL(b); + } + EXEC SQL UPDATE py_payout SET file_id = :h_wnew, status = 'F', + updated_at = now() + WHERE biz_date = :h_bizdate AND status = 'A'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 52); + Bchg(b, T_MSG, 0, "지급건 파일 편성 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s file=%ld 상세=%ld건 %ld원 반영=%ld", "PAY_FILEGEN", h_wnew, h_wrcnt, h_wramt, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_FILEGEN', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 당일 지급파일 전체 현황 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(total_amount),0) INTO :h_wa1, :h_wa2 + FROM py_file WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "당일 파일 현황 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE biz_date = :h_wdate AND status = 'F'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 편성 건수 초과: 기준=%ld 실적=%ld", "PAY_FILEGEN", h_wa1, h_wa3); + } + /* 8) 응답부 */ + setl(b, T_ID2, h_wnew); + setl(b, T_COUNT, h_wrcnt); + setl(b, T_GROSS, h_wramt); + Bchg(b, T_STR1, 0, h_wfname, 0L); + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + userlog("PAY_FILEGEN 완료: file=%ld 상세=%ld건 %ld원", h_wnew, h_wrcnt, h_wramt); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_FILEGEN", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_FILEREBUILD.pgc b/app/src/py/svc/PAY_FILEREBUILD.pgc index b7adad4..0ca129e 100644 --- a/app/src/py/svc/PAY_FILEREBUILD.pgc +++ b/app/src/py/svc/PAY_FILEREBUILD.pgc @@ -1,7 +1,9 @@ /* - * PAY_FILEREBUILD.pgc - py 모듈 서비스 PAY_FILEREBUILD (one service = one file). + * PAY_FILEREBUILD.pgc - py (지급) 모듈 온라인 서비스 PAY_FILEREBUILD. * 지급파일 재생성: 아직 미확정(G)인 파일 헤더를 실제 편성 상세 기준으로 * 재집계해 갱신한다. 확정(C)된 파일은 재생성 불가로 거절. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [file]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_FILEREBUILD.h" @@ -10,27 +12,129 @@ void PAY_FILEREBUILD(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - long h_fid, h_cnt, h_sum; - char h_st[4]; + long h_id2; + char h_wfst[8]; + long h_whcnt; + long h_whamt; + long h_wrcnt; + long h_wramt; + long h_waff; + long h_wnew; + char h_wfname[64]; + char h_wfdate[16]; + char h_wdate[16]; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_fid = getl(b, T_ID2); - EXEC SQL SELECT status INTO :h_st FROM py_file WHERE file_id = :h_fid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - if (h_st[0] == 'C') { - setl(b, T_RC, 1); - Bchg(b, T_MSG, 0, "확정 파일은 재생성 불가", 0L); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id2 = getl(b, T_ID2); + if (h_id2 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + /* 2) 마스터 조회부 - 지급파일 헤더 + 실제 편성 상세 대사 */ + EXEC SQL SELECT status, record_count, total_amount, + to_char(biz_date, 'YYYY-MM-DD'), file_name + INTO :h_wfst, :h_whcnt, :h_whamt, :h_wfdate, :h_wfname + FROM py_file WHERE file_id = :h_id2; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "지급파일 헤더가 존재하지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급파일 헤더 조회 오류", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wrcnt, :h_wramt + FROM py_payout WHERE file_id = :h_id2; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "편성 상세 집계 오류", 0L); + FAIL(b); + } + userlog("PAY_FILEREBUILD 진입: file=%ld status=%s 헤더=%ld/%ld 상세=%ld/%ld", + h_id2, h_wfst, h_whcnt, h_whamt, h_wrcnt, h_wramt); + /* 3) 업무규칙 분기부 - 파일 상태/정합 판정 (3분기) */ + if (strcmp(h_wfst, "C") == 0) { + setl(b, T_RC, 43); + Bchg(b, T_MSG, 0, "확정된 파일은 재생성 불가", 0L); + FAIL(b); + } else if (h_wrcnt == 0) { + setl(b, T_RC, 44); + Bchg(b, T_MSG, 0, "재생성할 편성 상세가 없음", 0L); + FAIL(b); + } + /* 4) 본처리부 - 헤더를 실제 상세 기준으로 재산정 */ + EXEC SQL UPDATE py_file + SET record_count = :h_wrcnt, total_amount = :h_wramt + WHERE file_id = :h_id2 AND status = 'G'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "파일 헤더 재산정 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + h_wnew = h_id2; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "재생성 대상 헤더 없음", 0L); + FAIL(b); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s file=%ld 상세=%ld건 %ld원 반영=%ld", "PAY_FILEREBUILD", h_wnew, h_wrcnt, h_wramt, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_FILEREBUILD', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 당일 지급파일 전체 현황 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(total_amount),0) INTO :h_wa1, :h_wa2 + FROM py_file WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "당일 파일 현황 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE biz_date = :h_wdate AND status = 'F'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 편성 건수 초과: 기준=%ld 실적=%ld", "PAY_FILEREBUILD", h_wa1, h_wa3); + } + /* 8) 응답부 */ + setl(b, T_COUNT, h_wrcnt); + setl(b, T_GROSS, h_wramt); + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + userlog("PAY_FILEREBUILD 완료: file=%ld 상세=%ld건 %ld원", h_wnew, h_wrcnt, h_wramt); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_FILEREBUILD", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); FAIL(b); } - EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_cnt, :h_sum - FROM py_payout WHERE file_id = :h_fid AND status = 'F'; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL UPDATE py_file SET record_count = :h_cnt, total_amount = :h_sum - WHERE file_id = :h_fid; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_RC, 0); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_sum); - userlog("PAY_FILEREBUILD file=%ld recs=%ld amt=%ld", h_fid, h_cnt, h_sum); OK(b); } diff --git a/app/src/py/svc/PAY_FILESTAT.pgc b/app/src/py/svc/PAY_FILESTAT.pgc index b81adc9..fbe02ef 100644 --- a/app/src/py/svc/PAY_FILESTAT.pgc +++ b/app/src/py/svc/PAY_FILESTAT.pgc @@ -1,7 +1,9 @@ /* - * PAY_FILESTAT.pgc - py 모듈 서비스 PAY_FILESTAT (one service = one file). + * PAY_FILESTAT.pgc - py (지급) 모듈 온라인 서비스 PAY_FILESTAT. * 지급파일 현황: 당일 파일을 생성(G)/확정(C) 상태별로 세고 총 레코드수와 * 총액을 한 번의 CASE 집계로 회신한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [cursor]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_FILESTAT.h" @@ -9,24 +11,128 @@ void PAY_FILESTAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + int gi = 0; + long tot_cnt = 0, tot_sum = 0, tot_fee = 0, top_cnt = 0, top_sum = 0; + char top_grp[64]; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_gen, h_conf, h_recs, h_amt; + char h_bizdate[64]; + char h_wgrp[64]; + long h_wgcnt; + long h_wgsum; + long h_wgfee; + long h_wtot; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT coalesce(sum(CASE WHEN status = 'G' THEN 1 ELSE 0 END),0), - coalesce(sum(CASE WHEN status = 'C' THEN 1 ELSE 0 END),0), - coalesce(sum(record_count),0), - coalesce(sum(total_amount),0) - INTO :h_gen, :h_conf, :h_recs, :h_amt - FROM py_file WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - - setl(b, T_ARG1, h_gen); - setl(b, T_ARG2, h_conf); - setl(b, T_COUNT, h_recs); - setl(b, T_GROSS, h_amt); - setl(b, T_RC, 0); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + top_grp[0] = 0; + /* 2) 사전 규모 확인 - 커서 오픈 전 총건수 파악 */ + EXEC SQL SELECT count(*) INTO :h_wtot FROM py_file f WHERE f.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "집계 사전 조회 실패", 0L); + FAIL(b); + } + userlog("PAY_FILESTAT 집계 시작: 기준일=%s 대상=%ld건 (파일상태별)", h_wdate, h_wtot); + /* 커서 정의: 파일상태별 건수/금액 집계 */ + EXEC SQL DECLARE cur_pay_filestat CURSOR FOR + SELECT f.status, count(*), coalesce(sum(f.total_amount),0), + coalesce(sum(f.record_count),0) + FROM py_file f WHERE f.biz_date = :h_bizdate + GROUP BY f.status ORDER BY 2 DESC, 1; + EXEC SQL OPEN cur_pay_filestat; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "집계 커서 OPEN 실패", 0L); + FAIL(b); + } + /* 3) 본처리부 - 커서 순회하며 그룹별 집계/상위 추출 */ + for (;;) { + EXEC SQL FETCH cur_pay_filestat INTO :h_wgrp, :h_wgcnt, :h_wgsum, :h_wgfee; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { + EXEC SQL CLOSE cur_pay_filestat; + setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "집계 FETCH 오류", 0L); FAIL(b); + } + tot_cnt += h_wgcnt; + tot_sum += h_wgsum; + tot_fee += h_wgfee; + if (h_wgcnt > top_cnt) { + top_cnt = h_wgcnt; + top_sum = h_wgsum; + strncpy(top_grp, h_wgrp, sizeof(top_grp)-1); + top_grp[sizeof(top_grp)-1] = 0; + } + if (gi < 10) { + Bchg(b, T_STR1, gi, h_wgrp, 0L); + Bchg(b, T_AMT1, gi, (char *)&h_wgsum, 0L); + Bchg(b, T_ARG1, gi, (char *)&h_wgcnt, 0L); + gi++; + } + } + EXEC SQL CLOSE cur_pay_filestat; + strcpy(h_pval, "5000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_filestat_concentration'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 5000; + /* 업무규칙 분기부 - 분포 집중도 판정 (3분기) */ + if (gi == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "파일상태별 집계 대상 없음", 0L); + } else if (tot_cnt > 0 && top_cnt * 10000 / tot_cnt > h_thr) { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "특정 파일상태 집중 - 확인 필요", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "파일상태별 정상 분포", 0L); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s %s별 그룹=%d 건수=%ld 금액=%ld 최다=%s", "PAY_FILESTAT", "파일상태", gi, tot_cnt, tot_sum, top_grp); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_FILESTAT', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 8) 응답부 */ + setl(b, T_COUNT, tot_cnt); + setl(b, T_GROSS, tot_sum); + setl(b, T_ARG2, (long)gi); + userlog("PAY_FILESTAT 완료: 그룹=%d 총건=%ld 총액=%ld 최다=%s", + gi, tot_cnt, tot_sum, top_grp); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_FILESTAT", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_FILEVERIFY.pgc b/app/src/py/svc/PAY_FILEVERIFY.pgc index 468ceaa..f80766b 100644 --- a/app/src/py/svc/PAY_FILEVERIFY.pgc +++ b/app/src/py/svc/PAY_FILEVERIFY.pgc @@ -1,7 +1,9 @@ /* - * PAY_FILEVERIFY.pgc - py 모듈 서비스 PAY_FILEVERIFY (one service = one file). + * PAY_FILEVERIFY.pgc - py (지급) 모듈 온라인 서비스 PAY_FILEVERIFY. * 지급파일 검증: py_file 헤더의 건수/총액과 실제 편성된 py_payout 상세 집계를 * 대사해 일치 여부를 회신한다 (헤더-상세 정합 점검). + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [file]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_FILEVERIFY.h" @@ -10,29 +12,117 @@ void PAY_FILEVERIFY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - long h_fid, h_fcnt, h_fsum, h_acnt, h_asum; + long h_id2; + char h_wfst[8]; + long h_whcnt; + long h_whamt; + long h_wrcnt; + long h_wramt; + long h_waff; + long h_wnew; + char h_wfname[64]; + char h_wfdate[16]; + char h_wdate[16]; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_fid = getl(b, T_ID2); - EXEC SQL SELECT record_count, total_amount INTO :h_fcnt, :h_fsum - FROM py_file WHERE file_id = :h_fid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_acnt, :h_asum - FROM py_payout WHERE file_id = :h_fid; - if (sqlca.sqlcode < 0) FAIL(b); - - setl(b, T_ARG1, h_fcnt); - setl(b, T_AMT1, h_fsum); - setl(b, T_COUNT, h_acnt); - setl(b, T_GROSS, h_asum); - if (h_fcnt == h_acnt && h_fsum == h_asum) { - setl(b, T_RC, 0); - Bchg(b, T_MSG, 0, "지급파일 헤더-상세 일치", 0L); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id2 = getl(b, T_ID2); + if (h_id2 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + /* 2) 마스터 조회부 - 지급파일 헤더 + 실제 편성 상세 대사 */ + EXEC SQL SELECT status, record_count, total_amount, + to_char(biz_date, 'YYYY-MM-DD'), file_name + INTO :h_wfst, :h_whcnt, :h_whamt, :h_wfdate, :h_wfname + FROM py_file WHERE file_id = :h_id2; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "지급파일 헤더가 존재하지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급파일 헤더 조회 오류", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wrcnt, :h_wramt + FROM py_payout WHERE file_id = :h_id2; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "편성 상세 집계 오류", 0L); + FAIL(b); + } + userlog("PAY_FILEVERIFY 진입: file=%ld status=%s 헤더=%ld/%ld 상세=%ld/%ld", + h_id2, h_wfst, h_whcnt, h_whamt, h_wrcnt, h_wramt); + /* 3) 업무규칙 분기부 - 파일 상태/정합 판정 (3분기) */ + if (h_wrcnt == 0) { + setl(b, T_RC, 43); + Bchg(b, T_MSG, 0, "파일에 편성된 지급 상세가 없음", 0L); + FAIL(b); + } else if (h_whcnt != h_wrcnt || h_whamt != h_wramt) { + setl(b, T_RC, 5); Bchg(b, T_MSG, 0, "헤더-상세 건수/금액 불일치", 0L); } else { - setl(b, T_RC, 1); - Bchg(b, T_MSG, 0, "지급파일 헤더-상세 불일치", 0L); - userlog("PAY_FILEVERIFY 불일치 file=%ld hdr=%ld/%ld dtl=%ld/%ld", - h_fid, h_fcnt, h_fsum, h_acnt, h_asum); + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "헤더-상세 정합 일치", 0L); + } + h_waff = h_wrcnt; + h_wnew = h_id2; + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s file=%ld 상세=%ld건 %ld원 반영=%ld", "PAY_FILEVERIFY", h_wnew, h_wrcnt, h_wramt, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_FILEVERIFY', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 당일 지급파일 전체 현황 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(total_amount),0) INTO :h_wa1, :h_wa2 + FROM py_file WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "당일 파일 현황 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE biz_date = :h_wdate AND status = 'F'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 편성 건수 초과: 기준=%ld 실적=%ld", "PAY_FILEVERIFY", h_wa1, h_wa3); + } + /* 8) 응답부 */ + setl(b, T_COUNT, h_wrcnt); + setl(b, T_GROSS, h_wramt); + setl(b, T_AMT1, h_wramt); + setl(b, T_ARG1, h_waff); + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + userlog("PAY_FILEVERIFY 완료: file=%ld 상세=%ld건 %ld원", h_wnew, h_wrcnt, h_wramt); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_FILEVERIFY", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); } OK(b); } diff --git a/app/src/py/svc/PAY_GARNISH.pgc b/app/src/py/svc/PAY_GARNISH.pgc index 323c5a9..448e1b7 100644 --- a/app/src/py/svc/PAY_GARNISH.pgc +++ b/app/src/py/svc/PAY_GARNISH.pgc @@ -1,7 +1,9 @@ /* - * PAY_GARNISH.pgc - py 모듈 서비스 PAY_GARNISH (one service = one file). + * PAY_GARNISH.pgc - py (지급) 모듈 온라인 서비스 PAY_GARNISH. * 가압류 등록: 활성 계좌를 GARNISH 로 표시하고, 그 계좌 미지급(R/A) 잔액을 * 집계해 가압류 채권으로 py_ledger 에 음수 GARNISH 기표를 남긴다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [acct]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_GARNISH.h" @@ -10,29 +12,160 @@ void PAY_GARNISH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - char h_acct[32], h_bizdate[16]; - long h_cnt, h_sum, h_neg; + char h_bizdate[64]; + char h_key1[64]; + char h_wast[16]; + long h_wbal; + char h_wholder[64]; + char h_wmerch[64]; + long h_waff; + long h_wcas; + long h_wsum; + long h_wcnt; + char h_wacct[64]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, h_acct, sizeof(h_acct)); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + gets_(b, T_KEY1, h_key1, sizeof(h_key1)); + if (h_key1[0] == 0 || strlen(h_key1) < 3) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "계좌번호/키 값이 유효하지 않음", 0L); + FAIL(b); + } + /* 2) 마스터 조회부 - 대상 지급계좌 확정 후 상태/잔액/예금주 조회 */ + strncpy(h_wacct, h_key1, sizeof(h_wacct)-1); + h_wacct[sizeof(h_wacct)-1] = 0; + EXEC SQL SELECT status, balance, holder, merchant_id + INTO :h_wast, :h_wbal, :h_wholder, :h_wmerch + FROM py_account WHERE account_no = :h_wacct; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 지급계좌가 등록되어 있지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급계좌 조회 중 DB 오류", 0L); + FAIL(b); + } + /* 해당 계좌의 미지급 잔액 규모 파악 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wcnt, :h_wsum + FROM py_payout WHERE account_no = :h_wacct AND status IN ('R','A','H'); + if (sqlca.sqlcode < 0) { h_wcnt = 0; h_wsum = 0; } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + userlog("PAY_GARNISH 진입: acct=%s status=%s bal=%ld 미지급=%ld건", + h_wacct, h_wast, h_wbal, h_wcnt); + /* 3) 업무규칙 분기부 - 계좌 상태 전이 적법성 (3분기) */ + if (strcmp(h_wast, "GARNISH") == 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "이미 가압류 등록 상태인 계좌", 0L); + FAIL(b); + } + if (strcmp(h_wast, "ACTIVE") != 0) { + setl(b, T_RC, 43); Bchg(b, T_STATUS, 0, h_wast, 0L); + Bchg(b, T_MSG, 0, "현재 계좌 상태에서는 가압류 등록 처리 불가", 0L); FAIL(b); + } + if (h_wbal < 0) { + setl(b, T_RC, 44); + Bchg(b, T_MSG, 0, "잔액이 음수인 비정상 계좌 - 선행 정산 필요", 0L); + FAIL(b); + } + strcpy(h_pval, "1"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_garnish_cascade'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 1; + /* 4) 본처리부 - 계좌 상태 전이 후 미지급건 연쇄 처리 */ EXEC SQL UPDATE py_account SET status = 'GARNISH', updated_at = now() - WHERE account_no = :h_acct AND status = 'ACTIVE'; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "활성 계좌 아님", 0L); FAIL(b); } - - EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_cnt, :h_sum - FROM py_payout WHERE account_no = :h_acct AND status IN ('R','A'); - if (sqlca.sqlcode < 0) FAIL(b); - h_neg = -h_sum; - EXEC SQL INSERT INTO py_ledger (ledger_id, payout_id, entry_type, amount, biz_date) - VALUES (nextval('py_ledger_seq'), 0, 'GARNISH', :h_neg, :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); - + WHERE account_no = :h_wacct AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "가압류 등록 처리 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "계좌 상태 동시성 충돌", 0L); + FAIL(b); + } + EXEC SQL UPDATE py_payout SET status = 'H', reject_reason = '가압류', + updated_at = now() + WHERE account_no = :h_wacct AND status IN ('R','A') AND :h_thr > 0; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 51); + Bchg(b, T_MSG, 0, "미지급건 연쇄 처리 실패", 0L); + FAIL(b); + } + h_wcas = sqlca.sqlerrd[2]; + strcpy(h_lvl, "W"); + snprintf(h_amsg, sizeof(h_amsg), "%s acct=%s %s->%s 연쇄=%ld 금액=%ld", "PAY_GARNISH", h_wacct, "ACTIVE", "GARNISH", h_wcas, h_wsum); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_GARNISH', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 계좌 상태 전이 후 잔여 미지급 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE account_no = :h_wacct AND status = 'H'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "계좌 잔여 미지급 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE account_no = :h_wacct AND status IN ('R','A'); + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 연쇄 처리 후 잔여 존재: 기준=%ld 실적=%ld", "PAY_GARNISH", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_sum); - userlog("PAY_GARNISH acct=%s frozen=%ld amt=%ld", h_acct, h_cnt, h_sum); + Bchg(b, T_STATUS, 0, "GARNISH", 0L); + setl(b, T_COUNT, h_wcas); + setl(b, T_GROSS, h_wsum); + Bchg(b, T_MSG, 0, "가압류 등록 및 미지급건 연쇄처리 완료", 0L); + userlog("PAY_GARNISH 완료: acct=%s 연쇄처리=%ld건 보류금액=%ld", + h_wacct, h_wcas, h_wsum); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_GARNISH", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_HISTORY.pgc b/app/src/py/svc/PAY_HISTORY.pgc index 67ee236..fd587c3 100644 --- a/app/src/py/svc/PAY_HISTORY.pgc +++ b/app/src/py/svc/PAY_HISTORY.pgc @@ -1,23 +1,136 @@ /* - * PAY_HISTORY.pgc - py 모듈 서비스 PAY_HISTORY (one service = one file). + * PAY_HISTORY.pgc - py (지급) 모듈 온라인 서비스 PAY_HISTORY. * 가맹점 지급 이력 건수: 해당 가맹점의 전체 지급건 수를 집계해 회신. - * Moved verbatim from the former inline py_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by py_svr at tpsvrinit. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [scalar]. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_HISTORY.h" void PAY_HISTORY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt; - char h_merch[64]; + char h_merchant[64]; + long h_wcnt; + long h_wsum; + long h_wfee; + long h_wnet; + long h_wrej; + long h_wlim; + long h_wrate; + long h_wpaid; + long h_whold; + char h_wtop[64]; + long h_wdays; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - gets_(b, T_MERCHANT, merch, sizeof(merch)); - strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; - EXEC SQL SELECT count(*) INTO :h_cnt FROM py_payout WHERE merchant_id = :h_merch; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); + + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + gets_(b, T_MERCHANT, h_merchant, sizeof(h_merchant)); + if (h_merchant[0] == 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "가맹점 ID는 필수 입력", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + /* 2) 집계 조회부 - 건수/총액/수수료/순액 스칼라 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) + INTO :h_wcnt, :h_wsum, :h_wfee, :h_wnet + FROM py_payout WHERE merchant_id = :h_merchant; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "지급 집계 조회 오류", 0L); + FAIL(b); + } + /* 상태별 부가 지표 - 반려/지급완료/보류 */ + EXEC SQL SELECT + count(*) FILTER (WHERE status = 'J'), + count(*) FILTER (WHERE status = 'P'), + count(*) FILTER (WHERE status = 'H') + INTO :h_wrej, :h_wpaid, :h_whold + FROM py_payout WHERE merchant_id = :h_merchant; + if (sqlca.sqlcode < 0) { h_wrej = 0; h_wpaid = 0; h_whold = 0; } + EXEC SQL SELECT coalesce(daily_limit,0) INTO :h_wlim + FROM merchant WHERE merchant_id = :h_merchant; + if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) h_wlim = 0; + /* 최다 실적 가맹점 1건 추출 */ + EXEC SQL SELECT coalesce(merchant_id,'') INTO :h_wtop + FROM py_payout WHERE merchant_id = :h_merchant + GROUP BY merchant_id ORDER BY count(*) DESC LIMIT 1; + if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) strcpy(h_wtop, ""); + h_wdays = 30; + userlog("PAY_HISTORY 집계: 건수=%ld 총액=%ld 반려=%ld 보류=%ld", + h_wcnt, h_wsum, h_wrej, h_whold); + /* 3) 업무규칙 분기부 - 지표 임계 판정 (3분기) */ + h_wrate = (h_wcnt > 0) ? (h_wrej * 10000 / h_wcnt) : 0; + strcpy(h_pval, "2000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_history_fail_bps'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 2000; + if (h_wcnt == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "대상 지급건 없음", 0L); + } else if (h_wlim > 0 && h_wnet > h_wlim) { + setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "일 지급한도 초과 상태", 0L); + } else if (h_wrate > h_thr) { + setl(b, T_RC, 3); Bchg(b, T_MSG, 0, "실패율이 임계치를 초과", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "정상 범위", 0L); + } + if (h_whold > 0 && h_whold * 2 > h_wcnt) { + userlog("PAY_HISTORY 보류 과다 경고: 보류=%ld/전체=%ld", h_whold, h_wcnt); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s 건수=%ld 총액=%ld 반려=%ld 실패율=%ldbps 최다=%s", "PAY_HISTORY", h_wcnt, h_wsum, h_wrej, h_wrate, h_wtop); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_HISTORY', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계 갱신부 - py_summary upsert */ + if (h_wcnt > 0) { + if (pydb_upsert_summary(h_merchant, h_wdate, h_wcnt, h_wsum) < 0) { + setl(b, T_RC, 60); + Bchg(b, T_MSG, 0, "가맹점 지급집계 갱신 실패", 0L); + FAIL(b); + } + } + /* 8) 응답부 */ + setl(b, T_COUNT, h_wcnt); + userlog("PAY_HISTORY 완료: 건수=%ld 총액=%ld 실패율=%ldbps", + h_wcnt, h_wsum, h_wrate); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_HISTORY", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_HOLD.pgc b/app/src/py/svc/PAY_HOLD.pgc index 5069859..aeb0b1c 100644 --- a/app/src/py/svc/PAY_HOLD.pgc +++ b/app/src/py/svc/PAY_HOLD.pgc @@ -1,7 +1,9 @@ /* - * PAY_HOLD.pgc - py 모듈 서비스 PAY_HOLD (one service = one file). + * PAY_HOLD.pgc - py (지급) 모듈 온라인 서비스 PAY_HOLD. * 지급 보류: 이미 지급완료(P)/마감(X)된 건은 보류 불가 - dbio 로 현재 상태를 * 읽어 가드한 뒤 dbio 로 H 전이. dbio wrapper + status guard + extra field. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [txn]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_HOLD.h" @@ -9,13 +11,144 @@ void PAY_HOLD(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long payout_id = getl(b, T_ID1); - char st[4]; + EXEC SQL BEGIN DECLARE SECTION; + long h_id1; + char h_wst[8]; + long h_wamt; + long h_wnet; + char h_wchan[16]; + long h_wretry; + long h_waff; + char h_wmerch[64]; + char h_wacct[64]; + long h_wcnt; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; + EXEC SQL END DECLARE SECTION; - if (pydb_get_status(payout_id, st) < 0) FAIL(b); - if (st[0] == 'P' || st[0] == 'X') { setl(b, T_RC, 1); FAIL(b); } - if (pydb_update_status(payout_id, "H") < 0) FAIL(b); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + /* 2) 마스터 조회부 - 지급건 현재 상태/금액/채널/재시도 이력 */ + EXEC SQL SELECT status, amount, net, channel, retry_count, merchant_id, account_no + INTO :h_wst, :h_wamt, :h_wnet, :h_wchan, :h_wretry, :h_wmerch, :h_wacct + FROM py_payout WHERE payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 지급건이 존재하지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급건 조회 중 DB 오류", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + userlog("PAY_HOLD 진입: payout=%ld status=%s amt=%ld chan=%s", + h_id1, h_wst, h_wamt, h_wchan); + /* 3) 업무규칙 분기부 - 상태 전이 가능성/한도/재시도 판정 */ + if (strcmp(h_wst, "P") == 0 || strcmp(h_wst, "X") == 0) { + setl(b, T_RC, 2); Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "이미 종결(지급완료/마감)된 건은 지급 보류 불가", 0L); FAIL(b); + } + if (strcmp(h_wst, "R") != 0) { + setl(b, T_RC, 3); Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "현재 상태에서는 지급 보류 전이가 허용되지 않음", 0L); FAIL(b); + } + if (strcmp(h_wchan, "ADVANCE") == 0 && h_wnet > 50000000) { + userlog("PAY_HOLD 고액 선지급 건 주의: payout=%ld net=%ld", h_id1, h_wnet); + } + strcpy(h_pval, "100000000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_hold_amt_max'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 100000000; + if (h_wamt > h_thr) { + userlog("PAY_HOLD 정책 상한 초과: amt=%ld thr=%ld", h_wamt, h_thr); + } + /* 4) 본처리부 - 가드 조건부 상태 전이 UPDATE */ + EXEC SQL UPDATE py_payout SET status = 'H', updated_at = now() + WHERE payout_id = :h_id1 AND status = 'R'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "지급 보류 상태 갱신 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "동시성 충돌 - 전이 대상 건이 없음", 0L); + FAIL(b); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s payout=%ld %s->%s amt=%ld aff=%ld", "PAY_HOLD", h_id1, "R", "H", h_wamt, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_HOLD', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계 재산정부 - 당일 동일 상태 건수 재집계 */ + EXEC SQL SELECT count(*) INTO :h_wcnt FROM py_payout + WHERE biz_date = :h_wdate AND status = 'H'; + if (sqlca.sqlcode < 0) h_wcnt = 0; + /* 6) 집계/요약 갱신부 - 가맹점 당일 확정성 지급 실적 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_wmerch + AND biz_date = :h_wdate AND status IN ('A','F','P'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "가맹점 지급 실적 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE merchant_id = :h_wmerch AND biz_date = :h_wdate + AND status IN ('R','H'); + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 미지급 잔량 과다: 기준=%ld 실적=%ld", "PAY_HOLD", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); Bchg(b, T_STATUS, 0, "H", 0L); + Bchg(b, T_MSG, 0, "지급 보류 정상 처리", 0L); + userlog("PAY_HOLD 완료: payout=%ld -> %s (당일 %s상태 %ld건)", + h_id1, "H", "H", h_wcnt); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_HOLD", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_HOLDAGING.pgc b/app/src/py/svc/PAY_HOLDAGING.pgc index 73dbe21..8b6b6c7 100644 --- a/app/src/py/svc/PAY_HOLDAGING.pgc +++ b/app/src/py/svc/PAY_HOLDAGING.pgc @@ -1,7 +1,9 @@ /* - * PAY_HOLDAGING.pgc - py 모듈 서비스 PAY_HOLDAGING (one service = one file). + * PAY_HOLDAGING.pgc - py (지급) 모듈 온라인 서비스 PAY_HOLDAGING. * 장기 보류 점검: 보류(H) 상태로 N일 이상 경과한 건수/금액과 가장 오래된 * 보류 시각을 회신한다. 장기 보류가 있으면 경보(1). + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [scalar]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_HOLDAGING.h" @@ -10,24 +12,120 @@ void PAY_HOLDAGING(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - long h_days, h_cnt, h_sum; - char h_oldest[40]; + long h_arg1; + long h_wcnt; + long h_wsum; + long h_wfee; + long h_wnet; + long h_wrej; + long h_wlim; + long h_wrate; + long h_wpaid; + long h_whold; + char h_wtop[64]; + long h_wdays; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_days = getl(b, T_ARG1); - if (h_days <= 0 || h_days > 365) h_days = 3; - EXEC SQL SELECT count(*), coalesce(sum(net),0), coalesce(min(CAST(updated_at AS text)),'') - INTO :h_cnt, :h_sum, :h_oldest - FROM py_payout - WHERE status = 'H' AND updated_at < now() - :h_days * interval '1 day'; - if (sqlca.sqlcode < 0) FAIL(b); - - setl(b, T_RC, h_cnt > 0 ? 1 : 0); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_sum); - setl(b, T_ARG1, h_days); - Bchg(b, T_STR1, 0, h_oldest, 0L); - if (h_cnt > 0) - userlog("PAY_HOLDAGING 장기보류 경보 %ld일+ cnt=%ld amt=%ld", h_days, h_cnt, h_sum); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_arg1 = getl(b, T_ARG1); + if (h_arg1 < 0) h_arg1 = 0; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + /* 2) 집계 조회부 - 건수/총액/수수료/순액 스칼라 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) + INTO :h_wcnt, :h_wsum, :h_wfee, :h_wnet + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "지급 집계 조회 오류", 0L); + FAIL(b); + } + /* 상태별 부가 지표 - 반려/지급완료/보류 */ + EXEC SQL SELECT + count(*) FILTER (WHERE status = 'J'), + count(*) FILTER (WHERE status = 'P'), + count(*) FILTER (WHERE status = 'H') + INTO :h_wrej, :h_wpaid, :h_whold + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wrej = 0; h_wpaid = 0; h_whold = 0; } + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :h_wlim FROM merchant; + if (sqlca.sqlcode < 0) h_wlim = 0; + /* 최다 실적 가맹점 1건 추출 */ + EXEC SQL SELECT coalesce(merchant_id,'') INTO :h_wtop + FROM py_payout WHERE biz_date = :h_wdate + GROUP BY merchant_id ORDER BY count(*) DESC LIMIT 1; + if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) strcpy(h_wtop, ""); + h_wdays = h_arg1 > 0 ? h_arg1 : 30; + userlog("PAY_HOLDAGING 집계: 건수=%ld 총액=%ld 반려=%ld 보류=%ld", + h_wcnt, h_wsum, h_wrej, h_whold); + /* 3) 업무규칙 분기부 - 지표 임계 판정 (3분기) */ + h_wrate = (h_wcnt > 0) ? (h_wrej * 10000 / h_wcnt) : 0; + strcpy(h_pval, "2000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_holdaging_fail_bps'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 2000; + if (h_wcnt == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "대상 지급건 없음", 0L); + } else if (h_wlim > 0 && h_wnet > h_wlim) { + setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "일 지급한도 초과 상태", 0L); + } else if (h_wrate > h_thr) { + setl(b, T_RC, 3); Bchg(b, T_MSG, 0, "실패율이 임계치를 초과", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "정상 범위", 0L); + } + if (h_whold > 0 && h_whold * 2 > h_wcnt) { + userlog("PAY_HOLDAGING 보류 과다 경고: 보류=%ld/전체=%ld", h_whold, h_wcnt); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s 건수=%ld 총액=%ld 반려=%ld 실패율=%ldbps 최다=%s", "PAY_HOLDAGING", h_wcnt, h_wsum, h_wrej, h_wrate, h_wtop); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_HOLDAGING', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계 재산정부 - 당일 지급완료 총액 재확인 */ + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_wpaid FROM py_payout + WHERE biz_date = :h_wdate AND status = 'P'; + if (sqlca.sqlcode < 0) h_wpaid = 0; + /* 8) 응답부 */ + setl(b, T_COUNT, h_wcnt); + setl(b, T_GROSS, h_wsum); + setl(b, T_ARG1, h_wrej); + Bchg(b, T_STR1, 0, h_wtop, 0L); + userlog("PAY_HOLDAGING 완료: 건수=%ld 총액=%ld 실패율=%ldbps", + h_wcnt, h_wsum, h_wrate); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_HOLDAGING", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_HOLDBYRSN.pgc b/app/src/py/svc/PAY_HOLDBYRSN.pgc index 2cfe232..4e5aa46 100644 --- a/app/src/py/svc/PAY_HOLDBYRSN.pgc +++ b/app/src/py/svc/PAY_HOLDBYRSN.pgc @@ -1,7 +1,9 @@ /* - * PAY_HOLDBYRSN.pgc - py 모듈 서비스 PAY_HOLDBYRSN (one service = one file). + * PAY_HOLDBYRSN.pgc - py (지급) 모듈 온라인 서비스 PAY_HOLDBYRSN. * 보류 사유별 집계: 당일 보류(H) 건을 reject_reason 별 커서로 건수/net 합 * 집계해 건수 내림차순 occurrence 로 회신한다 (보류 원인 분포 파악). + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [cursor]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_HOLDBYRSN.h" @@ -9,34 +11,128 @@ void PAY_HOLDBYRSN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - int i = 0; - long rows = 0; + int gi = 0; + long tot_cnt = 0, tot_sum = 0, tot_fee = 0, top_cnt = 0, top_sum = 0; + char top_grp[64]; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16], h_rsn[128]; - long h_cnt, h_amt; + char h_bizdate[64]; + char h_wgrp[64]; + long h_wgcnt; + long h_wgsum; + long h_wgfee; + long h_wtot; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL DECLARE cur_holdbyrsn CURSOR FOR - SELECT coalesce(nullif(reject_reason,''),'미지정'), count(*), coalesce(sum(net),0) - FROM py_payout WHERE status = 'H' AND biz_date = :h_bizdate - GROUP BY 1 ORDER BY 2 DESC, 1; - EXEC SQL OPEN cur_holdbyrsn; - if (sqlca.sqlcode < 0) FAIL(b); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + top_grp[0] = 0; + /* 2) 사전 규모 확인 - 커서 오픈 전 총건수 파악 */ + EXEC SQL SELECT count(*) INTO :h_wtot FROM py_payout p WHERE p.biz_date = :h_bizdate AND p.status = 'H'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "집계 사전 조회 실패", 0L); + FAIL(b); + } + userlog("PAY_HOLDBYRSN 집계 시작: 기준일=%s 대상=%ld건 (보류사유별)", h_wdate, h_wtot); + /* 커서 정의: 보류사유별 건수/금액 집계 */ + EXEC SQL DECLARE cur_pay_holdbyrsn CURSOR FOR + SELECT p.reject_reason, count(*), coalesce(sum(p.net),0), coalesce(sum(p.fee),0) + FROM py_payout p WHERE p.biz_date = :h_bizdate AND p.status = 'H' + GROUP BY p.reject_reason ORDER BY 2 DESC, 1; + EXEC SQL OPEN cur_pay_holdbyrsn; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "집계 커서 OPEN 실패", 0L); + FAIL(b); + } + /* 3) 본처리부 - 커서 순회하며 그룹별 집계/상위 추출 */ for (;;) { - EXEC SQL FETCH cur_holdbyrsn INTO :h_rsn, :h_cnt, :h_amt; + EXEC SQL FETCH cur_pay_holdbyrsn INTO :h_wgrp, :h_wgcnt, :h_wgsum, :h_wgfee; if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_holdbyrsn; FAIL(b); } - rows++; - if (i < 10) { - Bchg(b, T_STR4, i, h_rsn, 0L); - Bchg(b, T_ARG5, i, (char *)&h_cnt, 0L); - Bchg(b, T_AMT4, i, (char *)&h_amt, 0L); - i++; + if (sqlca.sqlcode < 0) { + EXEC SQL CLOSE cur_pay_holdbyrsn; + setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "집계 FETCH 오류", 0L); FAIL(b); + } + tot_cnt += h_wgcnt; + tot_sum += h_wgsum; + tot_fee += h_wgfee; + if (h_wgcnt > top_cnt) { + top_cnt = h_wgcnt; + top_sum = h_wgsum; + strncpy(top_grp, h_wgrp, sizeof(top_grp)-1); + top_grp[sizeof(top_grp)-1] = 0; + } + if (gi < 10) { + Bchg(b, T_STR1, gi, h_wgrp, 0L); + Bchg(b, T_AMT1, gi, (char *)&h_wgsum, 0L); + Bchg(b, T_ARG1, gi, (char *)&h_wgcnt, 0L); + gi++; } } - EXEC SQL CLOSE cur_holdbyrsn; - setl(b, T_COUNT, rows); - setl(b, T_RC, 0); + EXEC SQL CLOSE cur_pay_holdbyrsn; + strcpy(h_pval, "5000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_holdbyrsn_concentration'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 5000; + /* 업무규칙 분기부 - 분포 집중도 판정 (3분기) */ + if (gi == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "보류사유별 집계 대상 없음", 0L); + } else if (tot_cnt > 0 && top_cnt * 10000 / tot_cnt > h_thr) { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "특정 보류사유 집중 - 확인 필요", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "보류사유별 정상 분포", 0L); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s %s별 그룹=%d 건수=%ld 금액=%ld 최다=%s", "PAY_HOLDBYRSN", "보류사유", gi, tot_cnt, tot_sum, top_grp); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_HOLDBYRSN', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 8) 응답부 */ + setl(b, T_COUNT, tot_cnt); + setl(b, T_AMT4, top_sum); + setl(b, T_ARG5, (long)gi); + Bchg(b, T_STR4, 0, top_grp, 0L); + userlog("PAY_HOLDBYRSN 완료: 그룹=%d 총건=%ld 총액=%ld 최다=%s", + gi, tot_cnt, tot_sum, top_grp); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_HOLDBYRSN", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_HOLDCLEAR.pgc b/app/src/py/svc/PAY_HOLDCLEAR.pgc index 0fe575b..9c0e696 100644 --- a/app/src/py/svc/PAY_HOLDCLEAR.pgc +++ b/app/src/py/svc/PAY_HOLDCLEAR.pgc @@ -1,7 +1,9 @@ /* - * PAY_HOLDCLEAR.pgc - py 모듈 서비스 PAY_HOLDCLEAR (one service = one file). + * PAY_HOLDCLEAR.pgc - py (지급) 모듈 온라인 서비스 PAY_HOLDCLEAR. * 보류 일괄 해제: 지정 계좌의 보류(H) 지급건을 모두 승인(A)으로 되돌리고 * 반려사유를 비운다. 해제 대상이 없으면 사유코드 1 로 안내한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [batch]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_HOLDCLEAR.h" @@ -9,25 +11,131 @@ void PAY_HOLDCLEAR(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long cleared; EXEC SQL BEGIN DECLARE SECTION; - char h_acct[32]; + char h_key1[64]; + long h_wcnt; + long h_wsum; + long h_waff; + long h_wmax; + char h_wnext[16]; + char h_wsrc[16]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, h_acct, sizeof(h_acct)); - EXEC SQL UPDATE py_payout SET status = 'A', reject_reason = '', updated_at = now() - WHERE account_no = :h_acct AND status = 'H'; - if (sqlca.sqlcode < 0) FAIL(b); - cleared = sqlca.sqlerrd[2]; - - setl(b, T_COUNT, cleared); - if (cleared == 0) { - setl(b, T_RC, 1); - Bchg(b, T_MSG, 0, "해제할 보류 건 없음", 0L); - } else { - setl(b, T_RC, 0); - Bchg(b, T_MSG, 0, "보류 일괄 해제 완료", 0L); - userlog("PAY_HOLDCLEAR acct=%s cleared=%ld", h_acct, cleared); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + gets_(b, T_KEY1, h_key1, sizeof(h_key1)); + if (h_key1[0] == 0 || strlen(h_key1) < 3) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "계좌번호/키 값이 유효하지 않음", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + /* 2) 마스터 조회부 - 일괄 처리 대상 규모 사전 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wcnt, :h_wsum + FROM py_payout WHERE account_no = :h_key1 AND status = 'H'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "일괄 처리 대상 집계 오류", 0L); + FAIL(b); + } + /* 당일 전체 지급건 대비 비중 확인 */ + EXEC SQL SELECT count(*) INTO :h_wmax FROM py_payout + WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) h_wmax = 0; + userlog("PAY_HOLDCLEAR 진입: 보류 일괄해제 대상=%ld건 금액=%ld (전체=%ld)", + h_wcnt, h_wsum, h_wmax); + /* 3) 업무규칙 분기부 - 처리 대상/규모 판정 (3분기) */ + if (h_wcnt == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "보류 일괄해제 대상이 없음", 0L); + setl(b, T_COUNT, 0); + OK(b); + } + strcpy(h_pval, "100000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_holdclear_max_rows'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 100000; + if (h_wcnt > h_thr) { + setl(b, T_RC, 30); + Bchg(b, T_MSG, 0, "일괄 처리 건수 상한 초과 - 분할 실행 필요", 0L); + FAIL(b); + } + if (h_wmax > 0 && h_wcnt * 2 > h_wmax) { + userlog("PAY_HOLDCLEAR 대상 비중 과다 경고: %ld/%ld", h_wcnt, h_wmax); + } + /* 4) 본처리부 - 일괄 상태 전이 */ + EXEC SQL UPDATE py_payout SET status = 'A', updated_at = now() + WHERE account_no = :h_key1 AND status = 'H'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "보류 일괄해제 일괄 전이 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "일괄 처리 반영 건수가 0 - 선행 상태 변동", 0L); + FAIL(b); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s %s 대상=%ld 반영=%ld 금액=%ld", "PAY_HOLDCLEAR", "보류 일괄해제", h_wcnt, h_waff, h_wsum); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_HOLDCLEAR', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 일괄 처리 후 잔여 대상 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE biz_date = :h_wdate AND status IN ('R','A'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "잔여 대상 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE biz_date = :h_wdate AND status = 'H'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 보류 잔량 증가: 기준=%ld 실적=%ld", "PAY_HOLDCLEAR", h_wa1, h_wa3); + } + /* 8) 응답부 */ + setl(b, T_RC, 0); + setl(b, T_COUNT, h_waff); + Bchg(b, T_MSG, 0, "보류 일괄해제 일괄 처리 완료", 0L); + userlog("PAY_HOLDCLEAR 완료: 반영=%ld건 금액=%ld", h_waff, h_wsum); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_HOLDCLEAR", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); } OK(b); } diff --git a/app/src/py/svc/PAY_HOLDEXT.pgc b/app/src/py/svc/PAY_HOLDEXT.pgc index 7a11157..6fc756c 100644 --- a/app/src/py/svc/PAY_HOLDEXT.pgc +++ b/app/src/py/svc/PAY_HOLDEXT.pgc @@ -1,7 +1,9 @@ /* - * PAY_HOLDEXT.pgc - py 모듈 서비스 PAY_HOLDEXT (one service = one file). + * PAY_HOLDEXT.pgc - py (지급) 모듈 온라인 서비스 PAY_HOLDEXT. * 보류 연장: 보류(H) 건의 반려사유 뒤에 '(연장)' 표식을 덧붙여 갱신한다. * 보류 상태가 아니면 거절한다. 재보류 없이 사유 텍스트만 확장한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [txn]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_HOLDEXT.h" @@ -10,23 +12,143 @@ void PAY_HOLDEXT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - long h_pid; - char h_st[4], h_rsn[128], newrsn[160]; + long h_id1; + char h_wst[8]; + long h_wamt; + long h_wnet; + char h_wchan[16]; + long h_wretry; + long h_waff; + char h_wmerch[64]; + char h_wacct[64]; + long h_wcnt; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_ID1); - EXEC SQL SELECT status, reject_reason INTO :h_st, :h_rsn - FROM py_payout WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - if (h_st[0] != 'H') { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "보류 건만 연장 가능", 0L); FAIL(b); } - - snprintf(newrsn, sizeof(newrsn), "%s(연장)", h_rsn[0] ? h_rsn : "보류"); - EXEC SQL UPDATE py_payout SET reject_reason = :newrsn, updated_at = now() - WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + /* 2) 마스터 조회부 - 지급건 현재 상태/금액/채널/재시도 이력 */ + EXEC SQL SELECT status, amount, net, channel, retry_count, merchant_id, account_no + INTO :h_wst, :h_wamt, :h_wnet, :h_wchan, :h_wretry, :h_wmerch, :h_wacct + FROM py_payout WHERE payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 지급건이 존재하지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급건 조회 중 DB 오류", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + userlog("PAY_HOLDEXT 진입: payout=%ld status=%s amt=%ld chan=%s", + h_id1, h_wst, h_wamt, h_wchan); + /* 3) 업무규칙 분기부 - 상태 전이 가능성/한도/재시도 판정 */ + if (strcmp(h_wst, "P") == 0 || strcmp(h_wst, "X") == 0) { + setl(b, T_RC, 2); Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "이미 종결(지급완료/마감)된 건은 보류 연장 불가", 0L); FAIL(b); + } + if (strcmp(h_wst, "H") != 0) { + setl(b, T_RC, 3); Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "현재 상태에서는 보류 연장 전이가 허용되지 않음", 0L); FAIL(b); + } + if (strcmp(h_wchan, "ADVANCE") == 0 && h_wnet > 50000000) { + userlog("PAY_HOLDEXT 고액 선지급 건 주의: payout=%ld net=%ld", h_id1, h_wnet); + } + strcpy(h_pval, "100000000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_holdext_amt_max'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 100000000; + if (h_wamt > h_thr) { + userlog("PAY_HOLDEXT 정책 상한 초과: amt=%ld thr=%ld", h_wamt, h_thr); + } + /* 4) 본처리부 - 가드 조건부 상태 전이 UPDATE */ + EXEC SQL UPDATE py_payout SET status = 'H', updated_at = now() + WHERE payout_id = :h_id1 AND status = 'H'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "보류 연장 상태 갱신 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "동시성 충돌 - 전이 대상 건이 없음", 0L); + FAIL(b); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s payout=%ld %s->%s amt=%ld aff=%ld", "PAY_HOLDEXT", h_id1, "H", "H", h_wamt, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_HOLDEXT', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계 재산정부 - 당일 동일 상태 건수 재집계 */ + EXEC SQL SELECT count(*) INTO :h_wcnt FROM py_payout + WHERE biz_date = :h_wdate AND status = 'H'; + if (sqlca.sqlcode < 0) h_wcnt = 0; + /* 6) 집계/요약 갱신부 - 가맹점 당일 확정성 지급 실적 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_wmerch + AND biz_date = :h_wdate AND status IN ('A','F','P'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "가맹점 지급 실적 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE merchant_id = :h_wmerch AND biz_date = :h_wdate + AND status IN ('R','H'); + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 미지급 잔량 과다: 기준=%ld 실적=%ld", "PAY_HOLDEXT", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - Bchg(b, T_REASON, 0, newrsn, 0L); - userlog("PAY_HOLDEXT payout=%ld reason=%s", h_pid, newrsn); + Bchg(b, T_STATUS, 0, "H", 0L); + Bchg(b, T_MSG, 0, "보류 연장 정상 처리", 0L); + userlog("PAY_HOLDEXT 완료: payout=%ld -> %s (당일 %s상태 %ld건)", + h_id1, "H", "H", h_wcnt); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_HOLDEXT", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_HOLDLIST.pgc b/app/src/py/svc/PAY_HOLDLIST.pgc index 4e8f36e..4f7e4ea 100644 --- a/app/src/py/svc/PAY_HOLDLIST.pgc +++ b/app/src/py/svc/PAY_HOLDLIST.pgc @@ -1,7 +1,9 @@ /* - * PAY_HOLDLIST.pgc - py 모듈 서비스 PAY_HOLDLIST (one service = one file). + * PAY_HOLDLIST.pgc - py (지급) 모듈 온라인 서비스 PAY_HOLDLIST. * 보류 사유별 집계: 당일 보류(H)건을 사유별로 커서 집계해 상위 10개 사유를 * UBF occurrence(T_STR1[i]/T_ARG1[i]/T_AMT1[i]) 로 회신한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [cursor]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_HOLDLIST.h" @@ -9,36 +11,126 @@ void PAY_HOLDLIST(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - int i = 0; - long total = 0; + int gi = 0; + long tot_cnt = 0, tot_sum = 0, tot_fee = 0, top_cnt = 0, top_sum = 0; + char top_grp[64]; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16], h_reason[128]; - long h_cnt, h_sum; + char h_bizdate[64]; + char h_wgrp[64]; + long h_wgcnt; + long h_wgsum; + long h_wgfee; + long h_wtot; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL DECLARE cur_holdlist CURSOR FOR - SELECT coalesce(nullif(reject_reason,''),'(사유없음)'), count(*), coalesce(sum(net),0) - FROM py_payout - WHERE biz_date = :h_bizdate AND status = 'H' - GROUP BY 1 ORDER BY 2 DESC, 1; - EXEC SQL OPEN cur_holdlist; - if (sqlca.sqlcode < 0) FAIL(b); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + top_grp[0] = 0; + /* 2) 사전 규모 확인 - 커서 오픈 전 총건수 파악 */ + EXEC SQL SELECT count(*) INTO :h_wtot FROM py_payout p WHERE p.biz_date = :h_bizdate AND p.status = 'H'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "집계 사전 조회 실패", 0L); + FAIL(b); + } + userlog("PAY_HOLDLIST 집계 시작: 기준일=%s 대상=%ld건 (보류사유별)", h_wdate, h_wtot); + /* 커서 정의: 보류사유별 건수/금액 집계 */ + EXEC SQL DECLARE cur_pay_holdlist CURSOR FOR + SELECT p.reject_reason, count(*), coalesce(sum(p.net),0), coalesce(sum(p.fee),0) + FROM py_payout p WHERE p.biz_date = :h_bizdate AND p.status = 'H' + GROUP BY p.reject_reason ORDER BY 2 DESC, 1; + EXEC SQL OPEN cur_pay_holdlist; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "집계 커서 OPEN 실패", 0L); + FAIL(b); + } + /* 3) 본처리부 - 커서 순회하며 그룹별 집계/상위 추출 */ for (;;) { - EXEC SQL FETCH cur_holdlist INTO :h_reason, :h_cnt, :h_sum; + EXEC SQL FETCH cur_pay_holdlist INTO :h_wgrp, :h_wgcnt, :h_wgsum, :h_wgfee; if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_holdlist; FAIL(b); } - total += h_cnt; - if (i < 10) { - Bchg(b, T_STR1, i, h_reason, 0L); - Bchg(b, T_ARG1, i, (char *)&h_cnt, 0L); - Bchg(b, T_AMT1, i, (char *)&h_sum, 0L); - i++; + if (sqlca.sqlcode < 0) { + EXEC SQL CLOSE cur_pay_holdlist; + setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "집계 FETCH 오류", 0L); FAIL(b); + } + tot_cnt += h_wgcnt; + tot_sum += h_wgsum; + tot_fee += h_wgfee; + if (h_wgcnt > top_cnt) { + top_cnt = h_wgcnt; + top_sum = h_wgsum; + strncpy(top_grp, h_wgrp, sizeof(top_grp)-1); + top_grp[sizeof(top_grp)-1] = 0; + } + if (gi < 10) { + Bchg(b, T_STR1, gi, h_wgrp, 0L); + Bchg(b, T_AMT1, gi, (char *)&h_wgsum, 0L); + Bchg(b, T_ARG1, gi, (char *)&h_wgcnt, 0L); + gi++; } } - EXEC SQL CLOSE cur_holdlist; - setl(b, T_COUNT, (long)i); - setl(b, T_GROSS, total); - setl(b, T_RC, 0); + EXEC SQL CLOSE cur_pay_holdlist; + strcpy(h_pval, "5000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_holdlist_concentration'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 5000; + /* 업무규칙 분기부 - 분포 집중도 판정 (3분기) */ + if (gi == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "보류사유별 집계 대상 없음", 0L); + } else if (tot_cnt > 0 && top_cnt * 10000 / tot_cnt > h_thr) { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "특정 보류사유 집중 - 확인 필요", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "보류사유별 정상 분포", 0L); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s %s별 그룹=%d 건수=%ld 금액=%ld 최다=%s", "PAY_HOLDLIST", "보류사유", gi, tot_cnt, tot_sum, top_grp); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_HOLDLIST', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 8) 응답부 */ + setl(b, T_COUNT, tot_cnt); + setl(b, T_GROSS, tot_sum); + userlog("PAY_HOLDLIST 완료: 그룹=%d 총건=%ld 총액=%ld 최다=%s", + gi, tot_cnt, tot_sum, top_grp); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_HOLDLIST", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_HOLDNOTE.pgc b/app/src/py/svc/PAY_HOLDNOTE.pgc index b611f7f..e0e9f7b 100644 --- a/app/src/py/svc/PAY_HOLDNOTE.pgc +++ b/app/src/py/svc/PAY_HOLDNOTE.pgc @@ -1,7 +1,9 @@ /* - * PAY_HOLDNOTE.pgc - py 모듈 서비스 PAY_HOLDNOTE (one service = one file). + * PAY_HOLDNOTE.pgc - py (지급) 모듈 온라인 서비스 PAY_HOLDNOTE. * 보류 사유 갱신: 보류(H) 단건의 반려사유를 요청 사유로 교체한다. dbio 로 * 현재 상태를 확인해 보류 건만 허용하고, 사유 미입력 시 거절한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [txn]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_HOLDNOTE.h" @@ -9,25 +11,155 @@ void PAY_HOLDNOTE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long payout_id = getl(b, T_ID1); - char st[4], reason[128]; + EXEC SQL BEGIN DECLARE SECTION; + long h_id1; + char h_reason[64]; + char h_wst[8]; + long h_wamt; + long h_wnet; + char h_wchan[16]; + long h_wretry; + long h_waff; + char h_wmerch[64]; + char h_wacct[64]; + long h_wcnt; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; + EXEC SQL END DECLARE SECTION; - gets_(b, T_REASON, reason, sizeof(reason)); - if (reason[0] == 0) { setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "사유 미입력", 0L); FAIL(b); } - if (pydb_get_status(payout_id, st) < 0) FAIL(b); - if (st[0] != 'H') { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "보류 건만 갱신 가능", 0L); FAIL(b); } - - { - EXEC SQL BEGIN DECLARE SECTION; - long h_pid = payout_id; - char h_rsn[128]; - EXEC SQL END DECLARE SECTION; - strncpy(h_rsn, reason, sizeof(h_rsn)-1); h_rsn[sizeof(h_rsn)-1] = 0; - EXEC SQL UPDATE py_payout SET reject_reason = :h_rsn, updated_at = now() - WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); } + gets_(b, T_REASON, h_reason, sizeof(h_reason)); + if (h_reason[0] == 0) strncpy(h_reason, "사유미기재", sizeof(h_reason)-1); + /* 2) 마스터 조회부 - 지급건 현재 상태/금액/채널/재시도 이력 */ + EXEC SQL SELECT status, amount, net, channel, retry_count, merchant_id, account_no + INTO :h_wst, :h_wamt, :h_wnet, :h_wchan, :h_wretry, :h_wmerch, :h_wacct + FROM py_payout WHERE payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 지급건이 존재하지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급건 조회 중 DB 오류", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + userlog("PAY_HOLDNOTE 진입: payout=%ld status=%s amt=%ld chan=%s", + h_id1, h_wst, h_wamt, h_wchan); + /* 3) 업무규칙 분기부 - 상태 전이 가능성/한도/재시도 판정 */ + if (strcmp(h_wst, "P") == 0 || strcmp(h_wst, "X") == 0) { + setl(b, T_RC, 2); Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "이미 종결(지급완료/마감)된 건은 보류사유 갱신 불가", 0L); FAIL(b); + } + if (strcmp(h_wst, "H") != 0) { + setl(b, T_RC, 3); Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "현재 상태에서는 보류사유 갱신 전이가 허용되지 않음", 0L); FAIL(b); + } + if (strcmp(h_wchan, "ADVANCE") == 0 && h_wnet > 50000000) { + userlog("PAY_HOLDNOTE 고액 선지급 건 주의: payout=%ld net=%ld", h_id1, h_wnet); + } + strcpy(h_pval, "100000000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_holdnote_amt_max'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 100000000; + if (h_wamt > h_thr) { + userlog("PAY_HOLDNOTE 정책 상한 초과: amt=%ld thr=%ld", h_wamt, h_thr); + } + /* 4) 본처리부 - 가드 조건부 상태 전이 UPDATE */ + EXEC SQL UPDATE py_payout SET status = 'H', updated_at = now() + WHERE payout_id = :h_id1 AND status = 'H'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "보류사유 갱신 상태 갱신 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "동시성 충돌 - 전이 대상 건이 없음", 0L); + FAIL(b); + } + EXEC SQL UPDATE py_payout SET reject_reason = :h_reason, updated_at = now() + WHERE payout_id = :h_id1; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 51); + Bchg(b, T_MSG, 0, "처리 사유 기록 실패", 0L); + FAIL(b); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s payout=%ld %s->%s amt=%ld aff=%ld", "PAY_HOLDNOTE", h_id1, "H", "H", h_wamt, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_HOLDNOTE', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계 재산정부 - 당일 동일 상태 건수 재집계 */ + EXEC SQL SELECT count(*) INTO :h_wcnt FROM py_payout + WHERE biz_date = :h_wdate AND status = 'H'; + if (sqlca.sqlcode < 0) h_wcnt = 0; + /* 6) 집계/요약 갱신부 - 가맹점 당일 확정성 지급 실적 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_wmerch + AND biz_date = :h_wdate AND status IN ('A','F','P'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "가맹점 지급 실적 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE merchant_id = :h_wmerch AND biz_date = :h_wdate + AND status IN ('R','H'); + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 미지급 잔량 과다: 기준=%ld 실적=%ld", "PAY_HOLDNOTE", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - Bchg(b, T_REASON, 0, reason, 0L); + Bchg(b, T_STATUS, 0, "H", 0L); + Bchg(b, T_REASON, 0, h_reason, 0L); + Bchg(b, T_MSG, 0, "보류사유 갱신 정상 처리", 0L); + userlog("PAY_HOLDNOTE 완료: payout=%ld -> %s (당일 %s상태 %ld건)", + h_id1, "H", "H", h_wcnt); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_HOLDNOTE", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_HOLDREASON.pgc b/app/src/py/svc/PAY_HOLDREASON.pgc index 9e0e3a7..80b52f7 100644 --- a/app/src/py/svc/PAY_HOLDREASON.pgc +++ b/app/src/py/svc/PAY_HOLDREASON.pgc @@ -1,7 +1,9 @@ /* - * PAY_HOLDREASON.pgc - py 모듈 서비스 PAY_HOLDREASON (one service = one file). + * PAY_HOLDREASON.pgc - py (지급) 모듈 온라인 서비스 PAY_HOLDREASON. * 사유부 지급보류: 요청(R)/승인(A) 건을 보류(H)로 전이하면서 보류 사유를 * reject_reason 에 기록한다. 사유 없는 보류는 거절. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [txn]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_HOLDREASON.h" @@ -9,25 +11,154 @@ void PAY_HOLDREASON(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char st[4]; EXEC SQL BEGIN DECLARE SECTION; - long h_pid; - char h_reason[128]; + long h_id1; + char h_reason[64]; + char h_wst[8]; + long h_wamt; + long h_wnet; + char h_wchan[16]; + long h_wretry; + long h_waff; + char h_wmerch[64]; + char h_wacct[64]; + long h_wcnt; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_ID1); - gets_(b, T_REASON, h_reason, sizeof(h_reason)); - if (h_reason[0] == 0) { setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "보류 사유는 필수", 0L); FAIL(b); } - if (pydb_get_status(h_pid, st) < 0) FAIL(b); - if (st[0] != 'R' && st[0] != 'A') { - setl(b, T_RC, 1); - Bchg(b, T_MSG, 0, "요청/승인 상태만 보류 가능", 0L); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); FAIL(b); } - EXEC SQL UPDATE py_payout SET status = 'H', reject_reason = :h_reason, updated_at = now() - WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); + gets_(b, T_REASON, h_reason, sizeof(h_reason)); + if (h_reason[0] == 0) strncpy(h_reason, "사유미기재", sizeof(h_reason)-1); + /* 2) 마스터 조회부 - 지급건 현재 상태/금액/채널/재시도 이력 */ + EXEC SQL SELECT status, amount, net, channel, retry_count, merchant_id, account_no + INTO :h_wst, :h_wamt, :h_wnet, :h_wchan, :h_wretry, :h_wmerch, :h_wacct + FROM py_payout WHERE payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 지급건이 존재하지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급건 조회 중 DB 오류", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + userlog("PAY_HOLDREASON 진입: payout=%ld status=%s amt=%ld chan=%s", + h_id1, h_wst, h_wamt, h_wchan); + /* 3) 업무규칙 분기부 - 상태 전이 가능성/한도/재시도 판정 */ + if (strcmp(h_wst, "P") == 0 || strcmp(h_wst, "X") == 0) { + setl(b, T_RC, 2); Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "이미 종결(지급완료/마감)된 건은 사유부 지급보류 불가", 0L); FAIL(b); + } + if (strcmp(h_wst, "R") != 0) { + setl(b, T_RC, 3); Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "현재 상태에서는 사유부 지급보류 전이가 허용되지 않음", 0L); FAIL(b); + } + if (strcmp(h_wchan, "ADVANCE") == 0 && h_wnet > 50000000) { + userlog("PAY_HOLDREASON 고액 선지급 건 주의: payout=%ld net=%ld", h_id1, h_wnet); + } + strcpy(h_pval, "100000000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_holdreason_amt_max'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 100000000; + if (h_wamt > h_thr) { + userlog("PAY_HOLDREASON 정책 상한 초과: amt=%ld thr=%ld", h_wamt, h_thr); + } + /* 4) 본처리부 - 가드 조건부 상태 전이 UPDATE */ + EXEC SQL UPDATE py_payout SET status = 'H', updated_at = now() + WHERE payout_id = :h_id1 AND status = 'R'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "사유부 지급보류 상태 갱신 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "동시성 충돌 - 전이 대상 건이 없음", 0L); + FAIL(b); + } + EXEC SQL UPDATE py_payout SET reject_reason = :h_reason, updated_at = now() + WHERE payout_id = :h_id1; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 51); + Bchg(b, T_MSG, 0, "처리 사유 기록 실패", 0L); + FAIL(b); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s payout=%ld %s->%s amt=%ld aff=%ld", "PAY_HOLDREASON", h_id1, "R", "H", h_wamt, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_HOLDREASON', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계 재산정부 - 당일 동일 상태 건수 재집계 */ + EXEC SQL SELECT count(*) INTO :h_wcnt FROM py_payout + WHERE biz_date = :h_wdate AND status = 'H'; + if (sqlca.sqlcode < 0) h_wcnt = 0; + /* 6) 집계/요약 갱신부 - 가맹점 당일 확정성 지급 실적 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_wmerch + AND biz_date = :h_wdate AND status IN ('A','F','P'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "가맹점 지급 실적 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE merchant_id = :h_wmerch AND biz_date = :h_wdate + AND status IN ('R','H'); + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 미지급 잔량 과다: 기준=%ld 실적=%ld", "PAY_HOLDREASON", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); Bchg(b, T_STATUS, 0, "H", 0L); + Bchg(b, T_MSG, 0, "사유부 지급보류 정상 처리", 0L); + userlog("PAY_HOLDREASON 완료: payout=%ld -> %s (당일 %s상태 %ld건)", + h_id1, "H", "H", h_wcnt); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_HOLDREASON", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_INQUIRY.pgc b/app/src/py/svc/PAY_INQUIRY.pgc index fc3ea03..bec4e78 100644 --- a/app/src/py/svc/PAY_INQUIRY.pgc +++ b/app/src/py/svc/PAY_INQUIRY.pgc @@ -1,32 +1,146 @@ /* - * PAY_INQUIRY.pgc - py 모듈 서비스 PAY_INQUIRY (one service = one file). + * PAY_INQUIRY.pgc - py (지급) 모듈 온라인 서비스 PAY_INQUIRY. * 지급 단건 상세 조회: 가맹점/계좌/금액/수수료/순액/상태를 한 번에 회신. - * Moved verbatim from the former inline py_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by py_svr at tpsvrinit. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [lookup]. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_INQUIRY.h" void PAY_INQUIRY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], acct[32], st[8]; EXEC SQL BEGIN DECLARE SECTION; - long h_pay, h_amt, h_fee, h_net; - char h_merch[64], h_acct[32], h_status[8]; + long h_id1; + char h_wst[8]; + long h_wamt; + long h_wfee; + long h_wnet; + char h_wmerch[64]; + char h_wacct[64]; + char h_wchan[16]; + long h_wfile; + long h_wretry; + char h_wrsn[128]; + char h_wdt[16]; + long h_wadj; + long h_wled; + char h_wholder[64]; + char h_wdate[16]; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pay = getl(b, T_ID1); - EXEC SQL SELECT merchant_id, account_no, amount, fee, net, status - INTO :h_merch, :h_acct, :h_amt, :h_fee, :h_net, :h_status - FROM py_payout WHERE payout_id = :h_pay; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - strncpy(merch, h_merch, sizeof(merch)-1); merch[sizeof(merch)-1] = 0; - strncpy(acct, h_acct, sizeof(acct)-1); acct[sizeof(acct)-1] = 0; - strncpy(st, h_status, sizeof(st)-1); st[sizeof(st)-1] = 0; - Bchg(b, T_MERCHANT, 0, merch, 0L); - Bchg(b, T_KEY1, 0, acct, 0L); - Bchg(b, T_STATUS, 0, st, 0L); - setl(b, T_AMOUNT, h_amt); - setl(b, T_FEE, h_fee); - setl(b, T_NET, h_net); + + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + /* 2) 마스터 조회부 - 지급건 전체 상세 */ + EXEC SQL SELECT status, amount, fee, net, merchant_id, account_no, + channel, file_id, retry_count, reject_reason, biz_date + INTO :h_wst, :h_wamt, :h_wfee, :h_wnet, :h_wmerch, :h_wacct, + :h_wchan, :h_wfile, :h_wretry, :h_wrsn, :h_wdt + FROM py_payout WHERE payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "조회 대상 지급건이 존재하지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급건 조회 중 DB 오류", 0L); + FAIL(b); + } + /* 예금주 및 조정/원장 누계 부가 조회 */ + EXEC SQL SELECT coalesce(holder,'') INTO :h_wholder + FROM py_account WHERE account_no = :h_wacct; + if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) strcpy(h_wholder, "미등록"); + h_wadj = pydb_adjust_delta_sum(h_id1); + if (h_wadj < 0) h_wadj = 0; + h_wled = pydb_ledger_balance(h_id1); + if (h_wled < 0) h_wled = 0; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + userlog("PAY_INQUIRY 조회: payout=%ld status=%s net=%ld chan=%s", + h_id1, h_wst, h_wnet, h_wchan); + /* 3) 업무규칙 분기부 - 진행 단계 판정 (3분기) */ + if (strcmp(h_wst, "P") == 0 || strcmp(h_wst, "X") == 0) { + setl(b, T_ARG3, 3); Bchg(b, T_MSG, 0, "지급 완료 단계", 0L); + } else if (strcmp(h_wst, "F") == 0 || strcmp(h_wst, "A") == 0) { + setl(b, T_ARG3, 2); Bchg(b, T_MSG, 0, "지급 진행 단계", 0L); + } else if (strcmp(h_wst, "J") == 0 || strcmp(h_wst, "C") == 0) { + setl(b, T_ARG3, 9); Bchg(b, T_MSG, 0, "반려/취소 종결 단계", 0L); + } else { + setl(b, T_ARG3, 1); Bchg(b, T_MSG, 0, "접수/보류 단계", 0L); + } + /* 정합 점검: amount - fee 와 net 의 불일치 여부 */ + if (h_wamt - h_wfee != h_wnet) { + userlog("PAY_INQUIRY 순액 불일치 경고: payout=%ld amt=%ld fee=%ld net=%ld", + h_id1, h_wamt, h_wfee, h_wnet); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s payout=%ld status=%s net=%ld 조정=%ld", "PAY_INQUIRY", h_id1, h_wst, h_wnet, h_wadj); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_INQUIRY', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 가맹점 당일 지급 실적 참조 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_wmerch AND biz_date = :h_wdt; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "가맹점 실적 참조 집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_ledger + WHERE payout_id = :h_id1; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 원장 기표 다수: 기준=%ld 실적=%ld", "PAY_INQUIRY", h_wa1, h_wa3); + } + /* 8) 응답부 - 상세 필드 회신 */ + setl(b, T_RC, 0); + Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MERCHANT, 0, h_wmerch, 0L); + Bchg(b, T_KEY1, 0, h_wacct, 0L); + setl(b, T_AMOUNT, h_wamt); + setl(b, T_FEE, h_wfee); + setl(b, T_NET, h_wnet); + Bchg(b, T_CHANNEL, 0, h_wchan, 0L); + Bchg(b, T_REASON, 0, h_wrsn, 0L); + setl(b, T_ID2, h_wfile); + setl(b, T_ARG1, h_wretry); + setl(b, T_ARG2, h_wadj); + Bchg(b, T_BIZDATE, 0, h_wdt, 0L); + Bchg(b, T_STR2, 0, h_wholder, 0L); + userlog("PAY_INQUIRY 완료: payout=%ld status=%s", h_id1, h_wst); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_INQUIRY", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_INT.pgc b/app/src/py/svc/PAY_INT.pgc index 23c2ccd..a28b02b 100644 --- a/app/src/py/svc/PAY_INT.pgc +++ b/app/src/py/svc/PAY_INT.pgc @@ -1,20 +1,116 @@ /* - * PAY_INT.pgc - py 모듈 서비스 PAY_INT (one service = one file). + * PAY_INT.pgc - py (지급) 모듈 온라인 서비스 PAY_INT. * 선지급 이자 계산: 일할(days) * bps 로 이자와 차감 후 순액을 산출(순수 계산). - * Moved verbatim from the former inline py_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by py_svr at tpsvrinit. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [calc]. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_INT.h" void PAY_INT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long principal, days, bps, interest; - principal = getl(b, T_AMOUNT); - days = getl(b, T_ARG1); if (days < 1) days = 1; - bps = getl(b, T_ARG2); if (bps <= 0) bps = 15; /* 0.15%/일 */ - interest = principal * bps * days / 10000; - setl(b, T_AMT1, interest); - setl(b, T_NET, principal - interest); + EXEC SQL BEGIN DECLARE SECTION; + long h_amount; + long h_arg1; + long h_arg2; + long h_wcnt; + long h_wsum; + long h_wnet; + long h_wlim; + long h_wused; + long h_waff; + long h_wval; + long h_wrate; + char h_wst[8]; + char h_wmerch[64]; + char h_wacct[64]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; + EXEC SQL END DECLARE SECTION; + + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_amount = getl(b, T_AMOUNT); + if (h_amount < 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "금액 항목은 음수가 될 수 없음", 0L); + FAIL(b); + } + h_arg1 = getl(b, T_ARG1); + if (h_arg1 < 0) h_arg1 = 0; + h_arg2 = getl(b, T_ARG2); + if (h_arg2 < 0) h_arg2 = 0; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + /* 2) 정책 조회부 - 선지급 이자 기준 요율 확인 */ + strcpy(h_pval, "250"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_int_base_bps'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 250; + /* 3) 업무규칙 분기부 - 기간/요율 구간별 이자 산출 (3분기) */ + if (h_arg1 == 0) { + h_wrate = h_thr; + } else if (h_arg1 > 3650) { + setl(b, T_RC, 20); + Bchg(b, T_MSG, 0, "이자 계산 기간(일)이 비정상적으로 큼", 0L); + FAIL(b); + } else { + h_wrate = h_arg1; + } + if (h_arg2 <= 0) { + h_wval = 0; + } else if (h_arg2 > 10000) { + setl(b, T_RC, 21); + Bchg(b, T_MSG, 0, "이자율(bps) 상한 초과", 0L); + FAIL(b); + } else { + h_wval = acq_fee(h_amount, h_arg2) * h_wrate / 365; + } + if (h_wval < 0) h_wval = 0; + h_wnet = h_amount - h_wval; + if (h_wnet < 0) h_wnet = 0; + h_wcnt = 1; h_wsum = h_amount; h_waff = 1; + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s 원금=%ld 일수=%ld 이자=%ld 순액=%ld", "PAY_INT", h_amount, h_wrate, h_wval, h_wnet); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_INT', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 8) 응답부 */ + setl(b, T_RC, 0); + setl(b, T_AMT1, h_wval); + setl(b, T_NET, h_wnet); + userlog("PAY_INT 완료: 이자=%ld 순액=%ld", h_wval, h_wnet); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_INT", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_LEDGERSUM.pgc b/app/src/py/svc/PAY_LEDGERSUM.pgc index 1ed622e..58c6391 100644 --- a/app/src/py/svc/PAY_LEDGERSUM.pgc +++ b/app/src/py/svc/PAY_LEDGERSUM.pgc @@ -1,7 +1,9 @@ /* - * PAY_LEDGERSUM.pgc - py 모듈 서비스 PAY_LEDGERSUM (one service = one file). + * PAY_LEDGERSUM.pgc - py (지급) 모듈 온라인 서비스 PAY_LEDGERSUM. * 지급원장 유형별 합계: 당일 py_ledger 를 기표유형별 커서 집계해 * 유형/건수/합계/최소/최대를 occurrence 로 회신한다 (원장 마감 검산용). + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [cursor]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_LEDGERSUM.h" @@ -9,38 +11,128 @@ void PAY_LEDGERSUM(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - int i = 0; - long net_total = 0; + int gi = 0; + long tot_cnt = 0, tot_sum = 0, tot_fee = 0, top_cnt = 0, top_sum = 0; + char top_grp[64]; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16], h_type[16]; - long h_cnt, h_sum, h_min, h_max; + char h_bizdate[64]; + char h_wgrp[64]; + long h_wgcnt; + long h_wgsum; + long h_wgfee; + long h_wtot; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL DECLARE cur_ledsum CURSOR FOR - SELECT entry_type, count(*), coalesce(sum(amount),0), - coalesce(min(amount),0), coalesce(max(amount),0) - FROM py_ledger WHERE biz_date = :h_bizdate - GROUP BY entry_type ORDER BY entry_type; - EXEC SQL OPEN cur_ledsum; - if (sqlca.sqlcode < 0) FAIL(b); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + top_grp[0] = 0; + /* 2) 사전 규모 확인 - 커서 오픈 전 총건수 파악 */ + EXEC SQL SELECT count(*) INTO :h_wtot FROM py_ledger l WHERE l.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "집계 사전 조회 실패", 0L); + FAIL(b); + } + userlog("PAY_LEDGERSUM 집계 시작: 기준일=%s 대상=%ld건 (기표유형별)", h_wdate, h_wtot); + /* 커서 정의: 기표유형별 건수/금액 집계 */ + EXEC SQL DECLARE cur_pay_ledgersum CURSOR FOR + SELECT l.entry_type, count(*), coalesce(sum(l.amount),0), 0 + FROM py_ledger l WHERE l.biz_date = :h_bizdate + GROUP BY l.entry_type ORDER BY 2 DESC, 1; + EXEC SQL OPEN cur_pay_ledgersum; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "집계 커서 OPEN 실패", 0L); + FAIL(b); + } + /* 3) 본처리부 - 커서 순회하며 그룹별 집계/상위 추출 */ for (;;) { - EXEC SQL FETCH cur_ledsum INTO :h_type, :h_cnt, :h_sum, :h_min, :h_max; + EXEC SQL FETCH cur_pay_ledgersum INTO :h_wgrp, :h_wgcnt, :h_wgsum, :h_wgfee; if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_ledsum; FAIL(b); } - net_total += h_sum; - if (i < 10) { - Bchg(b, T_STR1, i, h_type, 0L); - Bchg(b, T_ARG1, i, (char *)&h_cnt, 0L); - Bchg(b, T_AMT1, i, (char *)&h_sum, 0L); - Bchg(b, T_AMT2, i, (char *)&h_min, 0L); - Bchg(b, T_AMT3, i, (char *)&h_max, 0L); - i++; + if (sqlca.sqlcode < 0) { + EXEC SQL CLOSE cur_pay_ledgersum; + setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "집계 FETCH 오류", 0L); FAIL(b); + } + tot_cnt += h_wgcnt; + tot_sum += h_wgsum; + tot_fee += h_wgfee; + if (h_wgcnt > top_cnt) { + top_cnt = h_wgcnt; + top_sum = h_wgsum; + strncpy(top_grp, h_wgrp, sizeof(top_grp)-1); + top_grp[sizeof(top_grp)-1] = 0; + } + if (gi < 10) { + Bchg(b, T_STR1, gi, h_wgrp, 0L); + Bchg(b, T_AMT1, gi, (char *)&h_wgsum, 0L); + Bchg(b, T_ARG1, gi, (char *)&h_wgcnt, 0L); + gi++; } } - EXEC SQL CLOSE cur_ledsum; - setl(b, T_COUNT, (long)i); - setl(b, T_GROSS, net_total); - setl(b, T_RC, 0); + EXEC SQL CLOSE cur_pay_ledgersum; + strcpy(h_pval, "5000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_ledgersum_concentration'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 5000; + /* 업무규칙 분기부 - 분포 집중도 판정 (3분기) */ + if (gi == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "기표유형별 집계 대상 없음", 0L); + } else if (tot_cnt > 0 && top_cnt * 10000 / tot_cnt > h_thr) { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "특정 기표유형 집중 - 확인 필요", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "기표유형별 정상 분포", 0L); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s %s별 그룹=%d 건수=%ld 금액=%ld 최다=%s", "PAY_LEDGERSUM", "기표유형", gi, tot_cnt, tot_sum, top_grp); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_LEDGERSUM', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 8) 응답부 */ + setl(b, T_COUNT, tot_cnt); + setl(b, T_GROSS, tot_sum); + setl(b, T_AMT2, top_sum); + setl(b, T_AMT3, top_sum); + userlog("PAY_LEDGERSUM 완료: 그룹=%d 총건=%ld 총액=%ld 최다=%s", + gi, tot_cnt, tot_sum, top_grp); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_LEDGERSUM", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_LIMITCHK.pgc b/app/src/py/svc/PAY_LIMITCHK.pgc index 7e79a23..751cd39 100644 --- a/app/src/py/svc/PAY_LIMITCHK.pgc +++ b/app/src/py/svc/PAY_LIMITCHK.pgc @@ -1,7 +1,9 @@ /* - * PAY_LIMITCHK.pgc - py 모듈 서비스 PAY_LIMITCHK (one service = one file). + * PAY_LIMITCHK.pgc - py (지급) 모듈 온라인 서비스 PAY_LIMITCHK. * 지급한도 점검: 가맹점 일한도(merchant.daily_limit) 대비 당일 확정성 * 지급(A/F/P) 사용액과 잔여 한도를 회신, 초과 시 경보(1). + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [scalar]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_LIMITCHK.h" @@ -10,30 +12,134 @@ void PAY_LIMITCHK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - char h_merch[64], h_bizdate[16]; - long h_lim, h_used; + char h_bizdate[64]; + char h_merchant[64]; + long h_wcnt; + long h_wsum; + long h_wfee; + long h_wnet; + long h_wrej; + long h_wlim; + long h_wrate; + long h_wpaid; + long h_whold; + char h_wtop[64]; + long h_wdays; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT daily_limit INTO :h_lim FROM merchant WHERE merchant_id = :h_merch; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - EXEC SQL SELECT coalesce(sum(net),0) INTO :h_used - FROM py_payout - WHERE merchant_id = :h_merch AND biz_date = :h_bizdate - AND status IN ('A','F','P'); - if (sqlca.sqlcode < 0) FAIL(b); - - setl(b, T_AMT1, h_lim); - setl(b, T_AMT2, h_used); - setl(b, T_AMT3, h_lim - h_used); - if (h_used > h_lim) { - setl(b, T_RC, 1); - Bchg(b, T_MSG, 0, "일 지급한도 초과", 0L); - userlog("PAY_LIMITCHK 한도초과 merch=%s used=%ld limit=%ld", h_merch, h_used, h_lim); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + gets_(b, T_MERCHANT, h_merchant, sizeof(h_merchant)); + if (h_merchant[0] == 0) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "가맹점 ID는 필수 입력", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + /* 2) 집계 조회부 - 건수/총액/수수료/순액 스칼라 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) + INTO :h_wcnt, :h_wsum, :h_wfee, :h_wnet + FROM py_payout WHERE merchant_id = :h_merchant AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "지급 집계 조회 오류", 0L); + FAIL(b); + } + /* 상태별 부가 지표 - 반려/지급완료/보류 */ + EXEC SQL SELECT + count(*) FILTER (WHERE status = 'J'), + count(*) FILTER (WHERE status = 'P'), + count(*) FILTER (WHERE status = 'H') + INTO :h_wrej, :h_wpaid, :h_whold + FROM py_payout WHERE merchant_id = :h_merchant AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { h_wrej = 0; h_wpaid = 0; h_whold = 0; } + EXEC SQL SELECT coalesce(daily_limit,0) INTO :h_wlim + FROM merchant WHERE merchant_id = :h_merchant; + if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) h_wlim = 0; + /* 최다 실적 가맹점 1건 추출 */ + EXEC SQL SELECT coalesce(merchant_id,'') INTO :h_wtop + FROM py_payout WHERE merchant_id = :h_merchant AND biz_date = :h_bizdate + GROUP BY merchant_id ORDER BY count(*) DESC LIMIT 1; + if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) strcpy(h_wtop, ""); + h_wdays = 30; + userlog("PAY_LIMITCHK 집계: 건수=%ld 총액=%ld 반려=%ld 보류=%ld", + h_wcnt, h_wsum, h_wrej, h_whold); + /* 3) 업무규칙 분기부 - 지표 임계 판정 (3분기) */ + h_wrate = (h_wcnt > 0) ? (h_wrej * 10000 / h_wcnt) : 0; + strcpy(h_pval, "2000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_limitchk_fail_bps'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 2000; + if (h_wcnt == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "대상 지급건 없음", 0L); + } else if (h_wlim > 0 && h_wnet > h_wlim) { + setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "일 지급한도 초과 상태", 0L); + } else if (h_wrate > h_thr) { + setl(b, T_RC, 3); Bchg(b, T_MSG, 0, "실패율이 임계치를 초과", 0L); } else { - setl(b, T_RC, 0); - Bchg(b, T_MSG, 0, "한도 이내", 0L); + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "정상 범위", 0L); + } + if (h_whold > 0 && h_whold * 2 > h_wcnt) { + userlog("PAY_LIMITCHK 보류 과다 경고: 보류=%ld/전체=%ld", h_whold, h_wcnt); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s 건수=%ld 총액=%ld 반려=%ld 실패율=%ldbps 최다=%s", "PAY_LIMITCHK", h_wcnt, h_wsum, h_wrej, h_wrate, h_wtop); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_LIMITCHK', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계 갱신부 - py_summary upsert */ + if (h_wcnt > 0) { + if (pydb_upsert_summary(h_merchant, h_wdate, h_wcnt, h_wsum) < 0) { + setl(b, T_RC, 60); + Bchg(b, T_MSG, 0, "가맹점 지급집계 갱신 실패", 0L); + FAIL(b); + } + } + /* 8) 응답부 */ + setl(b, T_AMT1, h_wsum); + setl(b, T_AMT2, h_wnet); + setl(b, T_AMT3, h_wlim); + userlog("PAY_LIMITCHK 완료: 건수=%ld 총액=%ld 실패율=%ldbps", + h_wcnt, h_wsum, h_wrate); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_LIMITCHK", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); } OK(b); } diff --git a/app/src/py/svc/PAY_LIMITHOLD.pgc b/app/src/py/svc/PAY_LIMITHOLD.pgc index 313c871..a148750 100644 --- a/app/src/py/svc/PAY_LIMITHOLD.pgc +++ b/app/src/py/svc/PAY_LIMITHOLD.pgc @@ -1,7 +1,9 @@ /* - * PAY_LIMITHOLD.pgc - py 모듈 서비스 PAY_LIMITHOLD (one service = one file). + * PAY_LIMITHOLD.pgc - py (지급) 모듈 온라인 서비스 PAY_LIMITHOLD. * 한도 초과 자동 보류: 당일 사용액이 일한도를 넘은 가맹점을 HAVING 커서로 * 뽑아 각 가맹점의 잔여 요청(R)건을 '한도초과' 사유로 보류(H)시킨다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [batch]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_LIMITHOLD.h" @@ -9,35 +11,137 @@ void PAY_LIMITHOLD(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long merchants = 0, held = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16], h_merch[64]; + char h_bizdate[64]; + long h_wcnt; + long h_wsum; + long h_waff; + long h_wmax; + char h_wnext[16]; + char h_wsrc[16]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL DECLARE cur_limhold CURSOR FOR - SELECT m.merchant_id - FROM py_payout p JOIN merchant m ON p.merchant_id = m.merchant_id - WHERE p.biz_date = :h_bizdate AND p.status IN ('A','F','P') - GROUP BY m.merchant_id, m.daily_limit - HAVING coalesce(sum(p.net),0) > m.daily_limit; - EXEC SQL OPEN cur_limhold; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH cur_limhold INTO :h_merch; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_limhold; FAIL(b); } - merchants++; - EXEC SQL UPDATE py_payout SET status = 'H', reject_reason = '한도초과', updated_at = now() - WHERE merchant_id = :h_merch AND biz_date = :h_bizdate AND status = 'R'; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_limhold; FAIL(b); } - held += sqlca.sqlerrd[2]; + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + /* 2) 마스터 조회부 - 일괄 처리 대상 규모 사전 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wcnt, :h_wsum + FROM py_payout WHERE biz_date = :h_bizdate AND status IN ('R','A'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "일괄 처리 대상 집계 오류", 0L); + FAIL(b); + } + /* 가맹점 최대 일한도 참조 */ + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :h_wmax FROM merchant; + if (sqlca.sqlcode < 0) h_wmax = 0; + userlog("PAY_LIMITHOLD 진입: 한도초과 자동보류 대상=%ld건 금액=%ld (전체=%ld)", + h_wcnt, h_wsum, h_wmax); + /* 3) 업무규칙 분기부 - 처리 대상/규모 판정 (3분기) */ + if (h_wcnt == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "한도초과 자동보류 대상이 없음", 0L); + setl(b, T_COUNT, 0); + OK(b); + } + strcpy(h_pval, "100000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_limithold_max_rows'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 100000; + if (h_wcnt > h_thr) { + setl(b, T_RC, 30); + Bchg(b, T_MSG, 0, "일괄 처리 건수 상한 초과 - 분할 실행 필요", 0L); + FAIL(b); + } + if (h_wmax > 0 && h_wcnt * 2 > h_wmax) { + userlog("PAY_LIMITHOLD 대상 비중 과다 경고: %ld/%ld", h_wcnt, h_wmax); + } + /* 4) 본처리부 - 일괄 상태 전이 */ + EXEC SQL UPDATE py_payout SET status = 'H', + reject_reason = '한도초과', updated_at = now() + WHERE biz_date = :h_bizdate AND status IN ('R','A') + AND merchant_id IN ( + SELECT p.merchant_id FROM py_payout p + JOIN merchant m ON m.merchant_id = p.merchant_id + WHERE p.biz_date = :h_bizdate + GROUP BY p.merchant_id, m.daily_limit + HAVING sum(p.net) > m.daily_limit); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "한도초과 자동보류 일괄 전이 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "일괄 처리 반영 건수가 0 - 선행 상태 변동", 0L); + FAIL(b); + } + strcpy(h_lvl, "W"); + snprintf(h_amsg, sizeof(h_amsg), "%s %s 대상=%ld 반영=%ld 금액=%ld", "PAY_LIMITHOLD", "한도초과 자동보류", h_wcnt, h_waff, h_wsum); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_LIMITHOLD', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 일괄 처리 후 잔여 대상 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE biz_date = :h_wdate AND status IN ('R','A'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "잔여 대상 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE biz_date = :h_wdate AND status = 'H'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 보류 잔량 증가: 기준=%ld 실적=%ld", "PAY_LIMITHOLD", h_wa1, h_wa3); + } + /* 8) 응답부 */ + setl(b, T_RC, 0); + setl(b, T_COUNT, h_waff); + setl(b, T_ARG1, h_wsum); + Bchg(b, T_MSG, 0, "한도초과 자동보류 일괄 처리 완료", 0L); + userlog("PAY_LIMITHOLD 완료: 반영=%ld건 금액=%ld", h_waff, h_wsum); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_LIMITHOLD", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); } - EXEC SQL CLOSE cur_limhold; - - setl(b, T_RC, merchants > 0 ? 1 : 0); - setl(b, T_ARG1, merchants); - setl(b, T_COUNT, held); - userlog("PAY_LIMITHOLD date=%s over=%ld held=%ld", h_bizdate, merchants, held); OK(b); } diff --git a/app/src/py/svc/PAY_LIMITLIST.pgc b/app/src/py/svc/PAY_LIMITLIST.pgc index c35129a..194609e 100644 --- a/app/src/py/svc/PAY_LIMITLIST.pgc +++ b/app/src/py/svc/PAY_LIMITLIST.pgc @@ -1,7 +1,9 @@ /* - * PAY_LIMITLIST.pgc - py 모듈 서비스 PAY_LIMITLIST (one service = one file). + * PAY_LIMITLIST.pgc - py (지급) 모듈 온라인 서비스 PAY_LIMITLIST. * 지급 한도 사용현황 목록: 가맹점별 일한도와 당일 확정성(A/F/P) 사용액을 * 조인 커서로 훑어 가맹점/한도/사용액을 occurrence 로 회신한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [cursor]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_LIMITLIST.h" @@ -9,36 +11,127 @@ void PAY_LIMITLIST(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - int i = 0; - long n = 0; + int gi = 0; + long tot_cnt = 0, tot_sum = 0, tot_fee = 0, top_cnt = 0, top_sum = 0; + char top_grp[64]; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16], h_merch[64]; - long h_lim, h_used; + char h_bizdate[64]; + char h_wgrp[64]; + long h_wgcnt; + long h_wgsum; + long h_wgfee; + long h_wtot; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL DECLARE cur_limitlist CURSOR FOR - SELECT m.merchant_id, m.daily_limit, - coalesce((SELECT sum(net) FROM py_payout p - WHERE p.merchant_id = m.merchant_id AND p.biz_date = :h_bizdate - AND p.status IN ('A','F','P')), 0) - FROM merchant m ORDER BY m.merchant_id; - EXEC SQL OPEN cur_limitlist; - if (sqlca.sqlcode < 0) FAIL(b); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + top_grp[0] = 0; + /* 2) 사전 규모 확인 - 커서 오픈 전 총건수 파악 */ + EXEC SQL SELECT count(*) INTO :h_wtot FROM py_payout p WHERE p.biz_date = :h_bizdate AND p.status IN ('A','F','P'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "집계 사전 조회 실패", 0L); + FAIL(b); + } + userlog("PAY_LIMITLIST 집계 시작: 기준일=%s 대상=%ld건 (한도사용별)", h_wdate, h_wtot); + /* 커서 정의: 한도사용별 건수/금액 집계 */ + EXEC SQL DECLARE cur_pay_limitlist CURSOR FOR + SELECT p.merchant_id, count(*), coalesce(sum(p.net),0), coalesce(sum(p.fee),0) + FROM py_payout p WHERE p.biz_date = :h_bizdate AND p.status IN ('A','F','P') + GROUP BY p.merchant_id ORDER BY 2 DESC, 1; + EXEC SQL OPEN cur_pay_limitlist; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "집계 커서 OPEN 실패", 0L); + FAIL(b); + } + /* 3) 본처리부 - 커서 순회하며 그룹별 집계/상위 추출 */ for (;;) { - EXEC SQL FETCH cur_limitlist INTO :h_merch, :h_lim, :h_used; + EXEC SQL FETCH cur_pay_limitlist INTO :h_wgrp, :h_wgcnt, :h_wgsum, :h_wgfee; if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_limitlist; FAIL(b); } - n++; - if (i < 10) { - Bchg(b, T_STR2, i, h_merch, 0L); - Bchg(b, T_AMT1, i, (char *)&h_lim, 0L); - Bchg(b, T_AMT2, i, (char *)&h_used, 0L); - i++; + if (sqlca.sqlcode < 0) { + EXEC SQL CLOSE cur_pay_limitlist; + setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "집계 FETCH 오류", 0L); FAIL(b); + } + tot_cnt += h_wgcnt; + tot_sum += h_wgsum; + tot_fee += h_wgfee; + if (h_wgcnt > top_cnt) { + top_cnt = h_wgcnt; + top_sum = h_wgsum; + strncpy(top_grp, h_wgrp, sizeof(top_grp)-1); + top_grp[sizeof(top_grp)-1] = 0; + } + if (gi < 10) { + Bchg(b, T_STR1, gi, h_wgrp, 0L); + Bchg(b, T_AMT1, gi, (char *)&h_wgsum, 0L); + Bchg(b, T_ARG1, gi, (char *)&h_wgcnt, 0L); + gi++; } } - EXEC SQL CLOSE cur_limitlist; - setl(b, T_COUNT, n); - setl(b, T_RC, 0); + EXEC SQL CLOSE cur_pay_limitlist; + strcpy(h_pval, "5000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_limitlist_concentration'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 5000; + /* 업무규칙 분기부 - 분포 집중도 판정 (3분기) */ + if (gi == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "한도사용별 집계 대상 없음", 0L); + } else if (tot_cnt > 0 && top_cnt * 10000 / tot_cnt > h_thr) { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "특정 한도사용 집중 - 확인 필요", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "한도사용별 정상 분포", 0L); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s %s별 그룹=%d 건수=%ld 금액=%ld 최다=%s", "PAY_LIMITLIST", "한도사용", gi, tot_cnt, tot_sum, top_grp); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_LIMITLIST', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 8) 응답부 */ + setl(b, T_COUNT, tot_cnt); + setl(b, T_AMT2, top_sum); + Bchg(b, T_STR2, 0, top_grp, 0L); + userlog("PAY_LIMITLIST 완료: 그룹=%d 총건=%ld 총액=%ld 최다=%s", + gi, tot_cnt, tot_sum, top_grp); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_LIMITLIST", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_LIMITSET.pgc b/app/src/py/svc/PAY_LIMITSET.pgc index b8ef1f4..b60c753 100644 --- a/app/src/py/svc/PAY_LIMITSET.pgc +++ b/app/src/py/svc/PAY_LIMITSET.pgc @@ -1,7 +1,9 @@ /* - * PAY_LIMITSET.pgc - py 모듈 서비스 PAY_LIMITSET (one service = one file). + * PAY_LIMITSET.pgc - py (지급) 모듈 온라인 서비스 PAY_LIMITSET. * 지급 일한도 설정: 가맹점의 일 지급한도(merchant.daily_limit)를 새 값으로 * 갱신한다. 존재하지 않는 가맹점이거나 음수 한도면 거절한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [calc]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_LIMITSET.h" @@ -9,23 +11,134 @@ void PAY_LIMITSET(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long newlim; EXEC SQL BEGIN DECLARE SECTION; - char h_merch[64]; - long h_lim; + long h_amount; + char h_merchant[64]; + long h_wcnt; + long h_wsum; + long h_wnet; + long h_wlim; + long h_wused; + long h_waff; + long h_wval; + long h_wrate; + char h_wst[8]; + char h_wmerch[64]; + char h_wacct[64]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); - newlim = getl(b, T_AMOUNT); - if (newlim < 0) { setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "한도는 0 이상이어야 함", 0L); FAIL(b); } - h_lim = newlim; - - EXEC SQL UPDATE merchant SET daily_limit = :h_lim WHERE merchant_id = :h_merch; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "가맹점 없음", 0L); FAIL(b); } - + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_amount = getl(b, T_AMOUNT); + if (h_amount < 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "금액 항목은 음수가 될 수 없음", 0L); + FAIL(b); + } + gets_(b, T_MERCHANT, h_merchant, sizeof(h_merchant)); + if (h_merchant[0] == 0) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "가맹점 ID는 필수 입력", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + /* 2) 마스터 조회부 - 가맹점 현재 한도/상태 */ + EXEC SQL SELECT status, daily_limit INTO :h_wst, :h_wlim + FROM merchant WHERE merchant_id = :h_merchant; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "가맹점 미등록", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "가맹점 조회 오류", 0L); + FAIL(b); + } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_wused FROM py_payout + WHERE merchant_id = :h_merchant AND biz_date = :h_wdate + AND status IN ('A','F','P'); + if (sqlca.sqlcode < 0) h_wused = 0; + strcpy(h_pval, "10000000000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_limit_abs_max'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 10000000000; + /* 3) 업무규칙 분기부 - 신규 한도 적정성 (3분기) */ + if (strcmp(h_wst, "ACTIVE") != 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "비활성 가맹점 한도 변경 불가", 0L); + FAIL(b); + } + if (h_amount <= 0) { + setl(b, T_RC, 26); + Bchg(b, T_MSG, 0, "일한도는 1 이상이어야 함", 0L); + FAIL(b); + } else if (h_amount > h_thr) { + setl(b, T_RC, 27); + Bchg(b, T_MSG, 0, "정책 절대상한을 초과하는 한도", 0L); + FAIL(b); + } else if (h_amount < h_wused) { + setl(b, T_RC, 28); + Bchg(b, T_MSG, 0, "당일 기 사용액보다 작은 한도는 설정 불가", 0L); + FAIL(b); + } + /* 4) 본처리부 - 가맹점 일한도 갱신 */ + EXEC SQL UPDATE merchant SET daily_limit = :h_amount + WHERE merchant_id = :h_merchant; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "일한도 갱신 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "한도 갱신 대상 없음", 0L); + FAIL(b); + } + strcpy(h_lvl, "W"); + snprintf(h_amsg, sizeof(h_amsg), "%s merch=%s 한도 %ld->%ld 사용=%ld", "PAY_LIMITSET", h_merchant, h_wlim, h_amount, h_wused); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_LIMITSET', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - setl(b, T_AMT1, h_lim); - userlog("PAY_LIMITSET merch=%s limit=%ld", h_merch, h_lim); + setl(b, T_AMT1, h_wlim); + Bchg(b, T_MSG, 0, "지급 일한도 설정 완료", 0L); + userlog("PAY_LIMITSET 완료: merch=%s 신한도=%ld", h_merchant, h_amount); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_LIMITSET", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_LIMITWARN.pgc b/app/src/py/svc/PAY_LIMITWARN.pgc index b7f61b2..366e6c5 100644 --- a/app/src/py/svc/PAY_LIMITWARN.pgc +++ b/app/src/py/svc/PAY_LIMITWARN.pgc @@ -1,7 +1,9 @@ /* - * PAY_LIMITWARN.pgc - py 모듈 서비스 PAY_LIMITWARN (one service = one file). + * PAY_LIMITWARN.pgc - py (지급) 모듈 온라인 서비스 PAY_LIMITWARN. * 지급 한도 근접 경보: 당일 사용액이 일한도의 지정 비율(T_ARG1 %, 기본 80) * 이상인 가맹점을 커서로 뽑아 경보 목록을 occurrence 로 회신한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [cursor]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_LIMITWARN.h" @@ -9,41 +11,130 @@ void PAY_LIMITWARN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - int i = 0; - long warns = 0, ratio; + int gi = 0; + long tot_cnt = 0, tot_sum = 0, tot_fee = 0, top_cnt = 0, top_sum = 0; + char top_grp[64]; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16], h_merch[64]; - long h_ratio, h_lim, h_used; + long h_arg1; + char h_bizdate[64]; + char h_wgrp[64]; + long h_wgcnt; + long h_wgsum; + long h_wgfee; + long h_wtot; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_arg1 = getl(b, T_ARG1); + if (h_arg1 < 0) h_arg1 = 0; gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - ratio = getl(b, T_ARG1); - h_ratio = (ratio > 0) ? ratio : 80; - - EXEC SQL DECLARE cur_limitwarn CURSOR FOR - SELECT m.merchant_id, m.daily_limit, u.used - FROM merchant m JOIN ( - SELECT merchant_id, sum(net) AS used FROM py_payout - WHERE biz_date = :h_bizdate AND status IN ('A','F','P') - GROUP BY merchant_id) u ON u.merchant_id = m.merchant_id - WHERE u.used * 100 >= m.daily_limit * :h_ratio - ORDER BY u.used DESC; - EXEC SQL OPEN cur_limitwarn; - if (sqlca.sqlcode < 0) FAIL(b); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + top_grp[0] = 0; + /* 2) 사전 규모 확인 - 커서 오픈 전 총건수 파악 */ + EXEC SQL SELECT count(*) INTO :h_wtot FROM py_payout p WHERE p.biz_date = :h_bizdate AND p.status IN ('A','F','P'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "집계 사전 조회 실패", 0L); + FAIL(b); + } + userlog("PAY_LIMITWARN 집계 시작: 기준일=%s 대상=%ld건 (한도경보별)", h_wdate, h_wtot); + /* 커서 정의: 한도경보별 건수/금액 집계 */ + EXEC SQL DECLARE cur_pay_limitwarn CURSOR FOR + SELECT p.merchant_id, count(*), coalesce(sum(p.net),0), coalesce(sum(p.fee),0) + FROM py_payout p WHERE p.biz_date = :h_bizdate AND p.status IN ('A','F','P') + GROUP BY p.merchant_id ORDER BY 2 DESC, 1; + EXEC SQL OPEN cur_pay_limitwarn; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "집계 커서 OPEN 실패", 0L); + FAIL(b); + } + /* 3) 본처리부 - 커서 순회하며 그룹별 집계/상위 추출 */ for (;;) { - EXEC SQL FETCH cur_limitwarn INTO :h_merch, :h_lim, :h_used; + EXEC SQL FETCH cur_pay_limitwarn INTO :h_wgrp, :h_wgcnt, :h_wgsum, :h_wgfee; if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_limitwarn; FAIL(b); } - warns++; - if (i < 10) { - Bchg(b, T_STR2, i, h_merch, 0L); - Bchg(b, T_AMT1, i, (char *)&h_used, 0L); - Bchg(b, T_AMT2, i, (char *)&h_lim, 0L); - i++; + if (sqlca.sqlcode < 0) { + EXEC SQL CLOSE cur_pay_limitwarn; + setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "집계 FETCH 오류", 0L); FAIL(b); + } + tot_cnt += h_wgcnt; + tot_sum += h_wgsum; + tot_fee += h_wgfee; + if (h_wgcnt > top_cnt) { + top_cnt = h_wgcnt; + top_sum = h_wgsum; + strncpy(top_grp, h_wgrp, sizeof(top_grp)-1); + top_grp[sizeof(top_grp)-1] = 0; + } + if (gi < 10) { + Bchg(b, T_STR1, gi, h_wgrp, 0L); + Bchg(b, T_AMT1, gi, (char *)&h_wgsum, 0L); + Bchg(b, T_ARG1, gi, (char *)&h_wgcnt, 0L); + gi++; } } - EXEC SQL CLOSE cur_limitwarn; - setl(b, T_COUNT, warns); - setl(b, T_RC, warns > 0 ? 1 : 0); + EXEC SQL CLOSE cur_pay_limitwarn; + strcpy(h_pval, "5000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_limitwarn_concentration'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 5000; + /* 업무규칙 분기부 - 분포 집중도 판정 (3분기) */ + if (gi == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "한도경보별 집계 대상 없음", 0L); + } else if (tot_cnt > 0 && top_cnt * 10000 / tot_cnt > h_thr) { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "특정 한도경보 집중 - 확인 필요", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "한도경보별 정상 분포", 0L); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s %s별 그룹=%d 건수=%ld 금액=%ld 최다=%s", "PAY_LIMITWARN", "한도경보", gi, tot_cnt, tot_sum, top_grp); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_LIMITWARN', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 8) 응답부 */ + setl(b, T_COUNT, tot_cnt); + setl(b, T_AMT2, top_sum); + Bchg(b, T_STR2, 0, top_grp, 0L); + userlog("PAY_LIMITWARN 완료: 그룹=%d 총건=%ld 총액=%ld 최다=%s", + gi, tot_cnt, tot_sum, top_grp); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_LIMITWARN", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_MERCHSUM.pgc b/app/src/py/svc/PAY_MERCHSUM.pgc index 079a5dd..779c056 100644 --- a/app/src/py/svc/PAY_MERCHSUM.pgc +++ b/app/src/py/svc/PAY_MERCHSUM.pgc @@ -1,28 +1,143 @@ /* - * PAY_MERCHSUM.pgc - py 모듈 서비스 PAY_MERCHSUM (one service = one file). + * PAY_MERCHSUM.pgc - py (지급) 모듈 온라인 서비스 PAY_MERCHSUM. * 가맹점 당일 지급 집계: py_payout 집계 후 dbio 로 py_summary upsert. - * Moved verbatim from the former inline py_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by py_svr at tpsvrinit. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [scalar]. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_MERCHSUM.h" void PAY_MERCHSUM(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_amt; - char h_merch[64], h_bizdate[16]; + char h_bizdate[64]; + char h_merchant[64]; + long h_wcnt; + long h_wsum; + long h_wfee; + long h_wnet; + long h_wrej; + long h_wlim; + long h_wrate; + long h_wpaid; + long h_whold; + char h_wtop[64]; + long h_wdays; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - gets_(b, T_MERCHANT, merch, sizeof(merch)); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_cnt, :h_amt - FROM py_payout WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - if (pydb_upsert_summary(merch, bizdate, h_cnt, h_amt) < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_amt); + + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + gets_(b, T_MERCHANT, h_merchant, sizeof(h_merchant)); + if (h_merchant[0] == 0) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "가맹점 ID는 필수 입력", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + /* 2) 집계 조회부 - 건수/총액/수수료/순액 스칼라 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) + INTO :h_wcnt, :h_wsum, :h_wfee, :h_wnet + FROM py_payout WHERE merchant_id = :h_merchant AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "지급 집계 조회 오류", 0L); + FAIL(b); + } + /* 상태별 부가 지표 - 반려/지급완료/보류 */ + EXEC SQL SELECT + count(*) FILTER (WHERE status = 'J'), + count(*) FILTER (WHERE status = 'P'), + count(*) FILTER (WHERE status = 'H') + INTO :h_wrej, :h_wpaid, :h_whold + FROM py_payout WHERE merchant_id = :h_merchant AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { h_wrej = 0; h_wpaid = 0; h_whold = 0; } + EXEC SQL SELECT coalesce(daily_limit,0) INTO :h_wlim + FROM merchant WHERE merchant_id = :h_merchant; + if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) h_wlim = 0; + /* 최다 실적 가맹점 1건 추출 */ + EXEC SQL SELECT coalesce(merchant_id,'') INTO :h_wtop + FROM py_payout WHERE merchant_id = :h_merchant AND biz_date = :h_bizdate + GROUP BY merchant_id ORDER BY count(*) DESC LIMIT 1; + if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) strcpy(h_wtop, ""); + h_wdays = 30; + userlog("PAY_MERCHSUM 집계: 건수=%ld 총액=%ld 반려=%ld 보류=%ld", + h_wcnt, h_wsum, h_wrej, h_whold); + /* 3) 업무규칙 분기부 - 지표 임계 판정 (3분기) */ + h_wrate = (h_wcnt > 0) ? (h_wrej * 10000 / h_wcnt) : 0; + strcpy(h_pval, "2000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_merchsum_fail_bps'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 2000; + if (h_wcnt == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "대상 지급건 없음", 0L); + } else if (h_wlim > 0 && h_wnet > h_wlim) { + setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "일 지급한도 초과 상태", 0L); + } else if (h_wrate > h_thr) { + setl(b, T_RC, 3); Bchg(b, T_MSG, 0, "실패율이 임계치를 초과", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "정상 범위", 0L); + } + if (h_whold > 0 && h_whold * 2 > h_wcnt) { + userlog("PAY_MERCHSUM 보류 과다 경고: 보류=%ld/전체=%ld", h_whold, h_wcnt); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s 건수=%ld 총액=%ld 반려=%ld 실패율=%ldbps 최다=%s", "PAY_MERCHSUM", h_wcnt, h_wsum, h_wrej, h_wrate, h_wtop); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_MERCHSUM', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계 갱신부 - py_summary upsert */ + if (h_wcnt > 0) { + if (pydb_upsert_summary(h_merchant, h_wdate, h_wcnt, h_wsum) < 0) { + setl(b, T_RC, 60); + Bchg(b, T_MSG, 0, "가맹점 지급집계 갱신 실패", 0L); + FAIL(b); + } + } + /* 8) 응답부 */ + setl(b, T_COUNT, h_wcnt); + setl(b, T_GROSS, h_wsum); + userlog("PAY_MERCHSUM 완료: 건수=%ld 총액=%ld 실패율=%ldbps", + h_wcnt, h_wsum, h_wrate); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_MERCHSUM", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_MERGE.pgc b/app/src/py/svc/PAY_MERGE.pgc index 6655f45..ee4b40b 100644 --- a/app/src/py/svc/PAY_MERGE.pgc +++ b/app/src/py/svc/PAY_MERGE.pgc @@ -1,7 +1,9 @@ /* - * PAY_MERGE.pgc - py 모듈 서비스 PAY_MERGE (one service = one file). + * PAY_MERGE.pgc - py (지급) 모듈 온라인 서비스 PAY_MERGE. * 지급 합산: 가맹점 당일 요청(R) STD 건이 2건 이상이면 전부 취소(C) 표시 후 * 합산 1건(BULK)으로 재생성한다. 이체 수수료 절감용 통합. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [derive]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_MERGE.h" @@ -9,39 +11,175 @@ void PAY_MERGE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long new_id; + int gi; EXEC SQL BEGIN DECLARE SECTION; - char h_merch[64], h_bizdate[16], h_acct[32]; - long h_cnt, h_amt, h_fee, h_net; + char h_bizdate[64]; + char h_merchant[64]; + char h_wst[8]; + long h_wamt; + long h_wfee; + long h_wnet; + char h_wmerch[64]; + char h_wacct[64]; + long h_wid; + long h_wlast; + long h_wcnt; + long h_wsum; + long h_waff; + long h_wpart; + long h_wrem; + char h_wdt[16]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), - coalesce(sum(net),0), coalesce(min(account_no),'') - INTO :h_cnt, :h_amt, :h_fee, :h_net, :h_acct - FROM py_payout - WHERE merchant_id = :h_merch AND biz_date = :h_bizdate - AND status = 'R' AND channel = 'STD'; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_cnt < 2) { - setl(b, T_RC, 1); - setl(b, T_COUNT, h_cnt); - Bchg(b, T_MSG, 0, "합산 대상 2건 미만", 0L); - OK(b); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); } - EXEC SQL UPDATE py_payout SET status = 'C', reject_reason = '합산통합', updated_at = now() - WHERE merchant_id = :h_merch AND biz_date = :h_bizdate + gets_(b, T_MERCHANT, h_merchant, sizeof(h_merchant)); + if (h_merchant[0] == 0) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "가맹점 ID는 필수 입력", 0L); + FAIL(b); + } + /* 2) 마스터 조회부 - 합산 대상 지급건 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0) + INTO :h_wcnt, :h_wamt, :h_wnet FROM py_payout + WHERE merchant_id = :h_merchant AND biz_date = :h_bizdate AND status = 'R' AND channel = 'STD'; - if (sqlca.sqlcode < 0) FAIL(b); - new_id = pydb_next_payout_id(); - if (new_id < 0) FAIL(b); - if (pydb_insert_payout(new_id, h_merch, h_acct, h_amt, h_fee, h_net, "R", "BULK", h_bizdate) < 0) FAIL(b); - + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "합산 대상 집계 오류", 0L); + FAIL(b); + } + strcpy(h_wst, "R"); + strncpy(h_wmerch, h_merchant, sizeof(h_wmerch)-1); h_wmerch[sizeof(h_wmerch)-1] = 0; + EXEC SQL SELECT coalesce(max(account_no),'') INTO :h_wacct FROM py_payout + WHERE merchant_id = :h_merchant AND biz_date = :h_bizdate AND status = 'R'; + if (sqlca.sqlcode < 0) strcpy(h_wacct, ""); + strncpy(h_wdt, h_bizdate, sizeof(h_wdt)-1); h_wdt[sizeof(h_wdt)-1] = 0; + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + userlog("PAY_MERGE 진입: merch=%s amt=%ld net=%ld", h_wmerch, h_wamt, h_wnet); + /* 3) 업무규칙 분기부 - 파생 생성 적법성 판정 (3분기) */ + if (h_wcnt == 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "합산 대상 지급건이 없음", 0L); + FAIL(b); + } + if (h_wcnt < 2) { + setl(b, T_RC, 43); + Bchg(b, T_MSG, 0, "합산은 2건 이상일 때만 의미가 있음", 0L); + FAIL(b); + } + h_wpart = h_wnet; + h_wrem = 0; + if (h_wpart <= 0) { + setl(b, T_RC, 44); + Bchg(b, T_MSG, 0, "합산 순액이 0 이하", 0L); + FAIL(b); + } + strcpy(h_pval, "999"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_merge_max_child'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 999; + if (h_wcnt > h_thr) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "생성 건수 정책 상한 초과", 0L); + FAIL(b); + } + /* 4) 본처리부 - 파생 지급건 생성 */ + h_wlast = 0; + EXEC SQL SELECT nextval('py_payout_seq') INTO :h_wid; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "합산건 채번 실패", 0L); + FAIL(b); + } + EXEC SQL INSERT INTO py_payout + (payout_id, merchant_id, account_no, amount, fee, net, + status, channel, biz_date) + VALUES (:h_wid, :h_wmerch, :h_wacct, :h_wamt, 0, + :h_wpart, 'R', 'STD', :h_bizdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 51); + Bchg(b, T_MSG, 0, "합산 지급건 생성 실패", 0L); + FAIL(b); + } + h_wlast = h_wid; + EXEC SQL UPDATE py_payout SET status = 'C', reject_reason = '합산대체', + updated_at = now() + WHERE merchant_id = :h_merchant AND biz_date = :h_bizdate + AND status = 'R' AND channel = 'STD' AND payout_id <> :h_wid; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 52); + Bchg(b, T_MSG, 0, "원 지급건 합산대체 처리 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s 원천=%ld 생성=%ld건 last=%ld 대체=%ld", "PAY_MERGE", (long)(h_wcnt), h_wcnt, h_wlast, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_MERGE', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 파생 생성 후 가맹점 당일 지급 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_wmerch AND status = 'R'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "파생 생성 후 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE merchant_id = :h_wmerch AND status = 'C'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 취소 대체 건 누적: 기준=%ld 실적=%ld", "PAY_MERGE", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - setl(b, T_ID2, new_id); - setl(b, T_COUNT, h_cnt); - setl(b, T_NET, h_net); - userlog("PAY_MERGE merch=%s merged=%ld -> id=%ld net=%ld", h_merch, h_cnt, new_id, h_net); + setl(b, T_ID2, h_wlast); + setl(b, T_COUNT, h_wcnt); + setl(b, T_NET, h_wpart); + Bchg(b, T_MSG, 0, "파생 지급건 생성 완료", 0L); + userlog("PAY_MERGE 완료: 생성=%ld건 last=%ld 대체=%ld건", h_wcnt, h_wlast, h_waff); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_MERGE", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_NETOFF.pgc b/app/src/py/svc/PAY_NETOFF.pgc index 89c684c..7625f4e 100644 --- a/app/src/py/svc/PAY_NETOFF.pgc +++ b/app/src/py/svc/PAY_NETOFF.pgc @@ -1,8 +1,9 @@ /* - * PAY_NETOFF.pgc - py 모듈 서비스 PAY_NETOFF (one service = one file). + * PAY_NETOFF.pgc - py (지급) 모듈 온라인 서비스 PAY_NETOFF. * 상계(net-off): 지급건의 net 을 조회해 차감액을 반영하고 갱신. - * Moved verbatim from the former inline py_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by py_svr at tpsvrinit. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [fee]. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_NETOFF.h" @@ -10,15 +11,166 @@ void PAY_NETOFF(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - long h_pay, h_off, h_new; + long h_amount; + long h_id1; + char h_wst[8]; + long h_wamt; + long h_wfee; + long h_wnet; + char h_wmerch[64]; + char h_wacct[64]; + long h_wmdr; + long h_wnew; + long h_wdelta; + long h_waff; + long h_wadj; + char h_wdt[16]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pay = getl(b, T_ID1); - h_off = getl(b, T_AMOUNT); - EXEC SQL SELECT net INTO :h_new FROM py_payout WHERE payout_id = :h_pay; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - h_new = h_new - h_off; - EXEC SQL UPDATE py_payout SET net = :h_new, updated_at = now() WHERE payout_id = :h_pay; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_NET, h_new); + + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_amount = getl(b, T_AMOUNT); + if (h_amount < 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "금액 항목은 음수가 될 수 없음", 0L); + FAIL(b); + } + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + /* 2) 마스터 조회부 - 지급건 + 가맹점 계약요율 (LEFT JOIN) */ + EXEC SQL SELECT p.status, p.amount, p.fee, p.net, p.merchant_id, + p.account_no, p.biz_date, coalesce(m.mdr_bps, 25) + INTO :h_wst, :h_wamt, :h_wfee, :h_wnet, :h_wmerch, + :h_wacct, :h_wdt, :h_wmdr + FROM py_payout p + LEFT JOIN merchant m ON m.merchant_id = p.merchant_id + WHERE p.payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 지급건 없음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급건/가맹점 조인 조회 오류", 0L); + FAIL(b); + } + /* 기 적재된 조정 합계 확인 */ + h_wadj = pydb_adjust_delta_sum(h_id1); + if (h_wadj < 0) h_wadj = 0; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + userlog("PAY_NETOFF 진입: payout=%ld status=%s fee=%ld net=%ld 조정누계=%ld", + h_id1, h_wst, h_wfee, h_wnet, h_wadj); + /* 3) 업무규칙 분기부 - 수수료/순액 재산정 (3분기) */ + if (strcmp(h_wst, "P") == 0 || strcmp(h_wst, "X") == 0 || + strcmp(h_wst, "C") == 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "종결된 지급건은 수수료/순액 조정 불가", 0L); + FAIL(b); + } + if (h_amount >= h_wnet) { + h_wdelta = h_wnet; + } else if (h_amount <= 0) { + setl(b, T_RC, 13); + Bchg(b, T_MSG, 0, "상계/환수 금액은 1 이상이어야 함", 0L); + FAIL(b); + } else { + h_wdelta = h_amount; + } + h_wnew = h_wfee; + strcpy(h_pval, "1000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_netoff_bps_cap'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 1000; + if (h_wnew > acq_fee(h_wamt, h_thr)) { + userlog("PAY_NETOFF 수수료 상한정책 초과: fee=%ld cap_bps=%ld", h_wnew, h_thr); + } + /* 4) 본처리부 - fee/net 재계산 및 갱신 */ + h_wnet = h_wnet - h_wdelta; + if (h_wnet < 0) h_wnet = 0; + EXEC SQL UPDATE py_payout SET net = :h_wnet, updated_at = now() + WHERE payout_id = :h_id1 AND status IN ('R','A','H'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "수수료/순액 갱신 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "갱신 대상 없음 - 처리 도중 상태 변동", 0L); + FAIL(b); + } + /* 5) 이력 적재부 - 조정 원장(py_adjust) 기록 */ + if (pydb_insert_adjust(h_id1, h_wdelta, "수수료/순액 조정", h_wdt) < 0) { + setl(b, T_RC, 55); + Bchg(b, T_MSG, 0, "조정 원장(py_adjust) 기록 실패", 0L); + FAIL(b); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s payout=%ld fee %ld->%ld net=%ld delta=%ld", "PAY_NETOFF", h_id1, h_wfee, h_wnew, h_wnet, h_wdelta); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_NETOFF', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 가맹점 당일 수수료/조정 누계 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(fee),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_wmerch AND biz_date = :h_wdt; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "수수료/조정 누계 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_adjust + WHERE payout_id = :h_id1; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 조정 이력 과다: 기준=%ld 실적=%ld", "PAY_NETOFF", h_wa1, h_wa3); + } + /* 8) 응답부 */ + setl(b, T_RC, 0); + setl(b, T_NET, h_wnet); + Bchg(b, T_MSG, 0, "수수료/순액 조정 완료", 0L); + userlog("PAY_NETOFF 완료: payout=%ld fee=%ld net=%ld", h_id1, h_wnew, h_wnet); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_NETOFF", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_NETRECALC.pgc b/app/src/py/svc/PAY_NETRECALC.pgc index 69f3b7d..e87fc4b 100644 --- a/app/src/py/svc/PAY_NETRECALC.pgc +++ b/app/src/py/svc/PAY_NETRECALC.pgc @@ -1,7 +1,9 @@ /* - * PAY_NETRECALC.pgc - py 모듈 서비스 PAY_NETRECALC (one service = one file). + * PAY_NETRECALC.pgc - py (지급) 모듈 온라인 서비스 PAY_NETRECALC. * 순지급액 정합 검증: 당일 건 중 amount - fee != net 인 불일치 건수와 * 누적 오차를 산출하고, 보정 플래그(T_ARG1=1) 시 net 을 재계산해 고친다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [cursor]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_NETRECALC.h" @@ -9,32 +11,130 @@ void PAY_NETRECALC(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long fix; + int gi = 0; + long tot_cnt = 0, tot_sum = 0, tot_fee = 0, top_cnt = 0, top_sum = 0; + char top_grp[64]; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_bad, h_drift, h_fixed; + long h_arg1; + char h_bizdate[64]; + char h_wgrp[64]; + long h_wgcnt; + long h_wgsum; + long h_wgfee; + long h_wtot; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_arg1 = getl(b, T_ARG1); + if (h_arg1 < 0) h_arg1 = 0; gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - fix = getl(b, T_ARG1); - h_fixed = 0; - - EXEC SQL SELECT count(*), coalesce(sum(amount - fee - net),0) - INTO :h_bad, :h_drift - FROM py_payout WHERE biz_date = :h_bizdate AND amount - fee <> net; - if (sqlca.sqlcode < 0) FAIL(b); - if (fix == 1 && h_bad > 0) { - EXEC SQL UPDATE py_payout SET net = amount - fee, updated_at = now() - WHERE biz_date = :h_bizdate AND amount - fee <> net; - if (sqlca.sqlcode < 0) FAIL(b); - h_fixed = sqlca.sqlerrd[2]; + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + top_grp[0] = 0; + /* 2) 사전 규모 확인 - 커서 오픈 전 총건수 파악 */ + EXEC SQL SELECT count(*) INTO :h_wtot FROM py_payout p WHERE p.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "집계 사전 조회 실패", 0L); + FAIL(b); + } + userlog("PAY_NETRECALC 집계 시작: 기준일=%s 대상=%ld건 (순액정합별)", h_wdate, h_wtot); + /* 커서 정의: 순액정합별 건수/금액 집계 */ + EXEC SQL DECLARE cur_pay_netrecalc CURSOR FOR + SELECT p.merchant_id, count(*), coalesce(sum(p.net),0), coalesce(sum(p.fee),0) + FROM py_payout p WHERE p.biz_date = :h_bizdate + GROUP BY p.merchant_id ORDER BY 2 DESC, 1; + EXEC SQL OPEN cur_pay_netrecalc; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "집계 커서 OPEN 실패", 0L); + FAIL(b); + } + /* 3) 본처리부 - 커서 순회하며 그룹별 집계/상위 추출 */ + for (;;) { + EXEC SQL FETCH cur_pay_netrecalc INTO :h_wgrp, :h_wgcnt, :h_wgsum, :h_wgfee; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { + EXEC SQL CLOSE cur_pay_netrecalc; + setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "집계 FETCH 오류", 0L); FAIL(b); + } + tot_cnt += h_wgcnt; + tot_sum += h_wgsum; + tot_fee += h_wgfee; + if (h_wgcnt > top_cnt) { + top_cnt = h_wgcnt; + top_sum = h_wgsum; + strncpy(top_grp, h_wgrp, sizeof(top_grp)-1); + top_grp[sizeof(top_grp)-1] = 0; + } + if (gi < 10) { + Bchg(b, T_STR1, gi, h_wgrp, 0L); + Bchg(b, T_AMT1, gi, (char *)&h_wgsum, 0L); + Bchg(b, T_ARG1, gi, (char *)&h_wgcnt, 0L); + gi++; + } + } + EXEC SQL CLOSE cur_pay_netrecalc; + strcpy(h_pval, "5000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_netrecalc_concentration'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 5000; + /* 업무규칙 분기부 - 분포 집중도 판정 (3분기) */ + if (gi == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "순액정합별 집계 대상 없음", 0L); + } else if (tot_cnt > 0 && top_cnt * 10000 / tot_cnt > h_thr) { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "특정 순액정합 집중 - 확인 필요", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "순액정합별 정상 분포", 0L); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s %s별 그룹=%d 건수=%ld 금액=%ld 최다=%s", "PAY_NETRECALC", "순액정합", gi, tot_cnt, tot_sum, top_grp); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_NETRECALC', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 8) 응답부 */ + setl(b, T_COUNT, tot_cnt); + setl(b, T_ARG2, (long)gi); + setl(b, T_DELTA, tot_fee); + userlog("PAY_NETRECALC 완료: 그룹=%d 총건=%ld 총액=%ld 최다=%s", + gi, tot_cnt, tot_sum, top_grp); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_NETRECALC", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); } - - setl(b, T_RC, h_bad > 0 ? 1 : 0); - setl(b, T_COUNT, h_bad); - setl(b, T_DELTA, h_drift); - setl(b, T_ARG2, h_fixed); - if (h_bad > 0) - userlog("PAY_NETRECALC date=%s bad=%ld drift=%ld fixed=%ld", h_bizdate, h_bad, h_drift, h_fixed); OK(b); } diff --git a/app/src/py/svc/PAY_NOTIFYALL.pgc b/app/src/py/svc/PAY_NOTIFYALL.pgc index 2d81fb4..18a927e 100644 --- a/app/src/py/svc/PAY_NOTIFYALL.pgc +++ b/app/src/py/svc/PAY_NOTIFYALL.pgc @@ -1,7 +1,9 @@ /* - * PAY_NOTIFYALL.pgc - py 모듈 서비스 PAY_NOTIFYALL (one service = one file). + * PAY_NOTIFYALL.pgc - py (지급) 모듈 온라인 서비스 PAY_NOTIFYALL. * 전 채널 일괄 통지(시뮬): 지급완료(P) 단건을 SMS/EMAIL/PUSH 세 채널로 각각 * 발송 시뮬레이션하고 발송 채널 수를 회신한다 (C 배열 순회). + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [notify]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_NOTIFYALL.h" @@ -9,26 +11,126 @@ void PAY_NOTIFYALL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - const char *chans[3] = { "SMS", "EMAIL", "PUSH" }; - int k, sent = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_net; - char h_merch[64], h_st[4]; + long h_id1; + char h_wst[8]; + long h_wnet; + char h_wmerch[64]; + char h_wacct[64]; + long h_wretry; + char h_wrsn[128]; + char h_wholder[64]; + char h_wbank[8]; + long h_wcnt; + long h_wsum; + long h_waff; + char h_wdate[16]; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_ID1); - EXEC SQL SELECT merchant_id, net, status INTO :h_merch, :h_net, :h_st - FROM py_payout WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - if (h_st[0] != 'P') { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "지급완료 건만 통지", 0L); FAIL(b); } - - for (k = 0; k < 3; k++) { - userlog("PAY_NOTIFYALL ch=%s payout=%ld to=%s", chans[k], h_pid, h_merch); - sent++; + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + /* 2) 마스터 조회부 - 통지 대상 지급건 + 계좌 예금주 (조인) */ + EXEC SQL SELECT p.status, p.net, p.merchant_id, p.account_no, + p.retry_count, p.reject_reason, + coalesce(a.holder,''), coalesce(a.bank_code,'') + INTO :h_wst, :h_wnet, :h_wmerch, :h_wacct, + :h_wretry, :h_wrsn, :h_wholder, :h_wbank + FROM py_payout p + LEFT JOIN py_account a ON a.account_no = p.account_no + WHERE p.payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "통지 대상 지급건 없음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "통지 대상 조회 오류", 0L); + FAIL(b); + } + userlog("PAY_NOTIFYALL 진입: payout=%ld status=%s 예금주=%s", + h_id1, h_wst, h_wholder); + /* 3) 업무규칙 분기부 - 통지 가능 상태/한도 (3분기) */ + if (strcmp(h_wst, "P") != 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "통지 대상 상태가 아님", 0L); + FAIL(b); + } + if (h_wnet <= 0) { + setl(b, T_RC, 43); + Bchg(b, T_MSG, 0, "통지할 지급 금액이 없음", 0L); + FAIL(b); + } else if (h_wholder[0] == 0) { + setl(b, T_RC, 44); + Bchg(b, T_MSG, 0, "예금주 정보가 없어 통지 불가", 0L); + FAIL(b); + } + /* 4) 본처리부 - SMS/EMAIL/PUSH 3채널 발송 (시뮬) */ + h_wcnt = 3; h_wsum = h_wnet; h_waff = 3; + userlog("PAY_NOTIFYALL SMS 발송: payout=%ld 예금주=%s 금액=%ld", + h_id1, h_wholder, h_wnet); + userlog("PAY_NOTIFYALL EMAIL 발송: payout=%ld 가맹점=%s", h_id1, h_wmerch); + userlog("PAY_NOTIFYALL PUSH 발송: payout=%ld 은행=%s", h_id1, h_wbank); + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s 통지=%ld건 금액=%ld 보류=%ld", "PAY_NOTIFYALL", h_wcnt, h_wsum, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_NOTIFYALL', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 당일 통지 대상/제외 누계 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE biz_date = :h_wdate AND status = 'P'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "통지 대상 누계 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE biz_date = :h_wdate AND status = 'J'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 실패 통지 대상 증가: 기준=%ld 실적=%ld", "PAY_NOTIFYALL", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - setl(b, T_COUNT, sent); - setl(b, T_NET, h_net); - Bchg(b, T_MSG, 0, "전 채널 통지 완료", 0L); + setl(b, T_COUNT, h_wcnt); + setl(b, T_NET, h_wnet); + Bchg(b, T_MSG, 0, "지급 통지 처리 완료", 0L); + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + userlog("PAY_NOTIFYALL 완료: 통지=%ld건 금액=%ld", h_wcnt, h_wsum); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_NOTIFYALL", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_NOTIFYEMAIL.pgc b/app/src/py/svc/PAY_NOTIFYEMAIL.pgc index 3a650e0..78d1148 100644 --- a/app/src/py/svc/PAY_NOTIFYEMAIL.pgc +++ b/app/src/py/svc/PAY_NOTIFYEMAIL.pgc @@ -1,7 +1,9 @@ /* - * PAY_NOTIFYEMAIL.pgc - py 모듈 서비스 PAY_NOTIFYEMAIL (one service = one file). + * PAY_NOTIFYEMAIL.pgc - py (지급) 모듈 온라인 서비스 PAY_NOTIFYEMAIL. * 지급내역 이메일 통지(시뮬): 가맹점의 당일 지급완료(P) 건수/합계를 집계해 * 메일 제목과 본문 요약을 조립한다. 대상이 없으면 미발송(1) 회신. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [notify]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_NOTIFYEMAIL.h" @@ -9,30 +11,123 @@ void PAY_NOTIFYEMAIL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char subject[128], body[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_sum; - char h_merch[64], h_bizdate[16]; + char h_bizdate[64]; + char h_merchant[64]; + char h_wst[8]; + long h_wnet; + char h_wmerch[64]; + char h_wacct[64]; + long h_wretry; + char h_wrsn[128]; + char h_wholder[64]; + char h_wbank[8]; + long h_wcnt; + long h_wsum; + long h_waff; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_cnt, :h_sum - FROM py_payout - WHERE merchant_id = :h_merch AND biz_date = :h_bizdate AND status = 'P'; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_cnt == 0) { - setl(b, T_RC, 1); - Bchg(b, T_MSG, 0, "이메일 통지 대상 없음", 0L); - OK(b); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + gets_(b, T_MERCHANT, h_merchant, sizeof(h_merchant)); + if (h_merchant[0] == 0) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "가맹점 ID는 필수 입력", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + /* 2) 마스터 조회부 - 통지 대상 지급완료 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) + INTO :h_wcnt, :h_wsum + FROM py_payout WHERE status = 'P' AND merchant_id = :h_merchant AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "통지 대상 집계 오류", 0L); + FAIL(b); + } + /* 통지 제외 대상(보류) 확인 */ + EXEC SQL SELECT count(*) INTO :h_waff FROM py_payout + WHERE status = 'H' AND merchant_id = :h_merchant AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) h_waff = 0; + strcpy(h_pval, "500000000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_notifyemail_large'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 500000000; + /* 3) 업무규칙 분기부 - 통지 등급 판정 (3분기) */ + if (h_wcnt == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "통지 대상 지급건 없음", 0L); + } else if (h_wsum > h_thr) { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "대액 지급 통지 발송", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "지급 내역 통지 발송", 0L); + } + h_wnet = h_wsum; + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s 통지=%ld건 금액=%ld 보류=%ld", "PAY_NOTIFYEMAIL", h_wcnt, h_wsum, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_NOTIFYEMAIL', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 당일 통지 대상/제외 누계 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE biz_date = :h_wdate AND status = 'P'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "통지 대상 누계 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE biz_date = :h_wdate AND status = 'J'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 실패 통지 대상 증가: 기준=%ld 실적=%ld", "PAY_NOTIFYEMAIL", h_wa1, h_wa3); + } + /* 8) 응답부 */ + setl(b, T_COUNT, h_wcnt); + setl(b, T_GROSS, h_wsum); + Bchg(b, T_STR1, 0, h_wholder, 0L); + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + userlog("PAY_NOTIFYEMAIL 완료: 통지=%ld건 금액=%ld", h_wcnt, h_wsum); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_NOTIFYEMAIL", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); } - snprintf(subject, sizeof(subject), "[Forge] %s 지급내역 안내 (%s)", h_merch, h_bizdate); - snprintf(body, sizeof(body), "당일 %ld건, 합계 %ld원이 지급 완료되었습니다.", h_cnt, h_sum); - userlog("PAY_NOTIFYEMAIL 발송시뮬 merch=%s cnt=%ld amt=%ld", h_merch, h_cnt, h_sum); - setl(b, T_RC, 0); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_sum); - Bchg(b, T_STR1, 0, subject, 0L); - Bchg(b, T_MSG, 0, body, 0L); OK(b); } diff --git a/app/src/py/svc/PAY_NOTIFYFAIL.pgc b/app/src/py/svc/PAY_NOTIFYFAIL.pgc index 7aeb0b6..535c253 100644 --- a/app/src/py/svc/PAY_NOTIFYFAIL.pgc +++ b/app/src/py/svc/PAY_NOTIFYFAIL.pgc @@ -1,7 +1,9 @@ /* - * PAY_NOTIFYFAIL.pgc - py 모듈 서비스 PAY_NOTIFYFAIL (one service = one file). + * PAY_NOTIFYFAIL.pgc - py (지급) 모듈 온라인 서비스 PAY_NOTIFYFAIL. * 지급실패 통지(시뮬): 반려(J) 건의 가맹점/반려사유를 읽어 실패 안내 본문을 * 조립해 발송 시뮬레이션한다. 반려 건이 아니면 거절한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [notify]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_NOTIFYFAIL.h" @@ -9,23 +11,121 @@ void PAY_NOTIFYFAIL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char msg[224]; EXEC SQL BEGIN DECLARE SECTION; - long h_pid; - char h_merch[64], h_st[4], h_rsn[128]; + long h_id1; + char h_wst[8]; + long h_wnet; + char h_wmerch[64]; + char h_wacct[64]; + long h_wretry; + char h_wrsn[128]; + char h_wholder[64]; + char h_wbank[8]; + long h_wcnt; + long h_wsum; + long h_waff; + char h_wdate[16]; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_ID1); - EXEC SQL SELECT merchant_id, status, reject_reason INTO :h_merch, :h_st, :h_rsn - FROM py_payout WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - if (h_st[0] != 'J') { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "반려 건만 실패통지 대상", 0L); FAIL(b); } - if (h_rsn[0] == 0) strcpy(h_rsn, "사유미상"); - - snprintf(msg, sizeof(msg), "[Forge] %s 지급실패: %s", h_merch, h_rsn); - userlog("PAY_NOTIFYFAIL payout=%ld reason=%s", h_pid, h_rsn); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + /* 2) 마스터 조회부 - 통지 대상 지급건 + 계좌 예금주 (조인) */ + EXEC SQL SELECT p.status, p.net, p.merchant_id, p.account_no, + p.retry_count, p.reject_reason, + coalesce(a.holder,''), coalesce(a.bank_code,'') + INTO :h_wst, :h_wnet, :h_wmerch, :h_wacct, + :h_wretry, :h_wrsn, :h_wholder, :h_wbank + FROM py_payout p + LEFT JOIN py_account a ON a.account_no = p.account_no + WHERE p.payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "통지 대상 지급건 없음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "통지 대상 조회 오류", 0L); + FAIL(b); + } + userlog("PAY_NOTIFYFAIL 진입: payout=%ld status=%s 예금주=%s", + h_id1, h_wst, h_wholder); + /* 3) 업무규칙 분기부 - 통지 가능 상태/한도 (3분기) */ + if (strcmp(h_wst, "J") != 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "통지 대상 상태가 아님", 0L); + FAIL(b); + } + if (h_wnet < 0) { + setl(b, T_RC, 43); + Bchg(b, T_MSG, 0, "통지 금액이 비정상", 0L); + FAIL(b); + } else if (h_wmerch[0] == 0) { + setl(b, T_RC, 44); + Bchg(b, T_MSG, 0, "가맹점 정보 누락", 0L); + FAIL(b); + } + /* 4) 본처리부 - 단일 채널 통지 (시뮬) */ + h_wcnt = 1; h_wsum = h_wnet; h_waff = 1; + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s 통지=%ld건 금액=%ld 보류=%ld", "PAY_NOTIFYFAIL", h_wcnt, h_wsum, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_NOTIFYFAIL', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 당일 통지 대상/제외 누계 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE biz_date = :h_wdate AND status = 'P'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "통지 대상 누계 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE biz_date = :h_wdate AND status = 'J'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 실패 통지 대상 증가: 기준=%ld 실적=%ld", "PAY_NOTIFYFAIL", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - Bchg(b, T_REASON, 0, h_rsn, 0L); - Bchg(b, T_MSG, 0, msg, 0L); + Bchg(b, T_REASON, 0, h_wrsn, 0L); + Bchg(b, T_MSG, 0, "지급 통지 처리 완료", 0L); + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + userlog("PAY_NOTIFYFAIL 완료: 통지=%ld건 금액=%ld", h_wcnt, h_wsum); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_NOTIFYFAIL", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_NOTIFYKAKAO.pgc b/app/src/py/svc/PAY_NOTIFYKAKAO.pgc index 1c52a14..a889014 100644 --- a/app/src/py/svc/PAY_NOTIFYKAKAO.pgc +++ b/app/src/py/svc/PAY_NOTIFYKAKAO.pgc @@ -1,7 +1,9 @@ /* - * PAY_NOTIFYKAKAO.pgc - py 모듈 서비스 PAY_NOTIFYKAKAO (one service = one file). + * PAY_NOTIFYKAKAO.pgc - py (지급) 모듈 온라인 서비스 PAY_NOTIFYKAKAO. * 알림톡 일괄 통지(시뮬): 당일 지급완료(P) 건수/금액을 한 번에 집계해 알림톡 * 대량발송을 시뮬레이션한다. 대상이 없으면 안내 후 정상 종료한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [notify]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_NOTIFYKAKAO.h" @@ -10,20 +12,114 @@ void PAY_NOTIFYKAKAO(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_cnt, h_sum; + char h_bizdate[64]; + char h_wst[8]; + long h_wnet; + char h_wmerch[64]; + char h_wacct[64]; + long h_wretry; + char h_wrsn[128]; + char h_wholder[64]; + char h_wbank[8]; + long h_wcnt; + long h_wsum; + long h_waff; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_cnt, :h_sum - FROM py_payout WHERE biz_date = :h_bizdate AND status = 'P'; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_cnt == 0) { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "통지 대상 없음", 0L); OK(b); } - - userlog("PAY_NOTIFYKAKAO 일괄발송시뮬 bizdate=%s targets=%ld amt=%ld", h_bizdate, h_cnt, h_sum); - setl(b, T_RC, 0); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_sum); - Bchg(b, T_MSG, 0, "알림톡 발송 완료", 0L); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + /* 2) 마스터 조회부 - 통지 대상 지급완료 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) + INTO :h_wcnt, :h_wsum + FROM py_payout WHERE status = 'P' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "통지 대상 집계 오류", 0L); + FAIL(b); + } + /* 통지 제외 대상(보류) 확인 */ + EXEC SQL SELECT count(*) INTO :h_waff FROM py_payout + WHERE status = 'H' AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) h_waff = 0; + strcpy(h_pval, "500000000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_notifykakao_large'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 500000000; + /* 3) 업무규칙 분기부 - 통지 등급 판정 (3분기) */ + if (h_wcnt == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "통지 대상 지급건 없음", 0L); + } else if (h_wsum > h_thr) { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "대액 지급 통지 발송", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "지급 내역 통지 발송", 0L); + } + h_wnet = h_wsum; + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s 통지=%ld건 금액=%ld 보류=%ld", "PAY_NOTIFYKAKAO", h_wcnt, h_wsum, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_NOTIFYKAKAO', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 당일 통지 대상/제외 누계 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE biz_date = :h_wdate AND status = 'P'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "통지 대상 누계 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE biz_date = :h_wdate AND status = 'J'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 실패 통지 대상 증가: 기준=%ld 실적=%ld", "PAY_NOTIFYKAKAO", h_wa1, h_wa3); + } + /* 8) 응답부 */ + setl(b, T_COUNT, h_wcnt); + setl(b, T_GROSS, h_wsum); + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + userlog("PAY_NOTIFYKAKAO 완료: 통지=%ld건 금액=%ld", h_wcnt, h_wsum); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_NOTIFYKAKAO", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_NOTIFYPLAN.pgc b/app/src/py/svc/PAY_NOTIFYPLAN.pgc index d073262..0978566 100644 --- a/app/src/py/svc/PAY_NOTIFYPLAN.pgc +++ b/app/src/py/svc/PAY_NOTIFYPLAN.pgc @@ -1,7 +1,9 @@ /* - * PAY_NOTIFYPLAN.pgc - py 모듈 서비스 PAY_NOTIFYPLAN (one service = one file). + * PAY_NOTIFYPLAN.pgc - py (지급) 모듈 온라인 서비스 PAY_NOTIFYPLAN. * 지급 예정 통보: 지정 실행일의 예약(S) 건을 py_payout 과 조인 집계해 * 지급 예정 건수/총액과 통보 문안을 회신한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [sched]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_NOTIFYPLAN.h" @@ -9,21 +11,114 @@ void PAY_NOTIFYPLAN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char msg[160]; EXEC SQL BEGIN DECLARE SECTION; - char h_rundate[16]; - long h_cnt, h_sum; + char h_bizdate[64]; + char h_wst[8]; + char h_wsst[8]; + long h_wsid; + long h_wcnt; + long h_wdone; + long h_wsum; + long h_waff; + char h_wrun[16]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, h_rundate, sizeof(h_rundate)); - EXEC SQL SELECT count(*), coalesce(sum(p.net),0) INTO :h_cnt, :h_sum - FROM py_schedule s JOIN py_payout p ON s.payout_id = p.payout_id - WHERE s.run_date = :h_rundate AND s.status = 'S'; - if (sqlca.sqlcode < 0) FAIL(b); - snprintf(msg, sizeof(msg), "%s 지급 예정 %ld건 / 총 %ld원", h_rundate, h_cnt, h_sum); - setl(b, T_RC, h_cnt > 0 ? 0 : 1); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_sum); - Bchg(b, T_MSG, 0, msg, 0L); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + /* 2) 마스터 조회부 - 지정 실행일 예약 지급 집계 (조인) */ + EXEC SQL SELECT count(*), coalesce(sum(p.net),0) + INTO :h_wcnt, :h_wsum + FROM py_schedule s JOIN py_payout p ON p.payout_id = s.payout_id + WHERE s.run_date = :h_bizdate AND s.status = 'S'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "예약 지급 집계 오류", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wdone FROM py_schedule + WHERE run_date = :h_bizdate AND status = 'D'; + if (sqlca.sqlcode < 0) h_wdone = 0; + strcpy(h_pval, "100000000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_notifyplan_large'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 100000000; + /* 3) 업무규칙 분기부 - 통보 등급 판정 (3분기) */ + if (h_wcnt == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "예정된 지급 없음", 0L); + } else if (h_wsum > h_thr) { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "대액 지급 예정 통보", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "지급 예정 통보", 0L); + } + h_waff = h_wcnt; h_wsid = 0; + strncpy(h_wrun, h_bizdate, sizeof(h_wrun)-1); + h_wrun[sizeof(h_wrun)-1] = 0; + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s sid=%ld 실행일=%s 건수=%ld 처리=%ld", "PAY_NOTIFYPLAN", h_wsid, h_wrun, h_wcnt, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_NOTIFYPLAN', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 실행일 기준 예약 잔량 재집계 */ + EXEC SQL SELECT count(*), coalesce(count(DISTINCT payout_id),0) + INTO :h_wa1, :h_wa2 + FROM py_schedule WHERE status = 'S'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "예약 잔량 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_schedule + WHERE status = 'D'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 실행 완료 예약 누적: 기준=%ld 실적=%ld", "PAY_NOTIFYPLAN", h_wa1, h_wa3); + } + /* 8) 응답부 */ + setl(b, T_COUNT, h_wcnt); + setl(b, T_GROSS, h_wsum); + userlog("PAY_NOTIFYPLAN 완료: sid=%ld 실행일=%s 건수=%ld", h_wsid, h_wrun, h_wcnt); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_NOTIFYPLAN", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_NOTIFYPUSH.pgc b/app/src/py/svc/PAY_NOTIFYPUSH.pgc index 0f6e099..1074f12 100644 --- a/app/src/py/svc/PAY_NOTIFYPUSH.pgc +++ b/app/src/py/svc/PAY_NOTIFYPUSH.pgc @@ -1,7 +1,9 @@ /* - * PAY_NOTIFYPUSH.pgc - py 모듈 서비스 PAY_NOTIFYPUSH (one service = one file). + * PAY_NOTIFYPUSH.pgc - py (지급) 모듈 온라인 서비스 PAY_NOTIFYPUSH. * 지급완료 푸시 통지(시뮬): 지급완료(P)건을 py_account 조인으로 예금주/은행을 * 읽어 푸시 본문을 조립하고 발송은 userlog 로 시뮬레이션한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [notify]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_NOTIFYPUSH.h" @@ -9,25 +11,122 @@ void PAY_NOTIFYPUSH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char msg[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_net; - char h_holder[64], h_st[4], h_bank[8]; + long h_id1; + char h_wst[8]; + long h_wnet; + char h_wmerch[64]; + char h_wacct[64]; + long h_wretry; + char h_wrsn[128]; + char h_wholder[64]; + char h_wbank[8]; + long h_wcnt; + long h_wsum; + long h_waff; + char h_wdate[16]; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_ID1); - EXEC SQL SELECT p.net, p.status, a.holder, a.bank_code - INTO :h_net, :h_st, :h_holder, :h_bank - FROM py_payout p JOIN py_account a ON p.account_no = a.account_no - WHERE p.payout_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - if (h_st[0] != 'P') { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "지급완료 건만 푸시 가능", 0L); FAIL(b); } - - snprintf(msg, sizeof(msg), "[Forge] %s(%s) %ld원 지급 알림", h_holder, h_bank, h_net); - userlog("PAY_NOTIFYPUSH 발송시뮬 payout=%ld holder=%s", h_pid, h_holder); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + /* 2) 마스터 조회부 - 통지 대상 지급건 + 계좌 예금주 (조인) */ + EXEC SQL SELECT p.status, p.net, p.merchant_id, p.account_no, + p.retry_count, p.reject_reason, + coalesce(a.holder,''), coalesce(a.bank_code,'') + INTO :h_wst, :h_wnet, :h_wmerch, :h_wacct, + :h_wretry, :h_wrsn, :h_wholder, :h_wbank + FROM py_payout p + LEFT JOIN py_account a ON a.account_no = p.account_no + WHERE p.payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "통지 대상 지급건 없음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "통지 대상 조회 오류", 0L); + FAIL(b); + } + userlog("PAY_NOTIFYPUSH 진입: payout=%ld status=%s 예금주=%s", + h_id1, h_wst, h_wholder); + /* 3) 업무규칙 분기부 - 통지 가능 상태/한도 (3분기) */ + if (strcmp(h_wst, "P") != 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "통지 대상 상태가 아님", 0L); + FAIL(b); + } + if (h_wnet < 0) { + setl(b, T_RC, 43); + Bchg(b, T_MSG, 0, "통지 금액이 비정상", 0L); + FAIL(b); + } else if (h_wmerch[0] == 0) { + setl(b, T_RC, 44); + Bchg(b, T_MSG, 0, "가맹점 정보 누락", 0L); + FAIL(b); + } + /* 4) 본처리부 - 단일 채널 통지 (시뮬) */ + h_wcnt = 1; h_wsum = h_wnet; h_waff = 1; + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s 통지=%ld건 금액=%ld 보류=%ld", "PAY_NOTIFYPUSH", h_wcnt, h_wsum, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_NOTIFYPUSH', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 당일 통지 대상/제외 누계 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE biz_date = :h_wdate AND status = 'P'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "통지 대상 누계 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE biz_date = :h_wdate AND status = 'J'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 실패 통지 대상 증가: 기준=%ld 실적=%ld", "PAY_NOTIFYPUSH", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - setl(b, T_NET, h_net); - Bchg(b, T_STR2, 0, h_bank, 0L); - Bchg(b, T_MSG, 0, msg, 0L); + setl(b, T_NET, h_wnet); + Bchg(b, T_STR2, 0, h_wholder, 0L); + Bchg(b, T_MSG, 0, "지급 통지 처리 완료", 0L); + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + userlog("PAY_NOTIFYPUSH 완료: 통지=%ld건 금액=%ld", h_wcnt, h_wsum); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_NOTIFYPUSH", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_NOTIFYRESEND.pgc b/app/src/py/svc/PAY_NOTIFYRESEND.pgc index 4411b58..e0ac2f7 100644 --- a/app/src/py/svc/PAY_NOTIFYRESEND.pgc +++ b/app/src/py/svc/PAY_NOTIFYRESEND.pgc @@ -1,7 +1,9 @@ /* - * PAY_NOTIFYRESEND.pgc - py 모듈 서비스 PAY_NOTIFYRESEND (one service = one file). + * PAY_NOTIFYRESEND.pgc - py (지급) 모듈 온라인 서비스 PAY_NOTIFYRESEND. * 통지 재발송: 지급완료(P) 건의 retry_count 를 재발송 횟수로 삼아 3회까지만 * 허용하며, 재발송 시 카운터를 +1 하고 현재 시도 회차를 회신한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [notify]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_NOTIFYRESEND.h" @@ -10,23 +12,142 @@ void PAY_NOTIFYRESEND(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_rc; - char h_st[4]; + long h_id1; + char h_wst[8]; + long h_wnet; + char h_wmerch[64]; + char h_wacct[64]; + long h_wretry; + char h_wrsn[128]; + char h_wholder[64]; + char h_wbank[8]; + long h_wcnt; + long h_wsum; + long h_waff; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_ID1); - EXEC SQL SELECT status, retry_count INTO :h_st, :h_rc - FROM py_payout WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - if (h_st[0] != 'P') { setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "지급완료 건만 재발송", 0L); FAIL(b); } - if (h_rc >= 3) { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "재발송 한도 초과", 0L); FAIL(b); } - - EXEC SQL UPDATE py_payout SET retry_count = retry_count + 1, updated_at = now() - WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - userlog("PAY_NOTIFYRESEND payout=%ld attempt=%ld", h_pid, h_rc + 1); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + /* 2) 마스터 조회부 - 통지 대상 지급건 + 계좌 예금주 (조인) */ + EXEC SQL SELECT p.status, p.net, p.merchant_id, p.account_no, + p.retry_count, p.reject_reason, + coalesce(a.holder,''), coalesce(a.bank_code,'') + INTO :h_wst, :h_wnet, :h_wmerch, :h_wacct, + :h_wretry, :h_wrsn, :h_wholder, :h_wbank + FROM py_payout p + LEFT JOIN py_account a ON a.account_no = p.account_no + WHERE p.payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "통지 대상 지급건 없음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "통지 대상 조회 오류", 0L); + FAIL(b); + } + userlog("PAY_NOTIFYRESEND 진입: payout=%ld status=%s 예금주=%s", + h_id1, h_wst, h_wholder); + /* 3) 업무규칙 분기부 - 통지 가능 상태/한도 (3분기) */ + if (strcmp(h_wst, "P") != 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "통지 대상 상태가 아님", 0L); + FAIL(b); + } + strcpy(h_pval, "3"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_notify_resend_max'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 3; + if (h_wretry >= h_thr) { + setl(b, T_RC, 43); + Bchg(b, T_MSG, 0, "통지 재발송 한도 초과", 0L); + FAIL(b); + } else if (h_wacct[0] == 0) { + setl(b, T_RC, 44); + Bchg(b, T_MSG, 0, "통지 대상 계좌 정보 없음", 0L); + FAIL(b); + } + /* 4) 본처리부 - 재발송 횟수 증가 */ + EXEC SQL UPDATE py_payout SET retry_count = retry_count + 1, + updated_at = now() + WHERE payout_id = :h_id1; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "재발송 횟수 갱신 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + h_wcnt = h_wretry + 1; + h_wsum = h_wnet; + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s 통지=%ld건 금액=%ld 보류=%ld", "PAY_NOTIFYRESEND", h_wcnt, h_wsum, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_NOTIFYRESEND', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 당일 통지 대상/제외 누계 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE biz_date = :h_wdate AND status = 'P'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "통지 대상 누계 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE biz_date = :h_wdate AND status = 'J'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 실패 통지 대상 증가: 기준=%ld 실적=%ld", "PAY_NOTIFYRESEND", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - setl(b, T_COUNT, h_rc + 1); - Bchg(b, T_MSG, 0, "통지 재발송 완료", 0L); + setl(b, T_COUNT, h_wcnt); + Bchg(b, T_MSG, 0, "지급 통지 처리 완료", 0L); + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + userlog("PAY_NOTIFYRESEND 완료: 통지=%ld건 금액=%ld", h_wcnt, h_wsum); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_NOTIFYRESEND", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_NOTIFYSMS.pgc b/app/src/py/svc/PAY_NOTIFYSMS.pgc index 21638d1..8dfb173 100644 --- a/app/src/py/svc/PAY_NOTIFYSMS.pgc +++ b/app/src/py/svc/PAY_NOTIFYSMS.pgc @@ -1,7 +1,9 @@ /* - * PAY_NOTIFYSMS.pgc - py 모듈 서비스 PAY_NOTIFYSMS (one service = one file). + * PAY_NOTIFYSMS.pgc - py (지급) 모듈 온라인 서비스 PAY_NOTIFYSMS. * 지급완료 SMS 통지(시뮬): 지급완료(P)건만 대상. 가맹점/금액을 조회해 * 문자 본문을 조립하고 발송은 userlog 로 시뮬레이션한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [notify]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_NOTIFYSMS.h" @@ -9,25 +11,121 @@ void PAY_NOTIFYSMS(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char msg[192]; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_net; - char h_merch[64], h_st[4]; + long h_id1; + char h_wst[8]; + long h_wnet; + char h_wmerch[64]; + char h_wacct[64]; + long h_wretry; + char h_wrsn[128]; + char h_wholder[64]; + char h_wbank[8]; + long h_wcnt; + long h_wsum; + long h_waff; + char h_wdate[16]; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_ID1); - EXEC SQL SELECT merchant_id, net, status INTO :h_merch, :h_net, :h_st - FROM py_payout WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - if (h_st[0] != 'P') { - setl(b, T_RC, 1); - Bchg(b, T_MSG, 0, "지급완료 건만 SMS 통지 가능", 0L); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); FAIL(b); } - snprintf(msg, sizeof(msg), "[Forge지급] %s님, %ld원 지급이 완료되었습니다.", h_merch, h_net); - userlog("PAY_NOTIFYSMS 발송시뮬 payout=%ld to=%s", h_pid, h_merch); + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + /* 2) 마스터 조회부 - 통지 대상 지급건 + 계좌 예금주 (조인) */ + EXEC SQL SELECT p.status, p.net, p.merchant_id, p.account_no, + p.retry_count, p.reject_reason, + coalesce(a.holder,''), coalesce(a.bank_code,'') + INTO :h_wst, :h_wnet, :h_wmerch, :h_wacct, + :h_wretry, :h_wrsn, :h_wholder, :h_wbank + FROM py_payout p + LEFT JOIN py_account a ON a.account_no = p.account_no + WHERE p.payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "통지 대상 지급건 없음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "통지 대상 조회 오류", 0L); + FAIL(b); + } + userlog("PAY_NOTIFYSMS 진입: payout=%ld status=%s 예금주=%s", + h_id1, h_wst, h_wholder); + /* 3) 업무규칙 분기부 - 통지 가능 상태/한도 (3분기) */ + if (strcmp(h_wst, "P") != 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "통지 대상 상태가 아님", 0L); + FAIL(b); + } + if (h_wnet < 0) { + setl(b, T_RC, 43); + Bchg(b, T_MSG, 0, "통지 금액이 비정상", 0L); + FAIL(b); + } else if (h_wmerch[0] == 0) { + setl(b, T_RC, 44); + Bchg(b, T_MSG, 0, "가맹점 정보 누락", 0L); + FAIL(b); + } + /* 4) 본처리부 - 단일 채널 통지 (시뮬) */ + h_wcnt = 1; h_wsum = h_wnet; h_waff = 1; + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s 통지=%ld건 금액=%ld 보류=%ld", "PAY_NOTIFYSMS", h_wcnt, h_wsum, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_NOTIFYSMS', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 당일 통지 대상/제외 누계 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE biz_date = :h_wdate AND status = 'P'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "통지 대상 누계 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE biz_date = :h_wdate AND status = 'J'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 실패 통지 대상 증가: 기준=%ld 실적=%ld", "PAY_NOTIFYSMS", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - setl(b, T_NET, h_net); - Bchg(b, T_MSG, 0, msg, 0L); + setl(b, T_NET, h_wnet); + Bchg(b, T_MSG, 0, "지급 통지 처리 완료", 0L); + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + userlog("PAY_NOTIFYSMS 완료: 통지=%ld건 금액=%ld", h_wcnt, h_wsum); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_NOTIFYSMS", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_OFFSETAPPLY.pgc b/app/src/py/svc/PAY_OFFSETAPPLY.pgc index 7f31c8b..78d6f88 100644 --- a/app/src/py/svc/PAY_OFFSETAPPLY.pgc +++ b/app/src/py/svc/PAY_OFFSETAPPLY.pgc @@ -1,7 +1,9 @@ /* - * PAY_OFFSETAPPLY.pgc - py 모듈 서비스 PAY_OFFSETAPPLY (one service = one file). + * PAY_OFFSETAPPLY.pgc - py (지급) 모듈 온라인 서비스 PAY_OFFSETAPPLY. * 상계 적용: 미지급(R/A) 건의 net 에서 상계액을 차감(net 미만으로 캡)하고, * 차감분을 py_adjust 에 음수 조정으로 남긴다. 지급 전 채권 상계 반영. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [fee]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_OFFSETAPPLY.h" @@ -9,33 +11,177 @@ void PAY_OFFSETAPPLY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char reason[96], bizdate[16]; - long off, adj; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_net, h_cut; - char h_st[4]; + long h_amount; + char h_bizdate[64]; + long h_id1; + char h_reason[64]; + char h_wst[8]; + long h_wamt; + long h_wfee; + long h_wnet; + char h_wmerch[64]; + char h_wacct[64]; + long h_wmdr; + long h_wnew; + long h_wdelta; + long h_waff; + long h_wadj; + char h_wdt[16]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_ID1); - off = getl(b, T_AMOUNT); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - gets_(b, T_REASON, reason, sizeof(reason)); - if (reason[0] == 0) strcpy(reason, "상계"); - - EXEC SQL SELECT net, status INTO :h_net, :h_st FROM py_payout WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - if (h_st[0] != 'R' && h_st[0] != 'A') { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "미지급 건만 상계 가능", 0L); FAIL(b); } - h_cut = (off > h_net) ? h_net : off; - if (h_cut <= 0) { setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "상계액 무효", 0L); FAIL(b); } - - adj = pydb_insert_adjust(h_pid, -h_cut, reason, bizdate); - if (adj < 0) FAIL(b); - EXEC SQL UPDATE py_payout SET net = net - :h_cut, updated_at = now() WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_amount = getl(b, T_AMOUNT); + if (h_amount < 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "금액 항목은 음수가 될 수 없음", 0L); + FAIL(b); + } + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 13); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + gets_(b, T_REASON, h_reason, sizeof(h_reason)); + if (h_reason[0] == 0) strncpy(h_reason, "사유미기재", sizeof(h_reason)-1); + /* 2) 마스터 조회부 - 지급건 + 가맹점 계약요율 (LEFT JOIN) */ + EXEC SQL SELECT p.status, p.amount, p.fee, p.net, p.merchant_id, + p.account_no, p.biz_date, coalesce(m.mdr_bps, 25) + INTO :h_wst, :h_wamt, :h_wfee, :h_wnet, :h_wmerch, + :h_wacct, :h_wdt, :h_wmdr + FROM py_payout p + LEFT JOIN merchant m ON m.merchant_id = p.merchant_id + WHERE p.payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 지급건 없음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급건/가맹점 조인 조회 오류", 0L); + FAIL(b); + } + /* 기 적재된 조정 합계 확인 */ + h_wadj = pydb_adjust_delta_sum(h_id1); + if (h_wadj < 0) h_wadj = 0; + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + userlog("PAY_OFFSETAPPLY 진입: payout=%ld status=%s fee=%ld net=%ld 조정누계=%ld", + h_id1, h_wst, h_wfee, h_wnet, h_wadj); + /* 3) 업무규칙 분기부 - 수수료/순액 재산정 (3분기) */ + if (strcmp(h_wst, "P") == 0 || strcmp(h_wst, "X") == 0 || + strcmp(h_wst, "C") == 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "종결된 지급건은 수수료/순액 조정 불가", 0L); + FAIL(b); + } + if (h_amount >= h_wnet) { + h_wdelta = h_wnet; + } else if (h_amount <= 0) { + setl(b, T_RC, 13); + Bchg(b, T_MSG, 0, "상계/환수 금액은 1 이상이어야 함", 0L); + FAIL(b); + } else { + h_wdelta = h_amount; + } + h_wnew = h_wfee; + strcpy(h_pval, "1000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_offsetapply_bps_cap'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 1000; + if (h_wnew > acq_fee(h_wamt, h_thr)) { + userlog("PAY_OFFSETAPPLY 수수료 상한정책 초과: fee=%ld cap_bps=%ld", h_wnew, h_thr); + } + /* 4) 본처리부 - fee/net 재계산 및 갱신 */ + h_wnet = h_wnet - h_wdelta; + if (h_wnet < 0) h_wnet = 0; + EXEC SQL UPDATE py_payout SET net = :h_wnet, updated_at = now() + WHERE payout_id = :h_id1 AND status IN ('R','A','H'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "수수료/순액 갱신 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "갱신 대상 없음 - 처리 도중 상태 변동", 0L); + FAIL(b); + } + /* 5) 이력 적재부 - 조정 원장(py_adjust) 기록 */ + if (pydb_insert_adjust(h_id1, h_wdelta, h_reason, h_wdt) < 0) { + setl(b, T_RC, 55); + Bchg(b, T_MSG, 0, "조정 원장(py_adjust) 기록 실패", 0L); + FAIL(b); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s payout=%ld fee %ld->%ld net=%ld delta=%ld", "PAY_OFFSETAPPLY", h_id1, h_wfee, h_wnew, h_wnet, h_wdelta); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_OFFSETAPPLY', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 가맹점 당일 수수료/조정 누계 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(fee),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_wmerch AND biz_date = :h_wdt; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "수수료/조정 누계 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_adjust + WHERE payout_id = :h_id1; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 조정 이력 과다: 기준=%ld 실적=%ld", "PAY_OFFSETAPPLY", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - setl(b, T_ADJ, adj); - setl(b, T_NET, h_net - h_cut); - userlog("PAY_OFFSETAPPLY id=%ld off=%ld", h_pid, h_cut); + setl(b, T_NET, h_wnet); + setl(b, T_ADJ, h_wdelta); + Bchg(b, T_MSG, 0, "수수료/순액 조정 완료", 0L); + userlog("PAY_OFFSETAPPLY 완료: payout=%ld fee=%ld net=%ld", h_id1, h_wnew, h_wnet); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_OFFSETAPPLY", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_PENDLIST.pgc b/app/src/py/svc/PAY_PENDLIST.pgc index 971b262..c833734 100644 --- a/app/src/py/svc/PAY_PENDLIST.pgc +++ b/app/src/py/svc/PAY_PENDLIST.pgc @@ -1,7 +1,9 @@ /* - * PAY_PENDLIST.pgc - py 모듈 서비스 PAY_PENDLIST (one service = one file). + * PAY_PENDLIST.pgc - py (지급) 모듈 온라인 서비스 PAY_PENDLIST. * 미지급 목록: 당일 미완결(요청R/승인A/보류H) 지급건을 커서로 훑어 지급ID/ * 가맹점/net/상태를 occurrence 로 회신한다 (집계 없는 상세 리스트). + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [cursor]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_PENDLIST.h" @@ -9,35 +11,127 @@ void PAY_PENDLIST(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - int i = 0; - long n = 0; + int gi = 0; + long tot_cnt = 0, tot_sum = 0, tot_fee = 0, top_cnt = 0, top_sum = 0; + char top_grp[64]; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16], h_merch[64], h_st[4]; - long h_pid, h_net; + char h_bizdate[64]; + char h_wgrp[64]; + long h_wgcnt; + long h_wgsum; + long h_wgfee; + long h_wtot; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL DECLARE cur_pendlist CURSOR FOR - SELECT payout_id, merchant_id, net, status FROM py_payout - WHERE biz_date = :h_bizdate AND status IN ('R','A','H') - ORDER BY payout_id; - EXEC SQL OPEN cur_pendlist; - if (sqlca.sqlcode < 0) FAIL(b); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + top_grp[0] = 0; + /* 2) 사전 규모 확인 - 커서 오픈 전 총건수 파악 */ + EXEC SQL SELECT count(*) INTO :h_wtot FROM py_payout p WHERE p.biz_date = :h_bizdate AND p.status IN ('R','A','H'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "집계 사전 조회 실패", 0L); + FAIL(b); + } + userlog("PAY_PENDLIST 집계 시작: 기준일=%s 대상=%ld건 (미지급상태별)", h_wdate, h_wtot); + /* 커서 정의: 미지급상태별 건수/금액 집계 */ + EXEC SQL DECLARE cur_pay_pendlist CURSOR FOR + SELECT p.status, count(*), coalesce(sum(p.net),0), coalesce(sum(p.fee),0) + FROM py_payout p WHERE p.biz_date = :h_bizdate AND p.status IN ('R','A','H') + GROUP BY p.status ORDER BY 2 DESC, 1; + EXEC SQL OPEN cur_pay_pendlist; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "집계 커서 OPEN 실패", 0L); + FAIL(b); + } + /* 3) 본처리부 - 커서 순회하며 그룹별 집계/상위 추출 */ for (;;) { - EXEC SQL FETCH cur_pendlist INTO :h_pid, :h_merch, :h_net, :h_st; + EXEC SQL FETCH cur_pay_pendlist INTO :h_wgrp, :h_wgcnt, :h_wgsum, :h_wgfee; if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_pendlist; FAIL(b); } - n++; - if (i < 12) { - Bchg(b, T_ARG1, i, (char *)&h_pid, 0L); - Bchg(b, T_STR2, i, h_merch, 0L); - Bchg(b, T_AMT1, i, (char *)&h_net, 0L); - Bchg(b, T_STR3, i, h_st, 0L); - i++; + if (sqlca.sqlcode < 0) { + EXEC SQL CLOSE cur_pay_pendlist; + setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "집계 FETCH 오류", 0L); FAIL(b); + } + tot_cnt += h_wgcnt; + tot_sum += h_wgsum; + tot_fee += h_wgfee; + if (h_wgcnt > top_cnt) { + top_cnt = h_wgcnt; + top_sum = h_wgsum; + strncpy(top_grp, h_wgrp, sizeof(top_grp)-1); + top_grp[sizeof(top_grp)-1] = 0; + } + if (gi < 10) { + Bchg(b, T_STR1, gi, h_wgrp, 0L); + Bchg(b, T_AMT1, gi, (char *)&h_wgsum, 0L); + Bchg(b, T_ARG1, gi, (char *)&h_wgcnt, 0L); + gi++; } } - EXEC SQL CLOSE cur_pendlist; - setl(b, T_COUNT, n); - setl(b, T_RC, 0); + EXEC SQL CLOSE cur_pay_pendlist; + strcpy(h_pval, "5000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_pendlist_concentration'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 5000; + /* 업무규칙 분기부 - 분포 집중도 판정 (3분기) */ + if (gi == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "미지급상태별 집계 대상 없음", 0L); + } else if (tot_cnt > 0 && top_cnt * 10000 / tot_cnt > h_thr) { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "특정 미지급상태 집중 - 확인 필요", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "미지급상태별 정상 분포", 0L); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s %s별 그룹=%d 건수=%ld 금액=%ld 최다=%s", "PAY_PENDLIST", "미지급상태", gi, tot_cnt, tot_sum, top_grp); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_PENDLIST', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 8) 응답부 */ + setl(b, T_COUNT, tot_cnt); + Bchg(b, T_STR2, 0, top_grp, 0L); + Bchg(b, T_STR3, 0, top_grp, 0L); + userlog("PAY_PENDLIST 완료: 그룹=%d 총건=%ld 총액=%ld 최다=%s", + gi, tot_cnt, tot_sum, top_grp); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_PENDLIST", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_REACTIVATE.pgc b/app/src/py/svc/PAY_REACTIVATE.pgc index 4412ab0..58cce92 100644 --- a/app/src/py/svc/PAY_REACTIVATE.pgc +++ b/app/src/py/svc/PAY_REACTIVATE.pgc @@ -1,7 +1,9 @@ /* - * PAY_REACTIVATE.pgc - py 모듈 서비스 PAY_REACTIVATE (one service = one file). + * PAY_REACTIVATE.pgc - py (지급) 모듈 온라인 서비스 PAY_REACTIVATE. * 휴면 계좌 재활성: 휴면(DORMANT) 처리된 지급계좌를 다시 활성(ACTIVE)으로 * 되돌린다. 휴면 상태가 아니면 재활성 대상이 아니므로 거절한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [acct]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_REACTIVATE.h" @@ -10,17 +12,152 @@ void PAY_REACTIVATE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - char h_acct[32]; + char h_key1[64]; + char h_wast[16]; + long h_wbal; + char h_wholder[64]; + char h_wmerch[64]; + long h_waff; + long h_wcas; + long h_wsum; + long h_wcnt; + char h_wacct[64]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, h_acct, sizeof(h_acct)); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + gets_(b, T_KEY1, h_key1, sizeof(h_key1)); + if (h_key1[0] == 0 || strlen(h_key1) < 3) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "계좌번호/키 값이 유효하지 않음", 0L); + FAIL(b); + } + /* 2) 마스터 조회부 - 대상 지급계좌 확정 후 상태/잔액/예금주 조회 */ + strncpy(h_wacct, h_key1, sizeof(h_wacct)-1); + h_wacct[sizeof(h_wacct)-1] = 0; + EXEC SQL SELECT status, balance, holder, merchant_id + INTO :h_wast, :h_wbal, :h_wholder, :h_wmerch + FROM py_account WHERE account_no = :h_wacct; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 지급계좌가 등록되어 있지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급계좌 조회 중 DB 오류", 0L); + FAIL(b); + } + /* 해당 계좌의 미지급 잔액 규모 파악 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wcnt, :h_wsum + FROM py_payout WHERE account_no = :h_wacct AND status IN ('R','A','H'); + if (sqlca.sqlcode < 0) { h_wcnt = 0; h_wsum = 0; } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + userlog("PAY_REACTIVATE 진입: acct=%s status=%s bal=%ld 미지급=%ld건", + h_wacct, h_wast, h_wbal, h_wcnt); + /* 3) 업무규칙 분기부 - 계좌 상태 전이 적법성 (3분기) */ + if (strcmp(h_wast, "ACTIVE") == 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "이미 휴면 재활성 상태인 계좌", 0L); + FAIL(b); + } + if (strcmp(h_wast, "DORMANT") != 0) { + setl(b, T_RC, 43); Bchg(b, T_STATUS, 0, h_wast, 0L); + Bchg(b, T_MSG, 0, "현재 계좌 상태에서는 휴면 재활성 처리 불가", 0L); FAIL(b); + } + if (h_wbal < 0) { + setl(b, T_RC, 44); + Bchg(b, T_MSG, 0, "잔액이 음수인 비정상 계좌 - 선행 정산 필요", 0L); + FAIL(b); + } + strcpy(h_pval, "1"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_reactivate_cascade'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 1; + /* 4) 본처리부 - 계좌 상태 전이 후 미지급건 연쇄 처리 */ EXEC SQL UPDATE py_account SET status = 'ACTIVE', updated_at = now() - WHERE account_no = :h_acct AND status = 'DORMANT'; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "휴면 계좌 아님", 0L); FAIL(b); } - + WHERE account_no = :h_wacct AND status = 'DORMANT'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "휴면 재활성 처리 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "계좌 상태 동시성 충돌", 0L); + FAIL(b); + } + EXEC SQL UPDATE py_payout SET status = 'A', reject_reason = '', + updated_at = now() + WHERE account_no = :h_wacct AND status IN ('H') AND :h_thr > 0; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 51); + Bchg(b, T_MSG, 0, "미지급건 연쇄 처리 실패", 0L); + FAIL(b); + } + h_wcas = sqlca.sqlerrd[2]; + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s acct=%s %s->%s 연쇄=%ld 금액=%ld", "PAY_REACTIVATE", h_wacct, "DORMANT", "ACTIVE", h_wcas, h_wsum); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_REACTIVATE', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 계좌 상태 전이 후 잔여 미지급 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE account_no = :h_wacct AND status = 'H'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "계좌 잔여 미지급 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE account_no = :h_wacct AND status IN ('R','A'); + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 연쇄 처리 후 잔여 존재: 기준=%ld 실적=%ld", "PAY_REACTIVATE", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); Bchg(b, T_STATUS, 0, "ACTIVE", 0L); - userlog("PAY_REACTIVATE acct=%s -> ACTIVE", h_acct); + Bchg(b, T_MSG, 0, "휴면 재활성 및 미지급건 연쇄처리 완료", 0L); + userlog("PAY_REACTIVATE 완료: acct=%s 연쇄처리=%ld건 보류금액=%ld", + h_wacct, h_wcas, h_wsum); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_REACTIVATE", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_RECLAIM.pgc b/app/src/py/svc/PAY_RECLAIM.pgc index cfc681f..1bf1827 100644 --- a/app/src/py/svc/PAY_RECLAIM.pgc +++ b/app/src/py/svc/PAY_RECLAIM.pgc @@ -1,7 +1,9 @@ /* - * PAY_RECLAIM.pgc - py 모듈 서비스 PAY_RECLAIM (one service = one file). + * PAY_RECLAIM.pgc - py (지급) 모듈 온라인 서비스 PAY_RECLAIM. * 과지급 환수: 지급완료(P) 건에서 요청 환수액(T_AMOUNT, net 한도)만큼을 * 지급계좌에서 차감(음수 account_add)하고 py_ledger 에 음수 RECLAIM 기표. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [fee]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_RECLAIM.h" @@ -9,30 +11,168 @@ void PAY_RECLAIM(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long want, recl; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_net, h_neg; - char h_acct[32], h_st[4], h_bizdate[16]; + long h_amount; + long h_id1; + char h_wst[8]; + long h_wamt; + long h_wfee; + long h_wnet; + char h_wmerch[64]; + char h_wacct[64]; + long h_wmdr; + long h_wnew; + long h_wdelta; + long h_waff; + long h_wadj; + char h_wdt[16]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_ID1); - want = getl(b, T_AMOUNT); - EXEC SQL SELECT account_no, net, status, CAST(biz_date AS text) - INTO :h_acct, :h_net, :h_st, :h_bizdate FROM py_payout WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - if (h_st[0] != 'P') { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "지급완료 건만 환수 가능", 0L); FAIL(b); } - - recl = (want > 0 && want < h_net) ? want : h_net; - if (recl <= 0) { setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "환수액 무효", 0L); FAIL(b); } - if (pydb_account_add(h_acct, -recl) < 0) FAIL(b); - h_neg = -recl; - EXEC SQL INSERT INTO py_ledger (ledger_id, payout_id, entry_type, amount, biz_date) - VALUES (nextval('py_ledger_seq'), :h_pid, 'RECLAIM', :h_neg, :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); - + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_amount = getl(b, T_AMOUNT); + if (h_amount < 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "금액 항목은 음수가 될 수 없음", 0L); + FAIL(b); + } + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + /* 2) 마스터 조회부 - 지급건 + 가맹점 계약요율 (LEFT JOIN) */ + EXEC SQL SELECT p.status, p.amount, p.fee, p.net, p.merchant_id, + p.account_no, p.biz_date, coalesce(m.mdr_bps, 25) + INTO :h_wst, :h_wamt, :h_wfee, :h_wnet, :h_wmerch, + :h_wacct, :h_wdt, :h_wmdr + FROM py_payout p + LEFT JOIN merchant m ON m.merchant_id = p.merchant_id + WHERE p.payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 지급건 없음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급건/가맹점 조인 조회 오류", 0L); + FAIL(b); + } + /* 기 적재된 조정 합계 확인 */ + h_wadj = pydb_adjust_delta_sum(h_id1); + if (h_wadj < 0) h_wadj = 0; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + userlog("PAY_RECLAIM 진입: payout=%ld status=%s fee=%ld net=%ld 조정누계=%ld", + h_id1, h_wst, h_wfee, h_wnet, h_wadj); + /* 3) 업무규칙 분기부 - 수수료/순액 재산정 (3분기) */ + if (strcmp(h_wst, "P") == 0 || strcmp(h_wst, "X") == 0 || + strcmp(h_wst, "C") == 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "종결된 지급건은 수수료/순액 조정 불가", 0L); + FAIL(b); + } + if (h_amount >= h_wnet) { + h_wdelta = h_wnet; + } else if (h_amount <= 0) { + setl(b, T_RC, 13); + Bchg(b, T_MSG, 0, "상계/환수 금액은 1 이상이어야 함", 0L); + FAIL(b); + } else { + h_wdelta = h_amount; + } + h_wnew = h_wfee; + strcpy(h_pval, "1000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_reclaim_bps_cap'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 1000; + if (h_wnew > acq_fee(h_wamt, h_thr)) { + userlog("PAY_RECLAIM 수수료 상한정책 초과: fee=%ld cap_bps=%ld", h_wnew, h_thr); + } + /* 4) 본처리부 - fee/net 재계산 및 갱신 */ + h_wnet = h_wnet - h_wdelta; + if (h_wnet < 0) h_wnet = 0; + EXEC SQL UPDATE py_payout SET net = :h_wnet, updated_at = now() + WHERE payout_id = :h_id1 AND status IN ('R','A','H'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "수수료/순액 갱신 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "갱신 대상 없음 - 처리 도중 상태 변동", 0L); + FAIL(b); + } + /* 5) 이력 적재부 - 조정 원장(py_adjust) 기록 */ + if (pydb_insert_adjust(h_id1, h_wdelta, "수수료/순액 조정", h_wdt) < 0) { + setl(b, T_RC, 55); + Bchg(b, T_MSG, 0, "조정 원장(py_adjust) 기록 실패", 0L); + FAIL(b); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s payout=%ld fee %ld->%ld net=%ld delta=%ld", "PAY_RECLAIM", h_id1, h_wfee, h_wnew, h_wnet, h_wdelta); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_RECLAIM', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 가맹점 당일 수수료/조정 누계 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(fee),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_wmerch AND biz_date = :h_wdt; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "수수료/조정 누계 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_adjust + WHERE payout_id = :h_id1; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 조정 이력 과다: 기준=%ld 실적=%ld", "PAY_RECLAIM", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - setl(b, T_AMT1, recl); - Bchg(b, T_STR1, 0, h_acct, 0L); - userlog("PAY_RECLAIM id=%ld reclaimed=%ld acct=%s", h_pid, recl, h_acct); + setl(b, T_AMT1, h_wnet); + Bchg(b, T_STR1, 0, h_wacct, 0L); + Bchg(b, T_MSG, 0, "수수료/순액 조정 완료", 0L); + userlog("PAY_RECLAIM 완료: payout=%ld fee=%ld net=%ld", h_id1, h_wnew, h_wnet); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_RECLAIM", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_RECON.pgc b/app/src/py/svc/PAY_RECON.pgc index 768c7ff..6eea285 100644 --- a/app/src/py/svc/PAY_RECON.pgc +++ b/app/src/py/svc/PAY_RECON.pgc @@ -1,8 +1,9 @@ /* - * PAY_RECON.pgc - py 모듈 서비스 PAY_RECON (one service = one file). + * PAY_RECON.pgc - py (지급) 모듈 온라인 서비스 PAY_RECON. * 지급-파일 대사: 특정 file_id 에 편성된 지급 건수/합계를 확인. - * Moved verbatim from the former inline py_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by py_svr at tpsvrinit. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [file]. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_RECON.h" @@ -10,13 +11,115 @@ void PAY_RECON(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - long h_file, h_cnt, h_sum; + long h_id2; + char h_wfst[8]; + long h_whcnt; + long h_whamt; + long h_wrcnt; + long h_wramt; + long h_waff; + long h_wnew; + char h_wfname[64]; + char h_wfdate[16]; + char h_wdate[16]; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_file = getl(b, T_ID2); - EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_cnt, :h_sum - FROM py_payout WHERE file_id = :h_file; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_sum); + + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id2 = getl(b, T_ID2); + if (h_id2 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + /* 2) 마스터 조회부 - 지급파일 헤더 + 실제 편성 상세 대사 */ + EXEC SQL SELECT status, record_count, total_amount, + to_char(biz_date, 'YYYY-MM-DD'), file_name + INTO :h_wfst, :h_whcnt, :h_whamt, :h_wfdate, :h_wfname + FROM py_file WHERE file_id = :h_id2; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "지급파일 헤더가 존재하지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급파일 헤더 조회 오류", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wrcnt, :h_wramt + FROM py_payout WHERE file_id = :h_id2; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "편성 상세 집계 오류", 0L); + FAIL(b); + } + userlog("PAY_RECON 진입: file=%ld status=%s 헤더=%ld/%ld 상세=%ld/%ld", + h_id2, h_wfst, h_whcnt, h_whamt, h_wrcnt, h_wramt); + /* 3) 업무규칙 분기부 - 파일 상태/정합 판정 (3분기) */ + if (h_wrcnt == 0) { + setl(b, T_RC, 43); + Bchg(b, T_MSG, 0, "파일에 편성된 지급 상세가 없음", 0L); + FAIL(b); + } else if (h_whcnt != h_wrcnt || h_whamt != h_wramt) { + setl(b, T_RC, 5); Bchg(b, T_MSG, 0, "헤더-상세 건수/금액 불일치", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "헤더-상세 정합 일치", 0L); + } + h_waff = h_wrcnt; + h_wnew = h_id2; + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s file=%ld 상세=%ld건 %ld원 반영=%ld", "PAY_RECON", h_wnew, h_wrcnt, h_wramt, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_RECON', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 당일 지급파일 전체 현황 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(total_amount),0) INTO :h_wa1, :h_wa2 + FROM py_file WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "당일 파일 현황 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE biz_date = :h_wdate AND status = 'F'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 편성 건수 초과: 기준=%ld 실적=%ld", "PAY_RECON", h_wa1, h_wa3); + } + /* 8) 응답부 */ + setl(b, T_COUNT, h_wrcnt); + setl(b, T_GROSS, h_wramt); + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + userlog("PAY_RECON 완료: file=%ld 상세=%ld건 %ld원", h_wnew, h_wrcnt, h_wramt); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_RECON", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_REFUND.pgc b/app/src/py/svc/PAY_REFUND.pgc index 8c72847..b6fe144 100644 --- a/app/src/py/svc/PAY_REFUND.pgc +++ b/app/src/py/svc/PAY_REFUND.pgc @@ -1,7 +1,9 @@ /* - * PAY_REFUND.pgc - py 모듈 서비스 PAY_REFUND (one service = one file). + * PAY_REFUND.pgc - py (지급) 모듈 온라인 서비스 PAY_REFUND. * 지급 환입: 지급완료(P) 건을 역분개(V)하며 지급계좌에 net 을 복원하고 * py_ledger 에 음수 REFUND 기표를 남긴다 (취소/환입 원장 일관성). + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [txn]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_REFUND.h" @@ -9,38 +11,163 @@ void PAY_REFUND(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char reason[96]; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_net, h_neg; - char h_acct[32], h_st[4], h_bizdate[16], h_rsn[96]; + long h_id1; + char h_reason[64]; + char h_wst[8]; + long h_wamt; + long h_wnet; + char h_wchan[16]; + long h_wretry; + long h_waff; + char h_wmerch[64]; + char h_wacct[64]; + long h_wcnt; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_ID1); - gets_(b, T_REASON, reason, sizeof(reason)); - if (reason[0] == 0) strcpy(reason, "지급환입"); - strncpy(h_rsn, reason, sizeof(h_rsn)-1); h_rsn[sizeof(h_rsn)-1] = 0; - - EXEC SQL SELECT account_no, net, status, CAST(biz_date AS text) - INTO :h_acct, :h_net, :h_st, :h_bizdate - FROM py_payout WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - if (h_st[0] != 'P') { - setl(b, T_RC, 1); - Bchg(b, T_MSG, 0, "지급완료 건만 환입 가능", 0L); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); FAIL(b); } - if (pydb_account_add(h_acct, h_net) < 0) FAIL(b); - EXEC SQL UPDATE py_payout SET status = 'V', reject_reason = :h_rsn, updated_at = now() - WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - h_neg = -h_net; - EXEC SQL INSERT INTO py_ledger (ledger_id, payout_id, entry_type, amount, biz_date) - VALUES (nextval('py_ledger_seq'), :h_pid, 'REFUND', :h_neg, :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); - + gets_(b, T_REASON, h_reason, sizeof(h_reason)); + if (h_reason[0] == 0) strncpy(h_reason, "사유미기재", sizeof(h_reason)-1); + /* 2) 마스터 조회부 - 지급건 현재 상태/금액/채널/재시도 이력 */ + EXEC SQL SELECT status, amount, net, channel, retry_count, merchant_id, account_no + INTO :h_wst, :h_wamt, :h_wnet, :h_wchan, :h_wretry, :h_wmerch, :h_wacct + FROM py_payout WHERE payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 지급건이 존재하지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급건 조회 중 DB 오류", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + userlog("PAY_REFUND 진입: payout=%ld status=%s amt=%ld chan=%s", + h_id1, h_wst, h_wamt, h_wchan); + /* 3) 업무규칙 분기부 - 상태 전이 가능성/한도/재시도 판정 */ + if (strcmp(h_wst, "P") == 0 || strcmp(h_wst, "X") == 0) { + setl(b, T_RC, 2); Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "이미 종결(지급완료/마감)된 건은 지급 환입 불가", 0L); FAIL(b); + } + if (strcmp(h_wst, "P") != 0) { + setl(b, T_RC, 3); Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "현재 상태에서는 지급 환입 전이가 허용되지 않음", 0L); FAIL(b); + } + if (strcmp(h_wchan, "ADVANCE") == 0 && h_wnet > 50000000) { + userlog("PAY_REFUND 고액 선지급 건 주의: payout=%ld net=%ld", h_id1, h_wnet); + } + strcpy(h_pval, "100000000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_refund_amt_max'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 100000000; + if (h_wamt > h_thr) { + userlog("PAY_REFUND 정책 상한 초과: amt=%ld thr=%ld", h_wamt, h_thr); + } + /* 4) 본처리부 - 가드 조건부 상태 전이 UPDATE */ + EXEC SQL UPDATE py_payout SET status = 'V', updated_at = now() + WHERE payout_id = :h_id1 AND status = 'P'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "지급 환입 상태 갱신 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "동시성 충돌 - 전이 대상 건이 없음", 0L); + FAIL(b); + } + EXEC SQL UPDATE py_payout SET reject_reason = :h_reason, updated_at = now() + WHERE payout_id = :h_id1; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 51); + Bchg(b, T_MSG, 0, "처리 사유 기록 실패", 0L); + FAIL(b); + } + /* 역분개: 지급계좌 잔액 원복 */ + if (h_wacct[0] != 0) { + if (pydb_account_add(h_wacct, h_wnet) < 0) { + setl(b, T_RC, 52); + Bchg(b, T_MSG, 0, "역분개 계좌 잔액 원복 실패", 0L); + FAIL(b); + } + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s payout=%ld %s->%s amt=%ld aff=%ld", "PAY_REFUND", h_id1, "P", "V", h_wamt, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_REFUND', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계 재산정부 - 당일 동일 상태 건수 재집계 */ + EXEC SQL SELECT count(*) INTO :h_wcnt FROM py_payout + WHERE biz_date = :h_wdate AND status = 'V'; + if (sqlca.sqlcode < 0) h_wcnt = 0; + /* 6) 집계/요약 갱신부 - 가맹점 당일 확정성 지급 실적 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_wmerch + AND biz_date = :h_wdate AND status IN ('A','F','P'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "가맹점 지급 실적 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE merchant_id = :h_wmerch AND biz_date = :h_wdate + AND status IN ('R','H'); + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 미지급 잔량 과다: 기준=%ld 실적=%ld", "PAY_REFUND", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - setl(b, T_NET, h_net); Bchg(b, T_STATUS, 0, "V", 0L); - userlog("PAY_REFUND id=%ld net=%ld acct=%s", h_pid, h_net, h_acct); + setl(b, T_NET, h_wnet); + Bchg(b, T_MSG, 0, "지급 환입 정상 처리", 0L); + userlog("PAY_REFUND 완료: payout=%ld -> %s (당일 %s상태 %ld건)", + h_id1, "V", "V", h_wcnt); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_REFUND", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_REJECT.pgc b/app/src/py/svc/PAY_REJECT.pgc index 3db90df..fc1186f 100644 --- a/app/src/py/svc/PAY_REJECT.pgc +++ b/app/src/py/svc/PAY_REJECT.pgc @@ -1,25 +1,163 @@ /* - * PAY_REJECT.pgc - py 모듈 서비스 PAY_REJECT (one service = one file). + * PAY_REJECT.pgc - py (지급) 모듈 온라인 서비스 PAY_REJECT. * 지급 반려: 상태를 반려(J)로 전이하고 반려 사유를 reject_reason 에 기록. - * Moved verbatim from the former inline py_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by py_svr at tpsvrinit. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [txn]. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_REJECT.h" void PAY_REJECT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char reason[128]; EXEC SQL BEGIN DECLARE SECTION; - long h_pay; - char h_reason[128]; + long h_id1; + char h_reason[64]; + char h_wst[8]; + long h_wamt; + long h_wnet; + char h_wchan[16]; + long h_wretry; + long h_waff; + char h_wmerch[64]; + char h_wacct[64]; + long h_wcnt; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pay = getl(b, T_ID1); - gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "REJECT"); - strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; - EXEC SQL UPDATE py_payout SET status = 'J', reject_reason = :h_reason, updated_at = now() - WHERE payout_id = :h_pay; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); + + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + gets_(b, T_REASON, h_reason, sizeof(h_reason)); + if (h_reason[0] == 0) strncpy(h_reason, "사유미기재", sizeof(h_reason)-1); + /* 2) 마스터 조회부 - 지급건 현재 상태/금액/채널/재시도 이력 */ + EXEC SQL SELECT status, amount, net, channel, retry_count, merchant_id, account_no + INTO :h_wst, :h_wamt, :h_wnet, :h_wchan, :h_wretry, :h_wmerch, :h_wacct + FROM py_payout WHERE payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 지급건이 존재하지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급건 조회 중 DB 오류", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + userlog("PAY_REJECT 진입: payout=%ld status=%s amt=%ld chan=%s", + h_id1, h_wst, h_wamt, h_wchan); + /* 3) 업무규칙 분기부 - 상태 전이 가능성/한도/재시도 판정 */ + if (strcmp(h_wst, "P") == 0 || strcmp(h_wst, "X") == 0) { + setl(b, T_RC, 2); Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "이미 종결(지급완료/마감)된 건은 지급 반려 불가", 0L); FAIL(b); + } + if (strcmp(h_wst, "R") != 0) { + setl(b, T_RC, 3); Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "현재 상태에서는 지급 반려 전이가 허용되지 않음", 0L); FAIL(b); + } + if (strcmp(h_wchan, "ADVANCE") == 0 && h_wnet > 50000000) { + userlog("PAY_REJECT 고액 선지급 건 주의: payout=%ld net=%ld", h_id1, h_wnet); + } + strcpy(h_pval, "100000000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_reject_amt_max'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 100000000; + if (h_wamt > h_thr) { + userlog("PAY_REJECT 정책 상한 초과: amt=%ld thr=%ld", h_wamt, h_thr); + } + /* 4) 본처리부 - 가드 조건부 상태 전이 UPDATE */ + EXEC SQL UPDATE py_payout SET status = 'J', updated_at = now() + WHERE payout_id = :h_id1 AND status = 'R'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "지급 반려 상태 갱신 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "동시성 충돌 - 전이 대상 건이 없음", 0L); + FAIL(b); + } + EXEC SQL UPDATE py_payout SET reject_reason = :h_reason, updated_at = now() + WHERE payout_id = :h_id1; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 51); + Bchg(b, T_MSG, 0, "처리 사유 기록 실패", 0L); + FAIL(b); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s payout=%ld %s->%s amt=%ld aff=%ld", "PAY_REJECT", h_id1, "R", "J", h_wamt, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_REJECT', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계 재산정부 - 당일 동일 상태 건수 재집계 */ + EXEC SQL SELECT count(*) INTO :h_wcnt FROM py_payout + WHERE biz_date = :h_wdate AND status = 'J'; + if (sqlca.sqlcode < 0) h_wcnt = 0; + /* 6) 집계/요약 갱신부 - 가맹점 당일 확정성 지급 실적 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_wmerch + AND biz_date = :h_wdate AND status IN ('A','F','P'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "가맹점 지급 실적 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE merchant_id = :h_wmerch AND biz_date = :h_wdate + AND status IN ('R','H'); + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 미지급 잔량 과다: 기준=%ld 실적=%ld", "PAY_REJECT", h_wa1, h_wa3); + } + /* 8) 응답부 */ + setl(b, T_RC, 0); Bchg(b, T_STATUS, 0, "J", 0L); + Bchg(b, T_MSG, 0, "지급 반려 정상 처리", 0L); + userlog("PAY_REJECT 완료: payout=%ld -> %s (당일 %s상태 %ld건)", + h_id1, "J", "J", h_wcnt); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_REJECT", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_RELEASE.pgc b/app/src/py/svc/PAY_RELEASE.pgc index 71b13d8..a6a8862 100644 --- a/app/src/py/svc/PAY_RELEASE.pgc +++ b/app/src/py/svc/PAY_RELEASE.pgc @@ -1,7 +1,9 @@ /* - * PAY_RELEASE.pgc - py 모듈 서비스 PAY_RELEASE (one service = one file). + * PAY_RELEASE.pgc - py (지급) 모듈 온라인 서비스 PAY_RELEASE. * 보류 해제: 직전 상태를 조회하여 보류(H)가 아니면 무효 처리, 보류면 dbio 로 * 요청(R) 전이하고 이전 상태를 T_STR1 로 함께 회신. SELECT + guard + dbio. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [txn]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_RELEASE.h" @@ -9,20 +11,145 @@ void PAY_RELEASE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char prev[4]; EXEC SQL BEGIN DECLARE SECTION; - long h_pay; - char h_prev[8]; + long h_id1; + char h_wst[8]; + long h_wamt; + long h_wnet; + char h_wchan[16]; + long h_wretry; + long h_waff; + char h_wmerch[64]; + char h_wacct[64]; + long h_wcnt; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pay = getl(b, T_ID1); - EXEC SQL SELECT status INTO :h_prev FROM py_payout WHERE payout_id = :h_pay; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - prev[0] = h_prev[0]; prev[1] = 0; - if (prev[0] != 'H') { Bchg(b, T_STATUS, 0, prev, 0L); setl(b, T_RC, 1); OK(b); } - if (pydb_update_status(h_pay, "R") < 0) FAIL(b); - Bchg(b, T_STR1, 0, prev, 0L); - Bchg(b, T_STATUS, 0, "R", 0L); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + /* 2) 마스터 조회부 - 지급건 현재 상태/금액/채널/재시도 이력 */ + EXEC SQL SELECT status, amount, net, channel, retry_count, merchant_id, account_no + INTO :h_wst, :h_wamt, :h_wnet, :h_wchan, :h_wretry, :h_wmerch, :h_wacct + FROM py_payout WHERE payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 지급건이 존재하지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급건 조회 중 DB 오류", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + userlog("PAY_RELEASE 진입: payout=%ld status=%s amt=%ld chan=%s", + h_id1, h_wst, h_wamt, h_wchan); + /* 3) 업무규칙 분기부 - 상태 전이 가능성/한도/재시도 판정 */ + if (strcmp(h_wst, "P") == 0 || strcmp(h_wst, "X") == 0) { + setl(b, T_RC, 2); Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "이미 종결(지급완료/마감)된 건은 보류 해제 불가", 0L); FAIL(b); + } + if (strcmp(h_wst, "H") != 0) { + setl(b, T_RC, 3); Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "현재 상태에서는 보류 해제 전이가 허용되지 않음", 0L); FAIL(b); + } + if (strcmp(h_wchan, "ADVANCE") == 0 && h_wnet > 50000000) { + userlog("PAY_RELEASE 고액 선지급 건 주의: payout=%ld net=%ld", h_id1, h_wnet); + } + strcpy(h_pval, "100000000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_release_amt_max'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 100000000; + if (h_wamt > h_thr) { + userlog("PAY_RELEASE 정책 상한 초과: amt=%ld thr=%ld", h_wamt, h_thr); + } + /* 4) 본처리부 - 가드 조건부 상태 전이 UPDATE */ + EXEC SQL UPDATE py_payout SET status = 'A', updated_at = now() + WHERE payout_id = :h_id1 AND status = 'H'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "보류 해제 상태 갱신 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "동시성 충돌 - 전이 대상 건이 없음", 0L); + FAIL(b); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s payout=%ld %s->%s amt=%ld aff=%ld", "PAY_RELEASE", h_id1, "H", "A", h_wamt, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_RELEASE', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계 재산정부 - 당일 동일 상태 건수 재집계 */ + EXEC SQL SELECT count(*) INTO :h_wcnt FROM py_payout + WHERE biz_date = :h_wdate AND status = 'A'; + if (sqlca.sqlcode < 0) h_wcnt = 0; + /* 6) 집계/요약 갱신부 - 가맹점 당일 확정성 지급 실적 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_wmerch + AND biz_date = :h_wdate AND status IN ('A','F','P'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "가맹점 지급 실적 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE merchant_id = :h_wmerch AND biz_date = :h_wdate + AND status IN ('R','H'); + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 미지급 잔량 과다: 기준=%ld 실적=%ld", "PAY_RELEASE", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); + Bchg(b, T_STATUS, 0, "A", 0L); + Bchg(b, T_STR1, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "보류 해제 정상 처리", 0L); + userlog("PAY_RELEASE 완료: payout=%ld -> %s (당일 %s상태 %ld건)", + h_id1, "A", "A", h_wcnt); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_RELEASE", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_REMIT.pgc b/app/src/py/svc/PAY_REMIT.pgc index 211c5ec..ac61eb9 100644 --- a/app/src/py/svc/PAY_REMIT.pgc +++ b/app/src/py/svc/PAY_REMIT.pgc @@ -1,24 +1,144 @@ /* - * PAY_REMIT.pgc - py 모듈 서비스 PAY_REMIT (one service = one file). + * PAY_REMIT.pgc - py (지급) 모듈 온라인 서비스 PAY_REMIT. * 실지급 송금: 계좌 잔액을 확인해 부족하면 거절, 충분하면 차감 후 상태 P. - * Moved verbatim from the former inline py_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by py_svr at tpsvrinit. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [scalar]. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_REMIT.h" void PAY_REMIT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char acct[32], st[8]; - long payout_id, net, bal = 0; - payout_id = getl(b, T_ID1); - net = getl(b, T_NET); - gets_(b, T_KEY1, acct, sizeof(acct)); - if (pydb_account_check(acct, st, &bal) < 0) FAIL(b); - if (bal < net) { setl(b, T_RC, 1); FAIL(b); } - if (pydb_account_add(acct, -net) < 0) FAIL(b); - if (pydb_update_status(payout_id, "P") < 0) FAIL(b); - setl(b, T_RC, 0); - Bchg(b, T_STATUS, 0, "P", 0L); + EXEC SQL BEGIN DECLARE SECTION; + long h_id1; + char h_key1[64]; + long h_netin; + long h_wcnt; + long h_wsum; + long h_wfee; + long h_wnet; + long h_wrej; + long h_wlim; + long h_wrate; + long h_wpaid; + long h_whold; + char h_wtop[64]; + long h_wdays; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; + EXEC SQL END DECLARE SECTION; + + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + gets_(b, T_KEY1, h_key1, sizeof(h_key1)); + if (h_key1[0] == 0 || strlen(h_key1) < 3) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "계좌번호/키 값이 유효하지 않음", 0L); + FAIL(b); + } + h_netin = getl(b, T_NET); + if (h_netin < 0) { + setl(b, T_RC, 13); + Bchg(b, T_MSG, 0, "금액 항목은 음수가 될 수 없음", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + /* 2) 집계 조회부 - 건수/총액/수수료/순액 스칼라 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) + INTO :h_wcnt, :h_wsum, :h_wfee, :h_wnet + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "지급 집계 조회 오류", 0L); + FAIL(b); + } + /* 상태별 부가 지표 - 반려/지급완료/보류 */ + EXEC SQL SELECT + count(*) FILTER (WHERE status = 'J'), + count(*) FILTER (WHERE status = 'P'), + count(*) FILTER (WHERE status = 'H') + INTO :h_wrej, :h_wpaid, :h_whold + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wrej = 0; h_wpaid = 0; h_whold = 0; } + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :h_wlim FROM merchant; + if (sqlca.sqlcode < 0) h_wlim = 0; + /* 최다 실적 가맹점 1건 추출 */ + EXEC SQL SELECT coalesce(merchant_id,'') INTO :h_wtop + FROM py_payout WHERE biz_date = :h_wdate + GROUP BY merchant_id ORDER BY count(*) DESC LIMIT 1; + if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) strcpy(h_wtop, ""); + h_wdays = 30; + userlog("PAY_REMIT 집계: 건수=%ld 총액=%ld 반려=%ld 보류=%ld", + h_wcnt, h_wsum, h_wrej, h_whold); + /* 3) 업무규칙 분기부 - 지표 임계 판정 (3분기) */ + h_wrate = (h_wcnt > 0) ? (h_wrej * 10000 / h_wcnt) : 0; + strcpy(h_pval, "2000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_remit_fail_bps'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 2000; + if (h_wcnt == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "대상 지급건 없음", 0L); + } else if (h_wlim > 0 && h_wnet > h_wlim) { + setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "일 지급한도 초과 상태", 0L); + } else if (h_wrate > h_thr) { + setl(b, T_RC, 3); Bchg(b, T_MSG, 0, "실패율이 임계치를 초과", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "정상 범위", 0L); + } + if (h_whold > 0 && h_whold * 2 > h_wcnt) { + userlog("PAY_REMIT 보류 과다 경고: 보류=%ld/전체=%ld", h_whold, h_wcnt); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s 건수=%ld 총액=%ld 반려=%ld 실패율=%ldbps 최다=%s", "PAY_REMIT", h_wcnt, h_wsum, h_wrej, h_wrate, h_wtop); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_REMIT', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계 재산정부 - 당일 지급완료 총액 재확인 */ + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_wpaid FROM py_payout + WHERE biz_date = :h_wdate AND status = 'P'; + if (sqlca.sqlcode < 0) h_wpaid = 0; + /* 8) 응답부 */ + userlog("PAY_REMIT 완료: 건수=%ld 총액=%ld 실패율=%ldbps", + h_wcnt, h_wsum, h_wrate); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_REMIT", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_REPAYACCT.pgc b/app/src/py/svc/PAY_REPAYACCT.pgc index 0484040..4cb5321 100644 --- a/app/src/py/svc/PAY_REPAYACCT.pgc +++ b/app/src/py/svc/PAY_REPAYACCT.pgc @@ -1,7 +1,9 @@ /* - * PAY_REPAYACCT.pgc - py 모듈 서비스 PAY_REPAYACCT (one service = one file). + * PAY_REPAYACCT.pgc - py (지급) 모듈 온라인 서비스 PAY_REPAYACCT. * 계좌변경 재지급: 반려(J)된 지급건의 금액/수수료를 승계해 새 계좌(T_KEY2)로 * 신규 요청(R) 지급건을 복제 생성한다 (원건은 그대로 두어 이력 보존). + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [derive]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_REPAYACCT.h" @@ -9,31 +11,181 @@ void PAY_REPAYACCT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char newacct[32], bizdate[16]; - long newid; + int gi; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_amt, h_fee, h_net; - char h_merch[64], h_st[4]; + char h_bizdate[64]; + long h_id1; + char h_key2[64]; + char h_wst[8]; + long h_wamt; + long h_wfee; + long h_wnet; + char h_wmerch[64]; + char h_wacct[64]; + long h_wid; + long h_wlast; + long h_wcnt; + long h_wsum; + long h_waff; + long h_wpart; + long h_wrem; + char h_wdt[16]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_ID1); - gets_(b, T_KEY2, newacct, sizeof(newacct)); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (newacct[0] == 0) { setl(b, T_RC, 3); Bchg(b, T_MSG, 0, "새 계좌 미지정", 0L); FAIL(b); } - - EXEC SQL SELECT merchant_id, amount, fee, net, status - INTO :h_merch, :h_amt, :h_fee, :h_net, :h_st - FROM py_payout WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - if (h_st[0] != 'J') { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "반려 건만 재지급 대상", 0L); FAIL(b); } - - newid = pydb_next_payout_id(); - if (newid < 0) FAIL(b); - if (pydb_insert_payout(newid, h_merch, newacct, h_amt, h_fee, h_net, "R", "STD", bizdate) < 0) FAIL(b); - + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + gets_(b, T_KEY2, h_key2, sizeof(h_key2)); + if (h_key2[0] == 0 || strlen(h_key2) < 3) { + setl(b, T_RC, 13); + Bchg(b, T_MSG, 0, "계좌번호/키 값이 유효하지 않음", 0L); + FAIL(b); + } + /* 2) 마스터 조회부 - 원천 지급건 상세 */ + EXEC SQL SELECT status, amount, fee, net, merchant_id, account_no, biz_date + INTO :h_wst, :h_wamt, :h_wfee, :h_wnet, :h_wmerch, :h_wacct, :h_wdt + FROM py_payout WHERE payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "원천 지급건이 존재하지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "원천 지급건 조회 오류", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + userlog("PAY_REPAYACCT 진입: merch=%s amt=%ld net=%ld", h_wmerch, h_wamt, h_wnet); + /* 3) 업무규칙 분기부 - 파생 생성 적법성 판정 (3분기) */ + if (strcmp(h_wst, "J") != 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "반려(J) 건만 재지급 대상", 0L); + FAIL(b); + } + strncpy(h_wacct, h_key2, sizeof(h_wacct)-1); h_wacct[sizeof(h_wacct)-1] = 0; + if (h_wacct[0] == 0) { + setl(b, T_RC, 43); + Bchg(b, T_MSG, 0, "재지급할 정상 계좌를 찾을 수 없음", 0L); + FAIL(b); + } + h_wcnt = 1; + h_wpart = h_wnet; + h_wrem = 0; + if (h_wpart <= 0) { + setl(b, T_RC, 44); + Bchg(b, T_MSG, 0, "재지급 금액이 0 이하", 0L); + FAIL(b); + } + strcpy(h_pval, "999"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_repayacct_max_child'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 999; + if (h_wcnt > h_thr) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "생성 건수 정책 상한 초과", 0L); + FAIL(b); + } + /* 4) 본처리부 - 파생 지급건 생성 */ + h_wlast = 0; + EXEC SQL SELECT nextval('py_payout_seq') INTO :h_wid; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "재지급건 채번 실패", 0L); + FAIL(b); + } + EXEC SQL INSERT INTO py_payout + (payout_id, merchant_id, account_no, amount, fee, net, + status, channel, biz_date) + VALUES (:h_wid, :h_wmerch, :h_wacct, :h_wamt, :h_wfee, + :h_wpart, 'R', 'STD', :h_wdt); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 51); + Bchg(b, T_MSG, 0, "재지급건 생성 실패", 0L); + FAIL(b); + } + h_wlast = h_wid; + /* 원 반려건은 재지급 대체로 종결 */ + EXEC SQL UPDATE py_payout SET status = 'C', reject_reason = '재지급대체', + updated_at = now() + WHERE payout_id = :h_id1 AND status = 'J'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 52); + Bchg(b, T_MSG, 0, "원 반려건 종결 처리 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s 원천=%ld 생성=%ld건 last=%ld 대체=%ld", "PAY_REPAYACCT", (long)(h_wcnt), h_wcnt, h_wlast, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_REPAYACCT', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 파생 생성 후 가맹점 당일 지급 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_wmerch AND status = 'R'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "파생 생성 후 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE merchant_id = :h_wmerch AND status = 'C'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 취소 대체 건 누적: 기준=%ld 실적=%ld", "PAY_REPAYACCT", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - setl(b, T_ID2, newid); - setl(b, T_NET, h_net); - userlog("PAY_REPAYACCT old=%ld new=%ld acct=%s", h_pid, newid, newacct); + setl(b, T_ID2, h_wlast); + setl(b, T_NET, h_wpart); + Bchg(b, T_MSG, 0, "파생 지급건 생성 완료", 0L); + userlog("PAY_REPAYACCT 완료: 생성=%ld건 last=%ld 대체=%ld건", h_wcnt, h_wlast, h_waff); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_REPAYACCT", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_REPAYNEW.pgc b/app/src/py/svc/PAY_REPAYNEW.pgc index fd378ba..d97cc61 100644 --- a/app/src/py/svc/PAY_REPAYNEW.pgc +++ b/app/src/py/svc/PAY_REPAYNEW.pgc @@ -1,7 +1,9 @@ /* - * PAY_REPAYNEW.pgc - py 모듈 서비스 PAY_REPAYNEW (one service = one file). + * PAY_REPAYNEW.pgc - py (지급) 모듈 온라인 서비스 PAY_REPAYNEW. * 계좌 변경 재지급: 반려(J)된 지급건을 가맹점의 최신 정상(ACTIVE) 계좌로 * 재지급 신규건(R) 생성 후 원건은 역분개(V) 처리한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [derive]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_REPAYNEW.h" @@ -9,38 +11,170 @@ void PAY_REPAYNEW(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long new_id; + int gi; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_amt, h_fee, h_net; - char h_merch[64], h_st[4], h_bizdate[16], h_newacct[32]; + long h_id1; + char h_wst[8]; + long h_wamt; + long h_wfee; + long h_wnet; + char h_wmerch[64]; + char h_wacct[64]; + long h_wid; + long h_wlast; + long h_wcnt; + long h_wsum; + long h_waff; + long h_wpart; + long h_wrem; + char h_wdt[16]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_ID1); - EXEC SQL SELECT merchant_id, amount, fee, net, status, CAST(biz_date AS text) - INTO :h_merch, :h_amt, :h_fee, :h_net, :h_st, :h_bizdate - FROM py_payout WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - if (h_st[0] != 'J') { - setl(b, T_RC, 1); - Bchg(b, T_MSG, 0, "반려 건만 재지급 가능", 0L); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); FAIL(b); } - EXEC SQL SELECT account_no INTO :h_newacct FROM py_account - WHERE merchant_id = :h_merch AND status = 'ACTIVE' - ORDER BY updated_at DESC LIMIT 1; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlcode == 100) { setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "재지급 가능한 정상 계좌 없음", 0L); FAIL(b); } - - new_id = pydb_next_payout_id(); - if (new_id < 0) FAIL(b); - if (pydb_insert_payout(new_id, h_merch, h_newacct, h_amt, h_fee, h_net, "R", "STD", h_bizdate) < 0) FAIL(b); - EXEC SQL UPDATE py_payout SET status = 'V', reject_reason = '재지급 전환', updated_at = now() - WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - + /* 2) 마스터 조회부 - 원천 지급건 상세 */ + EXEC SQL SELECT status, amount, fee, net, merchant_id, account_no, biz_date + INTO :h_wst, :h_wamt, :h_wfee, :h_wnet, :h_wmerch, :h_wacct, :h_wdt + FROM py_payout WHERE payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "원천 지급건이 존재하지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "원천 지급건 조회 오류", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + userlog("PAY_REPAYNEW 진입: merch=%s amt=%ld net=%ld", h_wmerch, h_wamt, h_wnet); + /* 3) 업무규칙 분기부 - 파생 생성 적법성 판정 (3분기) */ + if (strcmp(h_wst, "J") != 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "반려(J) 건만 재지급 대상", 0L); + FAIL(b); + } + EXEC SQL SELECT coalesce(max(account_no),'') INTO :h_wacct + FROM py_account WHERE merchant_id = :h_wmerch AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) strcpy(h_wacct, ""); + if (h_wacct[0] == 0) { + setl(b, T_RC, 43); + Bchg(b, T_MSG, 0, "재지급할 정상 계좌를 찾을 수 없음", 0L); + FAIL(b); + } + h_wcnt = 1; + h_wpart = h_wnet; + h_wrem = 0; + if (h_wpart <= 0) { + setl(b, T_RC, 44); + Bchg(b, T_MSG, 0, "재지급 금액이 0 이하", 0L); + FAIL(b); + } + strcpy(h_pval, "999"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_repaynew_max_child'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 999; + if (h_wcnt > h_thr) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "생성 건수 정책 상한 초과", 0L); + FAIL(b); + } + /* 4) 본처리부 - 파생 지급건 생성 */ + h_wlast = 0; + EXEC SQL SELECT nextval('py_payout_seq') INTO :h_wid; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "재지급건 채번 실패", 0L); + FAIL(b); + } + EXEC SQL INSERT INTO py_payout + (payout_id, merchant_id, account_no, amount, fee, net, + status, channel, biz_date) + VALUES (:h_wid, :h_wmerch, :h_wacct, :h_wamt, :h_wfee, + :h_wpart, 'R', 'STD', :h_wdt); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 51); + Bchg(b, T_MSG, 0, "재지급건 생성 실패", 0L); + FAIL(b); + } + h_wlast = h_wid; + /* 원 반려건은 재지급 대체로 종결 */ + EXEC SQL UPDATE py_payout SET status = 'C', reject_reason = '재지급대체', + updated_at = now() + WHERE payout_id = :h_id1 AND status = 'J'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 52); + Bchg(b, T_MSG, 0, "원 반려건 종결 처리 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s 원천=%ld 생성=%ld건 last=%ld 대체=%ld", "PAY_REPAYNEW", (long)(h_wcnt), h_wcnt, h_wlast, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_REPAYNEW', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 파생 생성 후 가맹점 당일 지급 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_wmerch AND status = 'R'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "파생 생성 후 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE merchant_id = :h_wmerch AND status = 'C'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 취소 대체 건 누적: 기준=%ld 실적=%ld", "PAY_REPAYNEW", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - setl(b, T_ID2, new_id); - Bchg(b, T_KEY1, 0, h_newacct, 0L); - userlog("PAY_REPAYNEW old=%ld new=%ld acct=%s", h_pid, new_id, h_newacct); + setl(b, T_ID2, h_wlast); + Bchg(b, T_KEY1, 0, h_wacct, 0L); + Bchg(b, T_MSG, 0, "파생 지급건 생성 완료", 0L); + userlog("PAY_REPAYNEW 완료: 생성=%ld건 last=%ld 대체=%ld건", h_wcnt, h_wlast, h_waff); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_REPAYNEW", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_REPROC.pgc b/app/src/py/svc/PAY_REPROC.pgc index ec8dcb9..6a9dd87 100644 --- a/app/src/py/svc/PAY_REPROC.pgc +++ b/app/src/py/svc/PAY_REPROC.pgc @@ -1,7 +1,9 @@ /* - * PAY_REPROC.pgc - py 모듈 서비스 PAY_REPROC (one service = one file). + * PAY_REPROC.pgc - py (지급) 모듈 온라인 서비스 PAY_REPROC. * 재처리: 직전 상태를 조회한 뒤 실패/보류/취소(J,H,C) -> 요청(R) 전이, 이전 상태 회신. * SELECT prev status then UPDATE with IN(...) guard, report the transition. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [txn]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_REPROC.h" @@ -10,19 +12,146 @@ void PAY_REPROC(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - long h_pay, h_aff; - char h_prev[8]; + long h_id1; + char h_wst[8]; + long h_wamt; + long h_wnet; + char h_wchan[16]; + long h_wretry; + long h_waff; + char h_wmerch[64]; + char h_wacct[64]; + long h_wcnt; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pay = getl(b, T_ID1); - EXEC SQL SELECT status INTO :h_prev FROM py_payout WHERE payout_id = :h_pay; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - /* reprocess only failed/held/cancelled; report whether the transition fired */ - EXEC SQL UPDATE py_payout SET status = 'R', retry_count = retry_count + 1, updated_at = now() - WHERE payout_id = :h_pay AND status IN ('J', 'H', 'C'); - if (sqlca.sqlcode < 0) FAIL(b); - h_aff = sqlca.sqlerrd[2]; - Bchg(b, T_STATUS, 0, h_aff > 0 ? "R" : h_prev, 0L); - setl(b, T_RC, h_aff > 0 ? 0 : 1); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + /* 2) 마스터 조회부 - 지급건 현재 상태/금액/채널/재시도 이력 */ + EXEC SQL SELECT status, amount, net, channel, retry_count, merchant_id, account_no + INTO :h_wst, :h_wamt, :h_wnet, :h_wchan, :h_wretry, :h_wmerch, :h_wacct + FROM py_payout WHERE payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 지급건이 존재하지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급건 조회 중 DB 오류", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + userlog("PAY_REPROC 진입: payout=%ld status=%s amt=%ld chan=%s", + h_id1, h_wst, h_wamt, h_wchan); + /* 3) 업무규칙 분기부 - 상태 전이 가능성/한도/재시도 판정 */ + if (strcmp(h_wst, "P") == 0 || strcmp(h_wst, "X") == 0) { + setl(b, T_RC, 2); Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "이미 종결(지급완료/마감)된 건은 재처리 불가", 0L); FAIL(b); + } + if (strcmp(h_wst, "J") != 0) { + setl(b, T_RC, 3); Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "현재 상태에서는 재처리 전이가 허용되지 않음", 0L); FAIL(b); + } + if (h_wretry >= 3) { + setl(b, T_RC, 5); + Bchg(b, T_MSG, 0, "재시도 한도(3회) 초과 - 수동 처리 필요", 0L); + FAIL(b); + } + strcpy(h_pval, "100000000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_reproc_amt_max'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 100000000; + if (h_wamt > h_thr) { + userlog("PAY_REPROC 정책 상한 초과: amt=%ld thr=%ld", h_wamt, h_thr); + } + /* 4) 본처리부 - 가드 조건부 상태 전이 UPDATE */ + EXEC SQL UPDATE py_payout SET status = 'R', retry_count = retry_count + 1, + updated_at = now() + WHERE payout_id = :h_id1 AND status = 'J'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "재처리 상태 갱신 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "동시성 충돌 - 전이 대상 건이 없음", 0L); + FAIL(b); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s payout=%ld %s->%s amt=%ld aff=%ld", "PAY_REPROC", h_id1, "J", "R", h_wamt, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_REPROC', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계 재산정부 - 당일 동일 상태 건수 재집계 */ + EXEC SQL SELECT count(*) INTO :h_wcnt FROM py_payout + WHERE biz_date = :h_wdate AND status = 'R'; + if (sqlca.sqlcode < 0) h_wcnt = 0; + /* 6) 집계/요약 갱신부 - 가맹점 당일 확정성 지급 실적 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_wmerch + AND biz_date = :h_wdate AND status IN ('A','F','P'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "가맹점 지급 실적 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE merchant_id = :h_wmerch AND biz_date = :h_wdate + AND status IN ('R','H'); + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 미지급 잔량 과다: 기준=%ld 실적=%ld", "PAY_REPROC", h_wa1, h_wa3); + } + /* 8) 응답부 */ + setl(b, T_RC, 0); + Bchg(b, T_STATUS, 0, "R", 0L); + Bchg(b, T_MSG, 0, "재처리 정상 처리", 0L); + userlog("PAY_REPROC 완료: payout=%ld -> %s (당일 %s상태 %ld건)", + h_id1, "R", "R", h_wcnt); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_REPROC", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_REQUEUE.pgc b/app/src/py/svc/PAY_REQUEUE.pgc index 45e724a..668a5a3 100644 --- a/app/src/py/svc/PAY_REQUEUE.pgc +++ b/app/src/py/svc/PAY_REQUEUE.pgc @@ -1,7 +1,9 @@ /* - * PAY_REQUEUE.pgc - py 모듈 서비스 PAY_REQUEUE (one service = one file). + * PAY_REQUEUE.pgc - py (지급) 모듈 온라인 서비스 PAY_REQUEUE. * 실패 일괄 재처리 적재: 당일 반려(J) 건 중 재시도 여력(retry_count<3)이 * 있는 건들을 요청(R)으로 되돌리고 retry_count 를 +1 해 재처리 큐에 올린다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [batch]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_REQUEUE.h" @@ -9,20 +11,131 @@ void PAY_REQUEUE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long requeued; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; + char h_bizdate[64]; + long h_wcnt; + long h_wsum; + long h_waff; + long h_wmax; + char h_wnext[16]; + char h_wsrc[16]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL UPDATE py_payout - SET status = 'R', retry_count = retry_count + 1, reject_reason = '', updated_at = now() + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + /* 2) 마스터 조회부 - 일괄 처리 대상 규모 사전 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wcnt, :h_wsum + FROM py_payout WHERE biz_date = :h_bizdate AND status = 'J' AND retry_count < 3; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "일괄 처리 대상 집계 오류", 0L); + FAIL(b); + } + /* 당일 전체 지급건 대비 비중 확인 */ + EXEC SQL SELECT count(*) INTO :h_wmax FROM py_payout + WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) h_wmax = 0; + userlog("PAY_REQUEUE 진입: 실패 재처리 적재 대상=%ld건 금액=%ld (전체=%ld)", + h_wcnt, h_wsum, h_wmax); + /* 3) 업무규칙 분기부 - 처리 대상/규모 판정 (3분기) */ + if (h_wcnt == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "실패 재처리 적재 대상이 없음", 0L); + setl(b, T_COUNT, 0); + OK(b); + } + strcpy(h_pval, "100000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_requeue_max_rows'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 100000; + if (h_wcnt > h_thr) { + setl(b, T_RC, 30); + Bchg(b, T_MSG, 0, "일괄 처리 건수 상한 초과 - 분할 실행 필요", 0L); + FAIL(b); + } + if (h_wmax > 0 && h_wcnt * 2 > h_wmax) { + userlog("PAY_REQUEUE 대상 비중 과다 경고: %ld/%ld", h_wcnt, h_wmax); + } + /* 4) 본처리부 - 일괄 상태 전이 */ + EXEC SQL UPDATE py_payout SET status = 'R', + retry_count = retry_count + 1, updated_at = now() WHERE biz_date = :h_bizdate AND status = 'J' AND retry_count < 3; - if (sqlca.sqlcode < 0) FAIL(b); - requeued = sqlca.sqlerrd[2]; - - setl(b, T_RC, requeued > 0 ? 0 : 1); - setl(b, T_COUNT, requeued); - userlog("PAY_REQUEUE date=%s requeued=%ld", h_bizdate, requeued); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "실패 재처리 적재 일괄 전이 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "일괄 처리 반영 건수가 0 - 선행 상태 변동", 0L); + FAIL(b); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s %s 대상=%ld 반영=%ld 금액=%ld", "PAY_REQUEUE", "실패 재처리 적재", h_wcnt, h_waff, h_wsum); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_REQUEUE', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 일괄 처리 후 잔여 대상 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE biz_date = :h_wdate AND status IN ('R','A'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "잔여 대상 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE biz_date = :h_wdate AND status = 'H'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 보류 잔량 증가: 기준=%ld 실적=%ld", "PAY_REQUEUE", h_wa1, h_wa3); + } + /* 8) 응답부 */ + setl(b, T_RC, 0); + setl(b, T_COUNT, h_waff); + Bchg(b, T_MSG, 0, "실패 재처리 적재 일괄 처리 완료", 0L); + userlog("PAY_REQUEUE 완료: 반영=%ld건 금액=%ld", h_waff, h_wsum); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_REQUEUE", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_RESULT.pgc b/app/src/py/svc/PAY_RESULT.pgc index f1003db..1b8c657 100644 --- a/app/src/py/svc/PAY_RESULT.pgc +++ b/app/src/py/svc/PAY_RESULT.pgc @@ -1,7 +1,9 @@ /* - * PAY_RESULT.pgc - py 모듈 서비스 PAY_RESULT (one service = one file). + * PAY_RESULT.pgc - py (지급) 모듈 온라인 서비스 PAY_RESULT. * 지급 결과 반영: 편성/요청(F,R) 상태만 지급완료(P)로 전이, 반영 건수를 T_RC 로. * Inline UPDATE with IN(...) guard + affected-count via sqlca.sqlerrd[2]. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [txn]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_RESULT.h" @@ -10,16 +12,143 @@ void PAY_RESULT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - long h_pay, h_aff; + long h_id1; + char h_wst[8]; + long h_wamt; + long h_wnet; + char h_wchan[16]; + long h_wretry; + long h_waff; + char h_wmerch[64]; + char h_wacct[64]; + long h_wcnt; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pay = getl(b, T_ID1); - /* only a filed(F)/requested(R) payout may be marked paid(P) */ + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + /* 2) 마스터 조회부 - 지급건 현재 상태/금액/채널/재시도 이력 */ + EXEC SQL SELECT status, amount, net, channel, retry_count, merchant_id, account_no + INTO :h_wst, :h_wamt, :h_wnet, :h_wchan, :h_wretry, :h_wmerch, :h_wacct + FROM py_payout WHERE payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 지급건이 존재하지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급건 조회 중 DB 오류", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + userlog("PAY_RESULT 진입: payout=%ld status=%s amt=%ld chan=%s", + h_id1, h_wst, h_wamt, h_wchan); + /* 3) 업무규칙 분기부 - 상태 전이 가능성/한도/재시도 판정 */ + if (strcmp(h_wst, "P") == 0 || strcmp(h_wst, "X") == 0) { + setl(b, T_RC, 2); Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "이미 종결(지급완료/마감)된 건은 지급결과 반영 불가", 0L); FAIL(b); + } + if (strcmp(h_wst, "F") != 0) { + setl(b, T_RC, 3); Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "현재 상태에서는 지급결과 반영 전이가 허용되지 않음", 0L); FAIL(b); + } + if (strcmp(h_wchan, "ADVANCE") == 0 && h_wnet > 50000000) { + userlog("PAY_RESULT 고액 선지급 건 주의: payout=%ld net=%ld", h_id1, h_wnet); + } + strcpy(h_pval, "100000000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_result_amt_max'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 100000000; + if (h_wamt > h_thr) { + userlog("PAY_RESULT 정책 상한 초과: amt=%ld thr=%ld", h_wamt, h_thr); + } + /* 4) 본처리부 - 가드 조건부 상태 전이 UPDATE */ EXEC SQL UPDATE py_payout SET status = 'P', updated_at = now() - WHERE payout_id = :h_pay AND status IN ('F', 'R'); - if (sqlca.sqlcode < 0) { userlog("PAY_RESULT FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } - h_aff = sqlca.sqlerrd[2]; - if (h_aff > 0) Bchg(b, T_STATUS, 0, "P", 0L); - setl(b, T_RC, h_aff > 0 ? 0 : 1); + WHERE payout_id = :h_id1 AND status = 'F'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "지급결과 반영 상태 갱신 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "동시성 충돌 - 전이 대상 건이 없음", 0L); + FAIL(b); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s payout=%ld %s->%s amt=%ld aff=%ld", "PAY_RESULT", h_id1, "F", "P", h_wamt, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_RESULT', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계 재산정부 - 당일 동일 상태 건수 재집계 */ + EXEC SQL SELECT count(*) INTO :h_wcnt FROM py_payout + WHERE biz_date = :h_wdate AND status = 'P'; + if (sqlca.sqlcode < 0) h_wcnt = 0; + /* 6) 집계/요약 갱신부 - 가맹점 당일 확정성 지급 실적 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_wmerch + AND biz_date = :h_wdate AND status IN ('A','F','P'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "가맹점 지급 실적 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE merchant_id = :h_wmerch AND biz_date = :h_wdate + AND status IN ('R','H'); + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 미지급 잔량 과다: 기준=%ld 실적=%ld", "PAY_RESULT", h_wa1, h_wa3); + } + /* 8) 응답부 */ + setl(b, T_RC, 0); + Bchg(b, T_STATUS, 0, "P", 0L); + Bchg(b, T_MSG, 0, "지급결과 반영 정상 처리", 0L); + userlog("PAY_RESULT 완료: payout=%ld -> %s (당일 %s상태 %ld건)", + h_id1, "P", "P", h_wcnt); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_RESULT", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_RESVLIST.pgc b/app/src/py/svc/PAY_RESVLIST.pgc index 8490744..3b96ff1 100644 --- a/app/src/py/svc/PAY_RESVLIST.pgc +++ b/app/src/py/svc/PAY_RESVLIST.pgc @@ -1,7 +1,9 @@ /* - * PAY_RESVLIST.pgc - py 모듈 서비스 PAY_RESVLIST (one service = one file). + * PAY_RESVLIST.pgc - py (지급) 모듈 온라인 서비스 PAY_RESVLIST. * 예약 지급 목록: 지정 실행일(T_BIZDATE)에 걸린 예약(S) 을 py_schedule 과 * py_payout 조인 커서로 훑어 예약ID/지급ID/금액을 occurrence 로 회신한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [cursor]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_RESVLIST.h" @@ -9,35 +11,126 @@ void PAY_RESVLIST(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - int i = 0; - long n = 0; + int gi = 0; + long tot_cnt = 0, tot_sum = 0, tot_fee = 0, top_cnt = 0, top_sum = 0; + char top_grp[64]; EXEC SQL BEGIN DECLARE SECTION; - char h_rundate[16]; - long h_sid, h_pid, h_amt; + char h_bizdate[64]; + char h_wgrp[64]; + long h_wgcnt; + long h_wgsum; + long h_wgfee; + long h_wtot; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, h_rundate, sizeof(h_rundate)); - EXEC SQL DECLARE cur_resvlist CURSOR FOR - SELECT s.schedule_id, s.payout_id, coalesce(p.net,0) - FROM py_schedule s LEFT JOIN py_payout p ON s.payout_id = p.payout_id - WHERE s.run_date = :h_rundate AND s.status = 'S' - ORDER BY s.schedule_id; - EXEC SQL OPEN cur_resvlist; - if (sqlca.sqlcode < 0) FAIL(b); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + top_grp[0] = 0; + /* 2) 사전 규모 확인 - 커서 오픈 전 총건수 파악 */ + EXEC SQL SELECT count(*) INTO :h_wtot FROM py_schedule s JOIN py_payout p ON p.payout_id = s.payout_id WHERE s.run_date = :h_bizdate AND s.status = 'S'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "집계 사전 조회 실패", 0L); + FAIL(b); + } + userlog("PAY_RESVLIST 집계 시작: 기준일=%s 대상=%ld건 (예약일별)", h_wdate, h_wtot); + /* 커서 정의: 예약일별 건수/금액 집계 */ + EXEC SQL DECLARE cur_pay_resvlist CURSOR FOR + SELECT to_char(s.run_date, 'YYYY-MM-DD'), count(*), coalesce(sum(p.net),0), coalesce(sum(p.fee),0) + FROM py_schedule s JOIN py_payout p ON p.payout_id = s.payout_id WHERE s.run_date = :h_bizdate AND s.status = 'S' + GROUP BY to_char(s.run_date, 'YYYY-MM-DD') ORDER BY 2 DESC, 1; + EXEC SQL OPEN cur_pay_resvlist; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "집계 커서 OPEN 실패", 0L); + FAIL(b); + } + /* 3) 본처리부 - 커서 순회하며 그룹별 집계/상위 추출 */ for (;;) { - EXEC SQL FETCH cur_resvlist INTO :h_sid, :h_pid, :h_amt; + EXEC SQL FETCH cur_pay_resvlist INTO :h_wgrp, :h_wgcnt, :h_wgsum, :h_wgfee; if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_resvlist; FAIL(b); } - n++; - if (i < 12) { - Bchg(b, T_ARG1, i, (char *)&h_sid, 0L); - Bchg(b, T_ARG2, i, (char *)&h_pid, 0L); - Bchg(b, T_AMT1, i, (char *)&h_amt, 0L); - i++; + if (sqlca.sqlcode < 0) { + EXEC SQL CLOSE cur_pay_resvlist; + setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "집계 FETCH 오류", 0L); FAIL(b); + } + tot_cnt += h_wgcnt; + tot_sum += h_wgsum; + tot_fee += h_wgfee; + if (h_wgcnt > top_cnt) { + top_cnt = h_wgcnt; + top_sum = h_wgsum; + strncpy(top_grp, h_wgrp, sizeof(top_grp)-1); + top_grp[sizeof(top_grp)-1] = 0; + } + if (gi < 10) { + Bchg(b, T_STR1, gi, h_wgrp, 0L); + Bchg(b, T_AMT1, gi, (char *)&h_wgsum, 0L); + Bchg(b, T_ARG1, gi, (char *)&h_wgcnt, 0L); + gi++; } } - EXEC SQL CLOSE cur_resvlist; - setl(b, T_COUNT, n); - setl(b, T_RC, 0); + EXEC SQL CLOSE cur_pay_resvlist; + strcpy(h_pval, "5000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_resvlist_concentration'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 5000; + /* 업무규칙 분기부 - 분포 집중도 판정 (3분기) */ + if (gi == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "예약일별 집계 대상 없음", 0L); + } else if (tot_cnt > 0 && top_cnt * 10000 / tot_cnt > h_thr) { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "특정 예약일 집중 - 확인 필요", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "예약일별 정상 분포", 0L); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s %s별 그룹=%d 건수=%ld 금액=%ld 최다=%s", "PAY_RESVLIST", "예약일", gi, tot_cnt, tot_sum, top_grp); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_RESVLIST', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 8) 응답부 */ + setl(b, T_COUNT, tot_cnt); + setl(b, T_ARG2, (long)gi); + userlog("PAY_RESVLIST 완료: 그룹=%d 총건=%ld 총액=%ld 최다=%s", + gi, tot_cnt, tot_sum, top_grp); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_RESVLIST", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_RESVMODIFY.pgc b/app/src/py/svc/PAY_RESVMODIFY.pgc index bff1366..6925e13 100644 --- a/app/src/py/svc/PAY_RESVMODIFY.pgc +++ b/app/src/py/svc/PAY_RESVMODIFY.pgc @@ -1,7 +1,9 @@ /* - * PAY_RESVMODIFY.pgc - py 모듈 서비스 PAY_RESVMODIFY (one service = one file). + * PAY_RESVMODIFY.pgc - py (지급) 모듈 온라인 서비스 PAY_RESVMODIFY. * 예약 변경: 아직 실행되지 않은 예약(S) 의 실행일(run_date)을 새 일자로 * 변경한다. 이미 실행(D)된 예약이거나 없으면 사유코드로 구분해 거절한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [sched]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_RESVMODIFY.h" @@ -10,24 +12,131 @@ void PAY_RESVMODIFY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - long h_sid; - char h_new[16], h_st[4]; + char h_bizdate[64]; + long h_id2; + char h_wst[8]; + char h_wsst[8]; + long h_wsid; + long h_wcnt; + long h_wdone; + long h_wsum; + long h_waff; + char h_wrun[16]; + char h_wdate[16]; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_sid = getl(b, T_ID2); - gets_(b, T_BIZDATE, h_new, sizeof(h_new)); - if (h_new[0] == 0) { setl(b, T_RC, 3); Bchg(b, T_MSG, 0, "변경일자 미지정", 0L); FAIL(b); } - - EXEC SQL SELECT status INTO :h_st FROM py_schedule WHERE schedule_id = :h_sid; - if (sqlca.sqlcode == 100) { setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "예약 없음", 0L); FAIL(b); } - if (sqlca.sqlcode < 0) FAIL(b); - if (h_st[0] != 'S') { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "실행 전 예약만 변경 가능", 0L); FAIL(b); } - - EXEC SQL UPDATE py_schedule SET run_date = :h_new WHERE schedule_id = :h_sid; - if (sqlca.sqlcode < 0) FAIL(b); - + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + h_id2 = getl(b, T_ID2); + if (h_id2 <= 0) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + /* 2) 마스터 조회부 - 예약 상태 및 기존 실행일 */ + EXEC SQL SELECT status, to_char(run_date, 'YYYY-MM-DD') + INTO :h_wsst, :h_wrun + FROM py_schedule WHERE schedule_id = :h_id2; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 예약이 존재하지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "예약 조회 오류", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wcnt FROM py_schedule + WHERE run_date = :h_bizdate AND status = 'S'; + if (sqlca.sqlcode < 0) h_wcnt = 0; + /* 3) 업무규칙 분기부 - 변경 가능 조건 (3분기) */ + if (strcmp(h_wsst, "D") == 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "이미 실행된 예약은 변경 불가", 0L); + FAIL(b); + } else if (strcmp(h_wsst, "S") != 0) { + setl(b, T_RC, 43); + Bchg(b, T_MSG, 0, "미실행(S) 예약만 변경 가능", 0L); + FAIL(b); + } else if (strcmp(h_wrun, h_bizdate) == 0) { + setl(b, T_RC, 44); + Bchg(b, T_MSG, 0, "기존 실행일과 동일한 일자", 0L); + FAIL(b); + } + /* 4) 본처리부 - 실행일 변경 */ + EXEC SQL UPDATE py_schedule SET run_date = :h_bizdate + WHERE schedule_id = :h_id2 AND status = 'S'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "예약 실행일 변경 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + h_wsid = h_id2; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "변경 대상 예약 없음 - 동시성 충돌", 0L); + FAIL(b); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s sid=%ld 실행일=%s 건수=%ld 처리=%ld", "PAY_RESVMODIFY", h_wsid, h_wrun, h_wcnt, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_RESVMODIFY', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 실행일 기준 예약 잔량 재집계 */ + EXEC SQL SELECT count(*), coalesce(count(DISTINCT payout_id),0) + INTO :h_wa1, :h_wa2 + FROM py_schedule WHERE status = 'S'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "예약 잔량 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_schedule + WHERE status = 'D'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 실행 완료 예약 누적: 기준=%ld 실적=%ld", "PAY_RESVMODIFY", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - Bchg(b, T_STR1, 0, h_new, 0L); - userlog("PAY_RESVMODIFY sid=%ld newdate=%s", h_sid, h_new); + Bchg(b, T_STR1, 0, h_wrun, 0L); + Bchg(b, T_MSG, 0, "지급 예약 처리 완료", 0L); + userlog("PAY_RESVMODIFY 완료: sid=%ld 실행일=%s 건수=%ld", h_wsid, h_wrun, h_wcnt); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_RESVMODIFY", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_RETRY.pgc b/app/src/py/svc/PAY_RETRY.pgc index 010859a..a350cfd 100644 --- a/app/src/py/svc/PAY_RETRY.pgc +++ b/app/src/py/svc/PAY_RETRY.pgc @@ -1,8 +1,9 @@ /* - * PAY_RETRY.pgc - py 모듈 서비스 PAY_RETRY (one service = one file). + * PAY_RETRY.pgc - py (지급) 모듈 온라인 서비스 PAY_RETRY. * 실패 지급 재시도: 실패/보류(J,H) 건만 요청(R)으로 되돌리고 retry_count 증가. - * Moved verbatim from the former inline py_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by py_svr at tpsvrinit. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [txn]. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_RETRY.h" @@ -10,14 +11,146 @@ void PAY_RETRY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - long h_pay; + long h_id1; + char h_wst[8]; + long h_wamt; + long h_wnet; + char h_wchan[16]; + long h_wretry; + long h_waff; + char h_wmerch[64]; + char h_wacct[64]; + long h_wcnt; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pay = getl(b, T_ID1); - EXEC SQL UPDATE py_payout SET status = 'R', retry_count = retry_count + 1, updated_at = now() - WHERE payout_id = :h_pay AND status IN ('J', 'H'); - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlcode == 100) { setl(b, T_RC, 1); FAIL(b); } + + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + /* 2) 마스터 조회부 - 지급건 현재 상태/금액/채널/재시도 이력 */ + EXEC SQL SELECT status, amount, net, channel, retry_count, merchant_id, account_no + INTO :h_wst, :h_wamt, :h_wnet, :h_wchan, :h_wretry, :h_wmerch, :h_wacct + FROM py_payout WHERE payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 지급건이 존재하지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급건 조회 중 DB 오류", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + userlog("PAY_RETRY 진입: payout=%ld status=%s amt=%ld chan=%s", + h_id1, h_wst, h_wamt, h_wchan); + /* 3) 업무규칙 분기부 - 상태 전이 가능성/한도/재시도 판정 */ + if (strcmp(h_wst, "P") == 0 || strcmp(h_wst, "X") == 0) { + setl(b, T_RC, 2); Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "이미 종결(지급완료/마감)된 건은 실패 재시도 불가", 0L); FAIL(b); + } + if (strcmp(h_wst, "J") != 0) { + setl(b, T_RC, 3); Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "현재 상태에서는 실패 재시도 전이가 허용되지 않음", 0L); FAIL(b); + } + if (h_wretry >= 3) { + setl(b, T_RC, 5); + Bchg(b, T_MSG, 0, "재시도 한도(3회) 초과 - 수동 처리 필요", 0L); + FAIL(b); + } + strcpy(h_pval, "100000000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_retry_amt_max'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 100000000; + if (h_wamt > h_thr) { + userlog("PAY_RETRY 정책 상한 초과: amt=%ld thr=%ld", h_wamt, h_thr); + } + /* 4) 본처리부 - 가드 조건부 상태 전이 UPDATE */ + EXEC SQL UPDATE py_payout SET status = 'R', retry_count = retry_count + 1, + updated_at = now() + WHERE payout_id = :h_id1 AND status = 'J'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "실패 재시도 상태 갱신 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "동시성 충돌 - 전이 대상 건이 없음", 0L); + FAIL(b); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s payout=%ld %s->%s amt=%ld aff=%ld", "PAY_RETRY", h_id1, "J", "R", h_wamt, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_RETRY', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계 재산정부 - 당일 동일 상태 건수 재집계 */ + EXEC SQL SELECT count(*) INTO :h_wcnt FROM py_payout + WHERE biz_date = :h_wdate AND status = 'R'; + if (sqlca.sqlcode < 0) h_wcnt = 0; + /* 6) 집계/요약 갱신부 - 가맹점 당일 확정성 지급 실적 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_wmerch + AND biz_date = :h_wdate AND status IN ('A','F','P'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "가맹점 지급 실적 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE merchant_id = :h_wmerch AND biz_date = :h_wdate + AND status IN ('R','H'); + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 미지급 잔량 과다: 기준=%ld 실적=%ld", "PAY_RETRY", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); Bchg(b, T_STATUS, 0, "R", 0L); + Bchg(b, T_MSG, 0, "실패 재시도 정상 처리", 0L); + userlog("PAY_RETRY 완료: payout=%ld -> %s (당일 %s상태 %ld건)", + h_id1, "R", "R", h_wcnt); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_RETRY", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_RETRYONE.pgc b/app/src/py/svc/PAY_RETRYONE.pgc index 94a5ce9..8805995 100644 --- a/app/src/py/svc/PAY_RETRYONE.pgc +++ b/app/src/py/svc/PAY_RETRYONE.pgc @@ -1,7 +1,9 @@ /* - * PAY_RETRYONE.pgc - py 모듈 서비스 PAY_RETRYONE (one service = one file). + * PAY_RETRYONE.pgc - py (지급) 모듈 온라인 서비스 PAY_RETRYONE. * 단건 재시도: 반려(J) 지급건 하나를 요청(R)으로 되돌리며 retry_count 를 * +1 한다. 재시도 횟수가 5회 이상이면 더 이상 재시도하지 않고 거절한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [txn]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_RETRYONE.h" @@ -10,23 +12,147 @@ void PAY_RETRYONE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_rc; - char h_st[4]; + long h_id1; + char h_wst[8]; + long h_wamt; + long h_wnet; + char h_wchan[16]; + long h_wretry; + long h_waff; + char h_wmerch[64]; + char h_wacct[64]; + long h_wcnt; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_ID1); - EXEC SQL SELECT status, retry_count INTO :h_st, :h_rc FROM py_payout WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - if (h_st[0] != 'J') { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "반려 건만 재시도 가능", 0L); FAIL(b); } - if (h_rc >= 5) { setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "재시도 한도 초과", 0L); FAIL(b); } - - EXEC SQL UPDATE py_payout SET status = 'R', retry_count = retry_count + 1, updated_at = now() - WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + /* 2) 마스터 조회부 - 지급건 현재 상태/금액/채널/재시도 이력 */ + EXEC SQL SELECT status, amount, net, channel, retry_count, merchant_id, account_no + INTO :h_wst, :h_wamt, :h_wnet, :h_wchan, :h_wretry, :h_wmerch, :h_wacct + FROM py_payout WHERE payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 지급건이 존재하지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급건 조회 중 DB 오류", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + userlog("PAY_RETRYONE 진입: payout=%ld status=%s amt=%ld chan=%s", + h_id1, h_wst, h_wamt, h_wchan); + /* 3) 업무규칙 분기부 - 상태 전이 가능성/한도/재시도 판정 */ + if (strcmp(h_wst, "P") == 0 || strcmp(h_wst, "X") == 0) { + setl(b, T_RC, 2); Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "이미 종결(지급완료/마감)된 건은 단건 재시도 불가", 0L); FAIL(b); + } + if (strcmp(h_wst, "J") != 0) { + setl(b, T_RC, 3); Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "현재 상태에서는 단건 재시도 전이가 허용되지 않음", 0L); FAIL(b); + } + if (h_wretry >= 3) { + setl(b, T_RC, 5); + Bchg(b, T_MSG, 0, "재시도 한도(3회) 초과 - 수동 처리 필요", 0L); + FAIL(b); + } + strcpy(h_pval, "100000000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_retryone_amt_max'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 100000000; + if (h_wamt > h_thr) { + userlog("PAY_RETRYONE 정책 상한 초과: amt=%ld thr=%ld", h_wamt, h_thr); + } + /* 4) 본처리부 - 가드 조건부 상태 전이 UPDATE */ + EXEC SQL UPDATE py_payout SET status = 'R', retry_count = retry_count + 1, + updated_at = now() + WHERE payout_id = :h_id1 AND status = 'J'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "단건 재시도 상태 갱신 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "동시성 충돌 - 전이 대상 건이 없음", 0L); + FAIL(b); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s payout=%ld %s->%s amt=%ld aff=%ld", "PAY_RETRYONE", h_id1, "J", "R", h_wamt, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_RETRYONE', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계 재산정부 - 당일 동일 상태 건수 재집계 */ + EXEC SQL SELECT count(*) INTO :h_wcnt FROM py_payout + WHERE biz_date = :h_wdate AND status = 'R'; + if (sqlca.sqlcode < 0) h_wcnt = 0; + /* 6) 집계/요약 갱신부 - 가맹점 당일 확정성 지급 실적 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_wmerch + AND biz_date = :h_wdate AND status IN ('A','F','P'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "가맹점 지급 실적 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE merchant_id = :h_wmerch AND biz_date = :h_wdate + AND status IN ('R','H'); + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 미지급 잔량 과다: 기준=%ld 실적=%ld", "PAY_RETRYONE", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - setl(b, T_COUNT, h_rc + 1); Bchg(b, T_STATUS, 0, "R", 0L); - userlog("PAY_RETRYONE payout=%ld attempt=%ld", h_pid, h_rc + 1); + setl(b, T_COUNT, h_waff); + Bchg(b, T_MSG, 0, "단건 재시도 정상 처리", 0L); + userlog("PAY_RETRYONE 완료: payout=%ld -> %s (당일 %s상태 %ld건)", + h_id1, "R", "R", h_wcnt); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_RETRYONE", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_REVERSE.pgc b/app/src/py/svc/PAY_REVERSE.pgc index ab4454b..c2de00b 100644 --- a/app/src/py/svc/PAY_REVERSE.pgc +++ b/app/src/py/svc/PAY_REVERSE.pgc @@ -1,19 +1,175 @@ /* - * PAY_REVERSE.pgc - py 모듈 서비스 PAY_REVERSE (one service = one file). + * PAY_REVERSE.pgc - py (지급) 모듈 온라인 서비스 PAY_REVERSE. * 지급 역분개(reverse): 상태 V(역분개)로 전이하고 계좌 잔액을 원복. - * Moved verbatim from the former inline py_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by py_svr at tpsvrinit. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [txn]. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_REVERSE.h" void PAY_REVERSE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char acct[32]; - long payout_id = getl(b, T_ID1), amount = getl(b, T_AMOUNT); - gets_(b, T_KEY1, acct, sizeof(acct)); - if (pydb_update_status(payout_id, "V") < 0) FAIL(b); - if (acct[0] && pydb_account_add(acct, amount) < 0) FAIL(b); + EXEC SQL BEGIN DECLARE SECTION; + long h_amount; + long h_id1; + char h_key1[64]; + char h_wst[8]; + long h_wamt; + long h_wnet; + char h_wchan[16]; + long h_wretry; + long h_waff; + char h_wmerch[64]; + char h_wacct[64]; + long h_wcnt; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; + EXEC SQL END DECLARE SECTION; + + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_amount = getl(b, T_AMOUNT); + if (h_amount < 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "금액 항목은 음수가 될 수 없음", 0L); + FAIL(b); + } + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + gets_(b, T_KEY1, h_key1, sizeof(h_key1)); + if (h_key1[0] == 0 || strlen(h_key1) < 3) { + setl(b, T_RC, 13); + Bchg(b, T_MSG, 0, "계좌번호/키 값이 유효하지 않음", 0L); + FAIL(b); + } + /* 2) 마스터 조회부 - 지급건 현재 상태/금액/채널/재시도 이력 */ + EXEC SQL SELECT status, amount, net, channel, retry_count, merchant_id, account_no + INTO :h_wst, :h_wamt, :h_wnet, :h_wchan, :h_wretry, :h_wmerch, :h_wacct + FROM py_payout WHERE payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 지급건이 존재하지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급건 조회 중 DB 오류", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + userlog("PAY_REVERSE 진입: payout=%ld status=%s amt=%ld chan=%s", + h_id1, h_wst, h_wamt, h_wchan); + /* 3) 업무규칙 분기부 - 상태 전이 가능성/한도/재시도 판정 */ + if (strcmp(h_wst, "P") == 0 || strcmp(h_wst, "X") == 0) { + setl(b, T_RC, 2); Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "이미 종결(지급완료/마감)된 건은 지급 역분개 불가", 0L); FAIL(b); + } + if (strcmp(h_wst, "P") != 0) { + setl(b, T_RC, 3); Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "현재 상태에서는 지급 역분개 전이가 허용되지 않음", 0L); FAIL(b); + } + if (strcmp(h_wchan, "ADVANCE") == 0 && h_wnet > 50000000) { + userlog("PAY_REVERSE 고액 선지급 건 주의: payout=%ld net=%ld", h_id1, h_wnet); + } + strcpy(h_pval, "100000000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_reverse_amt_max'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 100000000; + if (h_wamt > h_thr) { + userlog("PAY_REVERSE 정책 상한 초과: amt=%ld thr=%ld", h_wamt, h_thr); + } + /* 4) 본처리부 - 가드 조건부 상태 전이 UPDATE */ + EXEC SQL UPDATE py_payout SET status = 'V', updated_at = now() + WHERE payout_id = :h_id1 AND status = 'P'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "지급 역분개 상태 갱신 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "동시성 충돌 - 전이 대상 건이 없음", 0L); + FAIL(b); + } + /* 역분개: 지급계좌 잔액 원복 */ + if (h_wacct[0] != 0) { + if (pydb_account_add(h_wacct, h_wnet) < 0) { + setl(b, T_RC, 52); + Bchg(b, T_MSG, 0, "역분개 계좌 잔액 원복 실패", 0L); + FAIL(b); + } + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s payout=%ld %s->%s amt=%ld aff=%ld", "PAY_REVERSE", h_id1, "P", "V", h_wamt, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_REVERSE', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계 재산정부 - 당일 동일 상태 건수 재집계 */ + EXEC SQL SELECT count(*) INTO :h_wcnt FROM py_payout + WHERE biz_date = :h_wdate AND status = 'V'; + if (sqlca.sqlcode < 0) h_wcnt = 0; + /* 6) 집계/요약 갱신부 - 가맹점 당일 확정성 지급 실적 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_wmerch + AND biz_date = :h_wdate AND status IN ('A','F','P'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "가맹점 지급 실적 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE merchant_id = :h_wmerch AND biz_date = :h_wdate + AND status IN ('R','H'); + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 미지급 잔량 과다: 기준=%ld 실적=%ld", "PAY_REVERSE", h_wa1, h_wa3); + } + /* 8) 응답부 */ + setl(b, T_RC, 0); Bchg(b, T_STATUS, 0, "V", 0L); + Bchg(b, T_MSG, 0, "지급 역분개 정상 처리", 0L); + userlog("PAY_REVERSE 완료: payout=%ld -> %s (당일 %s상태 %ld건)", + h_id1, "V", "V", h_wcnt); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_REVERSE", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_ROLLFWD.pgc b/app/src/py/svc/PAY_ROLLFWD.pgc index 7297a8e..bba2499 100644 --- a/app/src/py/svc/PAY_ROLLFWD.pgc +++ b/app/src/py/svc/PAY_ROLLFWD.pgc @@ -1,7 +1,9 @@ /* - * PAY_ROLLFWD.pgc - py 모듈 서비스 PAY_ROLLFWD (one service = one file). + * PAY_ROLLFWD.pgc - py (지급) 모듈 온라인 서비스 PAY_ROLLFWD. * 미지급 일괄 이월: 지정 기준일 이전의 미지급(R) 건 전부를 차기 영업일 * (T_KEY2)로 biz_date 를 밀어 이월한다. 이월 건수를 회신한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [batch]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_ROLLFWD.h" @@ -9,23 +11,149 @@ void PAY_ROLLFWD(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long moved; EXEC SQL BEGIN DECLARE SECTION; - char h_cut[16], h_next[16]; + char h_bizdate[64]; + char h_key2[64]; + long h_wcnt; + long h_wsum; + long h_waff; + long h_wmax; + char h_wnext[16]; + char h_wsrc[16]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, h_cut, sizeof(h_cut)); - gets_(b, T_KEY2, h_next, sizeof(h_next)); - if (h_next[0] == 0) { setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "차기일자 미지정", 0L); FAIL(b); } - - EXEC SQL UPDATE py_payout SET biz_date = :h_next, updated_at = now() - WHERE status = 'R' AND biz_date < :h_cut; - if (sqlca.sqlcode < 0) FAIL(b); - moved = sqlca.sqlerrd[2]; - + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + gets_(b, T_KEY2, h_key2, sizeof(h_key2)); + if (h_key2[0] == 0 || strlen(h_key2) < 3) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "계좌번호/키 값이 유효하지 않음", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + /* 2) 마스터 조회부 - 일괄 처리 대상 규모 사전 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wcnt, :h_wsum + FROM py_payout WHERE biz_date < :h_bizdate AND status = 'R'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "일괄 처리 대상 집계 오류", 0L); + FAIL(b); + } + /* 당일 전체 지급건 대비 비중 확인 */ + EXEC SQL SELECT count(*) INTO :h_wmax FROM py_payout + WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) h_wmax = 0; + userlog("PAY_ROLLFWD 진입: 미지급 이월 대상=%ld건 금액=%ld (전체=%ld)", + h_wcnt, h_wsum, h_wmax); + /* 3) 업무규칙 분기부 - 처리 대상/규모 판정 (3분기) */ + if (h_wcnt == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "미지급 이월 대상이 없음", 0L); + setl(b, T_COUNT, 0); + OK(b); + } + strcpy(h_pval, "100000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_rollfwd_max_rows'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 100000; + if (h_wcnt > h_thr) { + setl(b, T_RC, 30); + Bchg(b, T_MSG, 0, "일괄 처리 건수 상한 초과 - 분할 실행 필요", 0L); + FAIL(b); + } + if (h_wmax > 0 && h_wcnt * 2 > h_wmax) { + userlog("PAY_ROLLFWD 대상 비중 과다 경고: %ld/%ld", h_wcnt, h_wmax); + } + /* 4) 본처리부 - 일괄 상태 전이 */ + strncpy(h_wsrc, h_bizdate, sizeof(h_wsrc)-1); + h_wsrc[sizeof(h_wsrc)-1] = 0; + strncpy(h_wnext, h_key2, sizeof(h_wnext)-1); + h_wnext[sizeof(h_wnext)-1] = 0; + if (strlen(h_wnext) != 10) { + if (acq_next_bizday(h_wsrc, h_wnext) < 0) { + setl(b, T_RC, 31); + Bchg(b, T_MSG, 0, "차기 영업일 산출 실패", 0L); + FAIL(b); + } + } + EXEC SQL UPDATE py_payout SET biz_date = :h_wnext, updated_at = now() + WHERE biz_date < :h_bizdate AND status = 'R'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "미지급 이월 일괄 전이 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "일괄 처리 반영 건수가 0 - 선행 상태 변동", 0L); + FAIL(b); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s %s 대상=%ld 반영=%ld 금액=%ld", "PAY_ROLLFWD", "미지급 이월", h_wcnt, h_waff, h_wsum); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_ROLLFWD', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 일괄 처리 후 잔여 대상 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE biz_date = :h_wdate AND status IN ('R','A'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "잔여 대상 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE biz_date = :h_wdate AND status = 'H'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 보류 잔량 증가: 기준=%ld 실적=%ld", "PAY_ROLLFWD", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - setl(b, T_COUNT, moved); - Bchg(b, T_STR1, 0, h_next, 0L); - userlog("PAY_ROLLFWD cut=%s next=%s moved=%ld", h_cut, h_next, moved); + setl(b, T_COUNT, h_waff); + Bchg(b, T_STR1, 0, h_wnext, 0L); + Bchg(b, T_MSG, 0, "미지급 이월 일괄 처리 완료", 0L); + userlog("PAY_ROLLFWD 완료: 반영=%ld건 금액=%ld", h_waff, h_wsum); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_ROLLFWD", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_SCHEDCANCEL.pgc b/app/src/py/svc/PAY_SCHEDCANCEL.pgc index ff75523..2e8f779 100644 --- a/app/src/py/svc/PAY_SCHEDCANCEL.pgc +++ b/app/src/py/svc/PAY_SCHEDCANCEL.pgc @@ -1,7 +1,9 @@ /* - * PAY_SCHEDCANCEL.pgc - py 모듈 서비스 PAY_SCHEDCANCEL (one service = one file). + * PAY_SCHEDCANCEL.pgc - py (지급) 모듈 온라인 서비스 PAY_SCHEDCANCEL. * 지급 예약 취소: 아직 미실행(S)인 예약만 DELETE 로 제거한다. 이미 * 실행(D)된 예약은 취소 불가로 거절하고 연결 지급건 id 를 회신. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [sched]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_SCHEDCANCEL.h" @@ -10,25 +12,112 @@ void PAY_SCHEDCANCEL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - long h_sid, h_pid; - char h_st[4]; + long h_id1; + char h_wst[8]; + char h_wsst[8]; + long h_wsid; + long h_wcnt; + long h_wdone; + long h_wsum; + long h_waff; + char h_wrun[16]; + char h_wdate[16]; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_sid = getl(b, T_ID1); - EXEC SQL SELECT payout_id, status INTO :h_pid, :h_st - FROM py_schedule WHERE schedule_id = :h_sid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - if (h_st[0] != 'S') { - setl(b, T_RC, 1); - Bchg(b, T_MSG, 0, "이미 실행된 예약은 취소 불가", 0L); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); FAIL(b); } - EXEC SQL DELETE FROM py_schedule WHERE schedule_id = :h_sid AND status = 'S'; - if (sqlca.sqlcode < 0) FAIL(b); - + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + /* 2) 마스터 조회부 - 미실행/실행 예약 현황 */ + EXEC SQL SELECT count(*) INTO :h_wcnt FROM py_schedule + WHERE payout_id = :h_id1 AND status = 'S'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "예약 조회 오류", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wdone FROM py_schedule + WHERE payout_id = :h_id1 AND status = 'D'; + if (sqlca.sqlcode < 0) h_wdone = 0; + /* 3) 업무규칙 분기부 - 취소 가능 여부 (3분기) */ + if (h_wcnt == 0 && h_wdone > 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "이미 실행된 예약은 취소 불가", 0L); + FAIL(b); + } else if (h_wcnt == 0) { + setl(b, T_RC, 43); + Bchg(b, T_MSG, 0, "취소할 예약이 없음", 0L); + FAIL(b); + } else if (h_wdone > 0) { + userlog("PAY_SCHEDCANCEL 실행 이력과 미실행 예약이 공존: payout=%ld", h_id1); + } + /* 4) 본처리부 - 미실행(S) 예약만 삭제 */ + EXEC SQL DELETE FROM py_schedule + WHERE payout_id = :h_id1 AND status = 'S'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "예약 삭제 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + h_wsid = h_waff; + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s sid=%ld 실행일=%s 건수=%ld 처리=%ld", "PAY_SCHEDCANCEL", h_wsid, h_wrun, h_wcnt, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_SCHEDCANCEL', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 실행일 기준 예약 잔량 재집계 */ + EXEC SQL SELECT count(*), coalesce(count(DISTINCT payout_id),0) + INTO :h_wa1, :h_wa2 + FROM py_schedule WHERE status = 'S'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "예약 잔량 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_schedule + WHERE status = 'D'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 실행 완료 예약 누적: 기준=%ld 실적=%ld", "PAY_SCHEDCANCEL", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - setl(b, T_ID2, h_pid); - Bchg(b, T_MSG, 0, "지급 예약 취소 완료", 0L); - userlog("PAY_SCHEDCANCEL sched=%ld payout=%ld", h_sid, h_pid); + setl(b, T_ID2, h_wsid); + Bchg(b, T_MSG, 0, "지급 예약 처리 완료", 0L); + userlog("PAY_SCHEDCANCEL 완료: sid=%ld 실행일=%s 건수=%ld", h_wsid, h_wrun, h_wcnt); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_SCHEDCANCEL", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_SCHEDULE.pgc b/app/src/py/svc/PAY_SCHEDULE.pgc index 1ebf21a..3cd8803 100644 --- a/app/src/py/svc/PAY_SCHEDULE.pgc +++ b/app/src/py/svc/PAY_SCHEDULE.pgc @@ -1,27 +1,145 @@ /* - * PAY_SCHEDULE.pgc - py 모듈 서비스 PAY_SCHEDULE (one service = one file). + * PAY_SCHEDULE.pgc - py (지급) 모듈 온라인 서비스 PAY_SCHEDULE. * 지급 예약 등록: 지정 실행일로 py_schedule 예약(S) 레코드를 생성. - * Moved verbatim from the former inline py_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by py_svr at tpsvrinit. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [sched]. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_SCHEDULE.h" void PAY_SCHEDULE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char rundate[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_id, h_pay; - char h_rundate[16]; + char h_bizdate[64]; + long h_id1; + char h_wst[8]; + char h_wsst[8]; + long h_wsid; + long h_wcnt; + long h_wdone; + long h_wsum; + long h_waff; + char h_wrun[16]; + char h_wdate[16]; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pay = getl(b, T_ID1); - gets_(b, T_BIZDATE, rundate, sizeof(rundate)); - strncpy(h_rundate, rundate, sizeof(h_rundate)-1); h_rundate[sizeof(h_rundate)-1] = 0; - EXEC SQL SELECT nextval('py_schedule_seq') INTO :h_id; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL INSERT INTO py_schedule (schedule_id, payout_id, run_date, status) - VALUES (:h_id, :h_pay, :h_rundate, 'S'); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_ID2, h_id); + + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + /* 2) 마스터 조회부 - 예약 대상 지급건 상태 및 중복 예약 확인 */ + EXEC SQL SELECT status INTO :h_wst FROM py_payout + WHERE payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "예약 대상 지급건 없음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급건 조회 오류", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wcnt FROM py_schedule + WHERE payout_id = :h_id1 AND status = 'S'; + if (sqlca.sqlcode < 0) h_wcnt = 0; + /* 3) 업무규칙 분기부 - 예약 가능 조건 (3분기) */ + if (strcmp(h_wst, "R") != 0 && strcmp(h_wst, "A") != 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "요청(R)/승인(A) 건만 예약 가능", 0L); + FAIL(b); + } + if (h_wcnt > 0) { + setl(b, T_RC, 43); + Bchg(b, T_MSG, 0, "이미 미실행 예약이 존재", 0L); + FAIL(b); + } + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 44); + Bchg(b, T_MSG, 0, "예약 실행일 형식 오류", 0L); + FAIL(b); + } + strncpy(h_wrun, h_bizdate, sizeof(h_wrun)-1); + h_wrun[sizeof(h_wrun)-1] = 0; + /* 4) 본처리부 - 예약 레코드 채번/생성 */ + EXEC SQL SELECT nextval('py_schedule_seq') INTO :h_wsid; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "예약 일련번호 채번 실패", 0L); + FAIL(b); + } + EXEC SQL INSERT INTO py_schedule + (schedule_id, payout_id, run_date, status) + VALUES (:h_wsid, :h_id1, :h_wrun, 'S'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 51); + Bchg(b, T_MSG, 0, "지급 예약 생성 실패", 0L); + FAIL(b); + } + h_waff = 1; + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s sid=%ld 실행일=%s 건수=%ld 처리=%ld", "PAY_SCHEDULE", h_wsid, h_wrun, h_wcnt, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_SCHEDULE', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 실행일 기준 예약 잔량 재집계 */ + EXEC SQL SELECT count(*), coalesce(count(DISTINCT payout_id),0) + INTO :h_wa1, :h_wa2 + FROM py_schedule WHERE status = 'S'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "예약 잔량 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_schedule + WHERE status = 'D'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 실행 완료 예약 누적: 기준=%ld 실적=%ld", "PAY_SCHEDULE", h_wa1, h_wa3); + } + /* 8) 응답부 */ + setl(b, T_RC, 0); + setl(b, T_ID2, h_wsid); + Bchg(b, T_MSG, 0, "지급 예약 처리 완료", 0L); + userlog("PAY_SCHEDULE 완료: sid=%ld 실행일=%s 건수=%ld", h_wsid, h_wrun, h_wcnt); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_SCHEDULE", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_SEIZE.pgc b/app/src/py/svc/PAY_SEIZE.pgc index cbda3cc..e8580de 100644 --- a/app/src/py/svc/PAY_SEIZE.pgc +++ b/app/src/py/svc/PAY_SEIZE.pgc @@ -1,7 +1,9 @@ /* - * PAY_SEIZE.pgc - py 모듈 서비스 PAY_SEIZE (one service = one file). + * PAY_SEIZE.pgc - py (지급) 모듈 온라인 서비스 PAY_SEIZE. * 압류/가압류 등록: 대상 계좌를 SEIZED 로 전환하고 그 계좌의 미지급(R/A) * 건을 '압류:<사유>' 로 일괄 보류(H)한다. 이미 압류면 거절. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [acct]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_SEIZE.h" @@ -9,33 +11,158 @@ void PAY_SEIZE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char reason[96]; EXEC SQL BEGIN DECLARE SECTION; - char h_acct[32], h_rsn[128]; - long h_held; + char h_key1[64]; + char h_reason[64]; + char h_wast[16]; + long h_wbal; + char h_wholder[64]; + char h_wmerch[64]; + long h_waff; + long h_wcas; + long h_wsum; + long h_wcnt; + char h_wacct[64]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, h_acct, sizeof(h_acct)); - gets_(b, T_REASON, reason, sizeof(reason)); - if (reason[0] == 0) strcpy(reason, "법원명령"); - - EXEC SQL UPDATE py_account SET status = 'SEIZED', updated_at = now() - WHERE account_no = :h_acct AND status <> 'SEIZED'; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlerrd[2] == 0) { - setl(b, T_RC, 1); - Bchg(b, T_MSG, 0, "이미 압류되었거나 없는 계좌", 0L); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + gets_(b, T_KEY1, h_key1, sizeof(h_key1)); + if (h_key1[0] == 0 || strlen(h_key1) < 3) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "계좌번호/키 값이 유효하지 않음", 0L); FAIL(b); } - snprintf(h_rsn, sizeof(h_rsn), "압류:%s", reason); - EXEC SQL UPDATE py_payout SET status = 'H', reject_reason = :h_rsn, updated_at = now() - WHERE account_no = :h_acct AND status IN ('R','A'); - if (sqlca.sqlcode < 0) FAIL(b); - h_held = sqlca.sqlerrd[2]; - + gets_(b, T_REASON, h_reason, sizeof(h_reason)); + if (h_reason[0] == 0) strncpy(h_reason, "사유미기재", sizeof(h_reason)-1); + /* 2) 마스터 조회부 - 대상 지급계좌 확정 후 상태/잔액/예금주 조회 */ + strncpy(h_wacct, h_key1, sizeof(h_wacct)-1); + h_wacct[sizeof(h_wacct)-1] = 0; + EXEC SQL SELECT status, balance, holder, merchant_id + INTO :h_wast, :h_wbal, :h_wholder, :h_wmerch + FROM py_account WHERE account_no = :h_wacct; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 지급계좌가 등록되어 있지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급계좌 조회 중 DB 오류", 0L); + FAIL(b); + } + /* 해당 계좌의 미지급 잔액 규모 파악 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wcnt, :h_wsum + FROM py_payout WHERE account_no = :h_wacct AND status IN ('R','A','H'); + if (sqlca.sqlcode < 0) { h_wcnt = 0; h_wsum = 0; } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + userlog("PAY_SEIZE 진입: acct=%s status=%s bal=%ld 미지급=%ld건", + h_wacct, h_wast, h_wbal, h_wcnt); + /* 3) 업무규칙 분기부 - 계좌 상태 전이 적법성 (3분기) */ + if (strcmp(h_wast, "SEIZED") == 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "이미 압류 등록 상태인 계좌", 0L); + FAIL(b); + } + if (strcmp(h_wast, "ACTIVE") != 0) { + setl(b, T_RC, 43); Bchg(b, T_STATUS, 0, h_wast, 0L); + Bchg(b, T_MSG, 0, "현재 계좌 상태에서는 압류 등록 처리 불가", 0L); FAIL(b); + } + if (h_wbal < 0) { + setl(b, T_RC, 44); + Bchg(b, T_MSG, 0, "잔액이 음수인 비정상 계좌 - 선행 정산 필요", 0L); + FAIL(b); + } + strcpy(h_pval, "1"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_seize_cascade'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 1; + /* 4) 본처리부 - 계좌 상태 전이 후 미지급건 연쇄 처리 */ + EXEC SQL UPDATE py_account SET status = 'SEIZED', updated_at = now() + WHERE account_no = :h_wacct AND status = 'ACTIVE'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "압류 등록 처리 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "계좌 상태 동시성 충돌", 0L); + FAIL(b); + } + EXEC SQL UPDATE py_payout SET status = 'H', reject_reason = '압류', + updated_at = now() + WHERE account_no = :h_wacct AND status IN ('R','A') AND :h_thr > 0; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 51); + Bchg(b, T_MSG, 0, "미지급건 연쇄 처리 실패", 0L); + FAIL(b); + } + h_wcas = sqlca.sqlerrd[2]; + strcpy(h_lvl, "W"); + snprintf(h_amsg, sizeof(h_amsg), "%s acct=%s %s->%s 연쇄=%ld 금액=%ld", "PAY_SEIZE", h_wacct, "ACTIVE", "SEIZED", h_wcas, h_wsum); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_SEIZE', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 계좌 상태 전이 후 잔여 미지급 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE account_no = :h_wacct AND status = 'H'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "계좌 잔여 미지급 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE account_no = :h_wacct AND status IN ('R','A'); + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 연쇄 처리 후 잔여 존재: 기준=%ld 실적=%ld", "PAY_SEIZE", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - setl(b, T_COUNT, h_held); - Bchg(b, T_STR1, 0, h_rsn, 0L); - userlog("PAY_SEIZE acct=%s held=%ld reason=%s", h_acct, h_held, h_rsn); + Bchg(b, T_STATUS, 0, "SEIZED", 0L); + setl(b, T_COUNT, h_wcas); + Bchg(b, T_STR1, 0, h_wholder, 0L); + Bchg(b, T_MSG, 0, "압류 등록 및 미지급건 연쇄처리 완료", 0L); + userlog("PAY_SEIZE 완료: acct=%s 연쇄처리=%ld건 보류금액=%ld", + h_wacct, h_wcas, h_wsum); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_SEIZE", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_SEIZEAMT.pgc b/app/src/py/svc/PAY_SEIZEAMT.pgc index 12bf803..cb4da27 100644 --- a/app/src/py/svc/PAY_SEIZEAMT.pgc +++ b/app/src/py/svc/PAY_SEIZEAMT.pgc @@ -1,7 +1,9 @@ /* - * PAY_SEIZEAMT.pgc - py 모듈 서비스 PAY_SEIZEAMT (one service = one file). + * PAY_SEIZEAMT.pgc - py (지급) 모듈 온라인 서비스 PAY_SEIZEAMT. * 압류 금액 상계: 압류 계좌의 보류(H) 지급건 net 합을 압류가능액으로 산정하고, * 요청 압류액(T_AMOUNT)과 비교해 상계 실행액을 py_ledger 에 SEIZE 로 기표한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [calc]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_SEIZEAMT.h" @@ -9,30 +11,140 @@ void PAY_SEIZEAMT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long claim, applied; EXEC SQL BEGIN DECLARE SECTION; - char h_acct[32], h_bizdate[16]; - long h_avail, h_led; + long h_amount; + char h_bizdate[64]; + char h_key1[64]; + long h_wcnt; + long h_wsum; + long h_wnet; + long h_wlim; + long h_wused; + long h_waff; + long h_wval; + long h_wrate; + char h_wst[8]; + char h_wmerch[64]; + char h_wacct[64]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, h_acct, sizeof(h_acct)); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_amount = getl(b, T_AMOUNT); + if (h_amount < 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "금액 항목은 음수가 될 수 없음", 0L); + FAIL(b); + } gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - claim = getl(b, T_AMOUNT); - - EXEC SQL SELECT coalesce(sum(net),0) INTO :h_avail - FROM py_payout WHERE account_no = :h_acct AND status = 'H'; - if (sqlca.sqlcode < 0) FAIL(b); - applied = (claim > 0 && claim < h_avail) ? claim : h_avail; - if (applied <= 0) { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "압류 가능액 없음", 0L); FAIL(b); } - - h_led = -applied; + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + gets_(b, T_KEY1, h_key1, sizeof(h_key1)); + if (h_key1[0] == 0 || strlen(h_key1) < 3) { + setl(b, T_RC, 13); + Bchg(b, T_MSG, 0, "계좌번호/키 값이 유효하지 않음", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + /* 2) 마스터 조회부 - 압류 계좌 및 보류건 규모 */ + EXEC SQL SELECT status INTO :h_wst FROM py_account + WHERE account_no = :h_key1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 계좌 없음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "계좌 조회 오류", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wcnt, :h_wsum + FROM py_payout WHERE account_no = :h_key1 AND status = 'H' + AND biz_date <= :h_bizdate; + if (sqlca.sqlcode < 0) { h_wcnt = 0; h_wsum = 0; } + strcpy(h_pval, "10000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_seize_ratio_bps'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 10000; + /* 3) 업무규칙 분기부 - 압류가능액 산정 (3분기) */ + if (strcmp(h_wst, "SEIZED") != 0 && strcmp(h_wst, "GARNISH") != 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "압류/가압류 등록 계좌가 아님", 0L); + FAIL(b); + } + if (h_wcnt == 0) { + setl(b, T_RC, 43); + Bchg(b, T_MSG, 0, "압류 대상 보류 지급건 없음", 0L); + FAIL(b); + } + h_wval = h_wsum * h_thr / 10000; + if (h_amount >= h_wval) { + h_waff = h_wval; + } else if (h_amount <= 0) { + setl(b, T_RC, 44); + Bchg(b, T_MSG, 0, "압류 상계액은 1 이상이어야 함", 0L); + FAIL(b); + } else { + h_waff = h_amount; + } + /* 4) 본처리부 - 압류 상계 원장 기표 */ EXEC SQL INSERT INTO py_ledger (ledger_id, payout_id, entry_type, amount, biz_date) - VALUES (nextval('py_ledger_seq'), 0, 'SEIZE', :h_led, :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); - + SELECT nextval('py_ledger_seq'), payout_id, 'SEIZE', net, :h_bizdate + FROM py_payout WHERE account_no = :h_key1 AND status = 'H' + AND biz_date <= :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "압류 상계 원장 기표 실패", 0L); + FAIL(b); + } + h_wused = sqlca.sqlerrd[2]; + strcpy(h_lvl, "W"); + snprintf(h_amsg, sizeof(h_amsg), "%s acct=%s 보류=%ld건 압류가능=%ld 상계=%ld", "PAY_SEIZEAMT", h_key1, h_wcnt, h_wval, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_SEIZEAMT', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - setl(b, T_AMT1, h_avail); - setl(b, T_AMT2, applied); - userlog("PAY_SEIZEAMT acct=%s avail=%ld applied=%ld", h_acct, h_avail, applied); + setl(b, T_AMT1, h_wval); + setl(b, T_AMT2, h_waff); + Bchg(b, T_MSG, 0, "압류 상계액 산정 완료", 0L); + userlog("PAY_SEIZEAMT 완료: acct=%s 상계=%ld 기표=%ld건", h_key1, h_waff, h_wused); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_SEIZEAMT", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_SEIZECHK.pgc b/app/src/py/svc/PAY_SEIZECHK.pgc index 189d008..6632267 100644 --- a/app/src/py/svc/PAY_SEIZECHK.pgc +++ b/app/src/py/svc/PAY_SEIZECHK.pgc @@ -1,7 +1,9 @@ /* - * PAY_SEIZECHK.pgc - py 모듈 서비스 PAY_SEIZECHK (one service = one file). + * PAY_SEIZECHK.pgc - py (지급) 모듈 온라인 서비스 PAY_SEIZECHK. * 압류계좌 지급 차단 점검: 당일 미지급(R/A)건 중 압류(SEIZED) 계좌로 * 나가려는 건수/금액을 조인 집계해 차단 필요 여부를 경보한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [batch]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_SEIZECHK.h" @@ -10,25 +12,133 @@ void PAY_SEIZECHK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_cnt, h_sum; + char h_bizdate[64]; + long h_wcnt; + long h_wsum; + long h_waff; + long h_wmax; + char h_wnext[16]; + char h_wsrc[16]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT count(*), coalesce(sum(p.net),0) INTO :h_cnt, :h_sum - FROM py_payout p JOIN py_account a ON p.account_no = a.account_no - WHERE p.biz_date = :h_bizdate AND p.status IN ('R','A') AND a.status = 'SEIZED'; - if (sqlca.sqlcode < 0) FAIL(b); - - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_sum); - if (h_cnt > 0) { - setl(b, T_RC, 1); - Bchg(b, T_MSG, 0, "압류계좌 대상 지급건 존재 - 차단 필요", 0L); - userlog("PAY_SEIZECHK 차단대상 cnt=%ld amt=%ld", h_cnt, h_sum); - } else { - setl(b, T_RC, 0); - Bchg(b, T_MSG, 0, "압류계좌 지급 시도 없음", 0L); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + /* 2) 마스터 조회부 - 일괄 처리 대상 규모 사전 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wcnt, :h_wsum + FROM py_payout WHERE biz_date = :h_bizdate AND status IN ('R','A'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "일괄 처리 대상 집계 오류", 0L); + FAIL(b); + } + /* 압류 계좌 수 확인 */ + EXEC SQL SELECT count(*) INTO :h_wmax FROM py_account + WHERE status IN ('SEIZED','GARNISH'); + if (sqlca.sqlcode < 0) h_wmax = 0; + userlog("PAY_SEIZECHK 진입: 압류계좌 지급차단 대상=%ld건 금액=%ld (전체=%ld)", + h_wcnt, h_wsum, h_wmax); + /* 3) 업무규칙 분기부 - 처리 대상/규모 판정 (3분기) */ + if (h_wcnt == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "압류계좌 지급차단 대상이 없음", 0L); + setl(b, T_COUNT, 0); + OK(b); + } + strcpy(h_pval, "100000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_seizechk_max_rows'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 100000; + if (h_wcnt > h_thr) { + setl(b, T_RC, 30); + Bchg(b, T_MSG, 0, "일괄 처리 건수 상한 초과 - 분할 실행 필요", 0L); + FAIL(b); + } + if (h_wmax > 0 && h_wcnt * 2 > h_wmax) { + userlog("PAY_SEIZECHK 대상 비중 과다 경고: %ld/%ld", h_wcnt, h_wmax); + } + /* 4) 본처리부 - 일괄 상태 전이 */ + EXEC SQL UPDATE py_payout SET status = 'H', + reject_reason = '압류계좌', updated_at = now() + WHERE biz_date = :h_bizdate AND status IN ('R','A') + AND account_no IN (SELECT account_no FROM py_account + WHERE status IN ('SEIZED','GARNISH')); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "압류계좌 지급차단 일괄 전이 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "일괄 처리 반영 건수가 0 - 선행 상태 변동", 0L); + FAIL(b); + } + strcpy(h_lvl, "W"); + snprintf(h_amsg, sizeof(h_amsg), "%s %s 대상=%ld 반영=%ld 금액=%ld", "PAY_SEIZECHK", "압류계좌 지급차단", h_wcnt, h_waff, h_wsum); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_SEIZECHK', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 일괄 처리 후 잔여 대상 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE biz_date = :h_wdate AND status IN ('R','A'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "잔여 대상 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE biz_date = :h_wdate AND status = 'H'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 보류 잔량 증가: 기준=%ld 실적=%ld", "PAY_SEIZECHK", h_wa1, h_wa3); + } + /* 8) 응답부 */ + setl(b, T_RC, 0); + setl(b, T_COUNT, h_waff); + setl(b, T_GROSS, h_wsum); + Bchg(b, T_MSG, 0, "압류계좌 지급차단 일괄 처리 완료", 0L); + userlog("PAY_SEIZECHK 완료: 반영=%ld건 금액=%ld", h_waff, h_wsum); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_SEIZECHK", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); } OK(b); } diff --git a/app/src/py/svc/PAY_SEIZELIST.pgc b/app/src/py/svc/PAY_SEIZELIST.pgc index 40fc11a..d614cf4 100644 --- a/app/src/py/svc/PAY_SEIZELIST.pgc +++ b/app/src/py/svc/PAY_SEIZELIST.pgc @@ -1,7 +1,9 @@ /* - * PAY_SEIZELIST.pgc - py 모듈 서비스 PAY_SEIZELIST (one service = one file). + * PAY_SEIZELIST.pgc - py (지급) 모듈 온라인 서비스 PAY_SEIZELIST. * 압류 계좌 목록: 압류(SEIZED) 상태 계좌를 커서로 훑어 계좌번호/예금주와 그 * 계좌의 보류(H) 지급 잔액을 occurrence 로 회신한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [cursor]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_SEIZELIST.h" @@ -9,34 +11,122 @@ void PAY_SEIZELIST(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - int i = 0; - long total = 0; + int gi = 0; + long tot_cnt = 0, tot_sum = 0, tot_fee = 0, top_cnt = 0, top_sum = 0; + char top_grp[64]; EXEC SQL BEGIN DECLARE SECTION; - char h_acct[32], h_holder[64]; - long h_held; + char h_wgrp[64]; + long h_wgcnt; + long h_wgsum; + long h_wgfee; + long h_wtot; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - EXEC SQL DECLARE cur_seizelist CURSOR FOR - SELECT a.account_no, a.holder, - coalesce((SELECT sum(net) FROM py_payout p - WHERE p.account_no = a.account_no AND p.status = 'H'), 0) - FROM py_account a WHERE a.status = 'SEIZED' ORDER BY a.account_no; - EXEC SQL OPEN cur_seizelist; - if (sqlca.sqlcode < 0) FAIL(b); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + /* 입력 파라미터가 없는 전역 집계 서비스 - 검증 대상 없음 */ + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + top_grp[0] = 0; + /* 2) 사전 규모 확인 - 커서 오픈 전 총건수 파악 */ + EXEC SQL SELECT count(*) INTO :h_wtot FROM py_account a WHERE a.status = 'SEIZED'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "집계 사전 조회 실패", 0L); + FAIL(b); + } + userlog("PAY_SEIZELIST 집계 시작: 기준일=%s 대상=%ld건 (압류계좌별)", h_wdate, h_wtot); + /* 커서 정의: 압류계좌별 건수/금액 집계 */ + EXEC SQL DECLARE cur_pay_seizelist CURSOR FOR + SELECT a.account_no, 1, coalesce(a.balance,0), 0 + FROM py_account a WHERE a.status = 'SEIZED' + ORDER BY a.account_no; + EXEC SQL OPEN cur_pay_seizelist; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "집계 커서 OPEN 실패", 0L); + FAIL(b); + } + /* 3) 본처리부 - 커서 순회하며 그룹별 집계/상위 추출 */ for (;;) { - EXEC SQL FETCH cur_seizelist INTO :h_acct, :h_holder, :h_held; + EXEC SQL FETCH cur_pay_seizelist INTO :h_wgrp, :h_wgcnt, :h_wgsum, :h_wgfee; if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_seizelist; FAIL(b); } - total++; - if (i < 10) { - Bchg(b, T_STR5, i, h_acct, 0L); - Bchg(b, T_STR6, i, h_holder, 0L); - Bchg(b, T_AMT1, i, (char *)&h_held, 0L); - i++; + if (sqlca.sqlcode < 0) { + EXEC SQL CLOSE cur_pay_seizelist; + setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "집계 FETCH 오류", 0L); FAIL(b); + } + tot_cnt += h_wgcnt; + tot_sum += h_wgsum; + tot_fee += h_wgfee; + if (h_wgcnt > top_cnt) { + top_cnt = h_wgcnt; + top_sum = h_wgsum; + strncpy(top_grp, h_wgrp, sizeof(top_grp)-1); + top_grp[sizeof(top_grp)-1] = 0; + } + if (gi < 10) { + Bchg(b, T_STR1, gi, h_wgrp, 0L); + Bchg(b, T_AMT1, gi, (char *)&h_wgsum, 0L); + Bchg(b, T_ARG1, gi, (char *)&h_wgcnt, 0L); + gi++; } } - EXEC SQL CLOSE cur_seizelist; - setl(b, T_COUNT, total); - setl(b, T_RC, 0); + EXEC SQL CLOSE cur_pay_seizelist; + strcpy(h_pval, "5000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_seizelist_concentration'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 5000; + /* 업무규칙 분기부 - 분포 집중도 판정 (3분기) */ + if (gi == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "압류계좌별 집계 대상 없음", 0L); + } else if (tot_cnt > 0 && top_cnt * 10000 / tot_cnt > h_thr) { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "특정 압류계좌 집중 - 확인 필요", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "압류계좌별 정상 분포", 0L); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s %s별 그룹=%d 건수=%ld 금액=%ld 최다=%s", "PAY_SEIZELIST", "압류계좌", gi, tot_cnt, tot_sum, top_grp); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_SEIZELIST', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 8) 응답부 */ + setl(b, T_COUNT, tot_cnt); + Bchg(b, T_STR5, 0, top_grp, 0L); + Bchg(b, T_STR6, 0, top_grp, 0L); + userlog("PAY_SEIZELIST 완료: 그룹=%d 총건=%ld 총액=%ld 최다=%s", + gi, tot_cnt, tot_sum, top_grp); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_SEIZELIST", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_SEIZEREL.pgc b/app/src/py/svc/PAY_SEIZEREL.pgc index a8cf023..d5b9130 100644 --- a/app/src/py/svc/PAY_SEIZEREL.pgc +++ b/app/src/py/svc/PAY_SEIZEREL.pgc @@ -1,7 +1,9 @@ /* - * PAY_SEIZEREL.pgc - py 모듈 서비스 PAY_SEIZEREL (one service = one file). + * PAY_SEIZEREL.pgc - py (지급) 모듈 온라인 서비스 PAY_SEIZEREL. * 압류 해제: SEIZED 계좌를 ACTIVE 로 되돌리고, 압류 사유로 보류됐던 * 지급건(H, 사유 '압류%')을 요청(R)으로 복원한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [acct]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_SEIZEREL.h" @@ -10,27 +12,153 @@ void PAY_SEIZEREL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - char h_acct[32]; - long h_rel; + char h_key1[64]; + char h_wast[16]; + long h_wbal; + char h_wholder[64]; + char h_wmerch[64]; + long h_waff; + long h_wcas; + long h_wsum; + long h_wcnt; + char h_wacct[64]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, h_acct, sizeof(h_acct)); - EXEC SQL UPDATE py_account SET status = 'ACTIVE', updated_at = now() - WHERE account_no = :h_acct AND status = 'SEIZED'; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlerrd[2] == 0) { - setl(b, T_RC, 1); - Bchg(b, T_MSG, 0, "압류 상태 계좌 아님", 0L); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + gets_(b, T_KEY1, h_key1, sizeof(h_key1)); + if (h_key1[0] == 0 || strlen(h_key1) < 3) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "계좌번호/키 값이 유효하지 않음", 0L); FAIL(b); } - EXEC SQL UPDATE py_payout SET status = 'R', reject_reason = '', updated_at = now() - WHERE account_no = :h_acct AND status = 'H' AND reject_reason LIKE '압류%'; - if (sqlca.sqlcode < 0) FAIL(b); - h_rel = sqlca.sqlerrd[2]; - + /* 2) 마스터 조회부 - 대상 지급계좌 확정 후 상태/잔액/예금주 조회 */ + strncpy(h_wacct, h_key1, sizeof(h_wacct)-1); + h_wacct[sizeof(h_wacct)-1] = 0; + EXEC SQL SELECT status, balance, holder, merchant_id + INTO :h_wast, :h_wbal, :h_wholder, :h_wmerch + FROM py_account WHERE account_no = :h_wacct; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 지급계좌가 등록되어 있지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급계좌 조회 중 DB 오류", 0L); + FAIL(b); + } + /* 해당 계좌의 미지급 잔액 규모 파악 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wcnt, :h_wsum + FROM py_payout WHERE account_no = :h_wacct AND status IN ('R','A','H'); + if (sqlca.sqlcode < 0) { h_wcnt = 0; h_wsum = 0; } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + userlog("PAY_SEIZEREL 진입: acct=%s status=%s bal=%ld 미지급=%ld건", + h_wacct, h_wast, h_wbal, h_wcnt); + /* 3) 업무규칙 분기부 - 계좌 상태 전이 적법성 (3분기) */ + if (strcmp(h_wast, "ACTIVE") == 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "이미 압류 해제 상태인 계좌", 0L); + FAIL(b); + } + if (strcmp(h_wast, "SEIZED") != 0) { + setl(b, T_RC, 43); Bchg(b, T_STATUS, 0, h_wast, 0L); + Bchg(b, T_MSG, 0, "현재 계좌 상태에서는 압류 해제 처리 불가", 0L); FAIL(b); + } + if (h_wbal < 0) { + setl(b, T_RC, 44); + Bchg(b, T_MSG, 0, "잔액이 음수인 비정상 계좌 - 선행 정산 필요", 0L); + FAIL(b); + } + strcpy(h_pval, "1"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_seizerel_cascade'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 1; + /* 4) 본처리부 - 계좌 상태 전이 후 미지급건 연쇄 처리 */ + EXEC SQL UPDATE py_account SET status = 'ACTIVE', updated_at = now() + WHERE account_no = :h_wacct AND status = 'SEIZED'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "압류 해제 처리 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "계좌 상태 동시성 충돌", 0L); + FAIL(b); + } + EXEC SQL UPDATE py_payout SET status = 'A', reject_reason = '', + updated_at = now() + WHERE account_no = :h_wacct AND status IN ('H') AND :h_thr > 0; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 51); + Bchg(b, T_MSG, 0, "미지급건 연쇄 처리 실패", 0L); + FAIL(b); + } + h_wcas = sqlca.sqlerrd[2]; + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s acct=%s %s->%s 연쇄=%ld 금액=%ld", "PAY_SEIZEREL", h_wacct, "SEIZED", "ACTIVE", h_wcas, h_wsum); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_SEIZEREL', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 계좌 상태 전이 후 잔여 미지급 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE account_no = :h_wacct AND status = 'H'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "계좌 잔여 미지급 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE account_no = :h_wacct AND status IN ('R','A'); + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 연쇄 처리 후 잔여 존재: 기준=%ld 실적=%ld", "PAY_SEIZEREL", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - setl(b, T_COUNT, h_rel); Bchg(b, T_STATUS, 0, "ACTIVE", 0L); - userlog("PAY_SEIZEREL acct=%s released=%ld", h_acct, h_rel); + setl(b, T_COUNT, h_wcas); + Bchg(b, T_MSG, 0, "압류 해제 및 미지급건 연쇄처리 완료", 0L); + userlog("PAY_SEIZEREL 완료: acct=%s 연쇄처리=%ld건 보류금액=%ld", + h_wacct, h_wcas, h_wsum); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_SEIZEREL", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_SPLIT.pgc b/app/src/py/svc/PAY_SPLIT.pgc index 2503e08..2991070 100644 --- a/app/src/py/svc/PAY_SPLIT.pgc +++ b/app/src/py/svc/PAY_SPLIT.pgc @@ -1,27 +1,230 @@ /* - * PAY_SPLIT.pgc - py 모듈 서비스 PAY_SPLIT (one service = one file). + * PAY_SPLIT.pgc - py (지급) 모듈 온라인 서비스 PAY_SPLIT. * 지급 분할: 금액을 N건으로 나눠 각각의 지급건(SPLIT 채널)으로 생성. - * Moved verbatim from the former inline py_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by py_svr at tpsvrinit. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [mkpay]. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_SPLIT.h" void PAY_SPLIT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], acct[32], bizdate[16]; - long n, amount, i, part, payout_id; - gets_(b, T_MERCHANT, merch, sizeof(merch)); - gets_(b, T_KEY1, acct, sizeof(acct)); - amount = getl(b, T_AMOUNT); - n = getl(b, T_COUNT); if (n < 1) n = 2; - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - part = amount / n; - for (i = 0; i < n; i++) { - payout_id = pydb_next_payout_id(); - if (payout_id < 0) FAIL(b); - if (pydb_insert_payout(payout_id, merch, acct, part, 0, part, "R", "SPLIT", bizdate) < 0) FAIL(b); + int gi; + EXEC SQL BEGIN DECLARE SECTION; + long h_amount; + char h_bizdate[64]; + long h_count; + char h_key1[64]; + char h_merchant[64]; + long h_wmdr; + char h_wmst[16]; + long h_wlim; + char h_wast[16]; + long h_wbal; + long h_wfee; + long h_wnet; + long h_wpart; + long h_wnum; + long h_wid; + long h_wlast; + long h_wused; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; + EXEC SQL END DECLARE SECTION; + + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_amount = getl(b, T_AMOUNT); + if (h_amount < 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "금액 항목은 음수가 될 수 없음", 0L); + FAIL(b); + } + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + h_count = getl(b, T_COUNT); + if (h_count < 1) h_count = 1; + if (h_count > 999) { + setl(b, T_RC, 13); + Bchg(b, T_MSG, 0, "분할/반복 건수 상한(999) 초과", 0L); + FAIL(b); + } + gets_(b, T_KEY1, h_key1, sizeof(h_key1)); + if (h_key1[0] == 0 || strlen(h_key1) < 3) { + setl(b, T_RC, 14); + Bchg(b, T_MSG, 0, "계좌번호/키 값이 유효하지 않음", 0L); + FAIL(b); + } + gets_(b, T_MERCHANT, h_merchant, sizeof(h_merchant)); + if (h_merchant[0] == 0) { + setl(b, T_RC, 15); + Bchg(b, T_MSG, 0, "가맹점 ID는 필수 입력", 0L); + FAIL(b); + } + /* 2) 마스터 조회부 - 가맹점 계약요율/상태/일한도 */ + EXEC SQL SELECT mdr_bps, status, daily_limit INTO :h_wmdr, :h_wmst, :h_wlim + FROM merchant WHERE merchant_id = :h_merchant; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "등록되지 않은 가맹점", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); + FAIL(b); + } + if (strcmp(h_wmst, "ACTIVE") != 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "비활성/폐업 가맹점은 지급 생성 불가", 0L); + FAIL(b); + } + /* 지급계좌 상태/잔액 확인 */ + EXEC SQL SELECT status, balance INTO :h_wast, :h_wbal + FROM py_account WHERE account_no = :h_key1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 43); + Bchg(b, T_MSG, 0, "지급계좌 미등록", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 44); + Bchg(b, T_MSG, 0, "지급계좌 조회 오류", 0L); + FAIL(b); + } + if (strcmp(h_wast, "ACTIVE") != 0) { + setl(b, T_RC, 45); + Bchg(b, T_MSG, 0, "정상 상태 계좌가 아니어서 지급 불가", 0L); + FAIL(b); + } + /* 당일 기 사용액 조회 - 일한도 통제용 */ + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_wused FROM py_payout + WHERE merchant_id = :h_merchant AND biz_date = :h_bizdate + AND status IN ('A','F','P'); + if (sqlca.sqlcode < 0) h_wused = 0; + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + userlog("PAY_SPLIT 진입: merch=%s amt=%ld mdr=%ldbps 기사용=%ld", + h_merchant, h_amount, h_wmdr, h_wused); + /* 3) 업무규칙 분기부 - 금액 구간별 수수료 산정 (3분기) */ + if (h_amount < 100000) { + h_wfee = acq_fee(h_amount, h_wmdr); + } else if (h_amount < 10000000) { + h_wfee = acq_fee(h_amount, h_wmdr) + acq_vat(acq_fee(h_amount, h_wmdr)); + } else { + h_wfee = acq_fee(h_amount, h_wmdr) - acq_wht_amt(acq_fee(h_amount, h_wmdr), 330); + } + if (h_wfee < 0) h_wfee = 0; + h_wnet = h_amount - h_wfee; + if (h_wnet < 0) h_wnet = 0; + /* 일한도 통제 */ + if (h_wlim > 0 && h_wused + h_wnet > h_wlim) { + setl(b, T_RC, 8); + Bchg(b, T_MSG, 0, "가맹점 일 지급한도 초과", 0L); + FAIL(b); + } + strcpy(h_pval, "500000000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_split_single_max'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 500000000; + if (h_amount > h_thr) { + setl(b, T_RC, 9); + Bchg(b, T_MSG, 0, "1건 지급 상한 정책 초과", 0L); + FAIL(b); + } + h_wnum = h_count; + h_wpart = h_amount / h_wnum; + if (h_wpart <= 0) { + setl(b, T_RC, 10); + Bchg(b, T_MSG, 0, "분할 후 건당 금액이 0 - 분할 건수 과다", 0L); + FAIL(b); + } + /* 4) 본처리부 - 지급건 생성 (분할 시 반복 채번) */ + h_wlast = 0; + for (gi = 0; gi < (int)h_wnum; gi++) { + EXEC SQL SELECT nextval('py_payout_seq') INTO :h_wid; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); Bchg(b, T_MSG, 0, "지급 일련번호 채번 실패", 0L); FAIL(b); + } + EXEC SQL INSERT INTO py_payout + (payout_id, merchant_id, account_no, amount, fee, net, + status, channel, biz_date) + VALUES (:h_wid, :h_merchant, :h_key1, :h_wpart, :h_wfee, + :h_wpart, 'R', 'SPLIT', :h_bizdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 51); Bchg(b, T_MSG, 0, "지급건 생성 INSERT 실패", 0L); FAIL(b); + } + h_wlast = h_wid; + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s merch=%s 생성=%ld건 총액=%ld 수수료=%ld", "PAY_SPLIT", h_merchant, h_wnum, h_amount, h_wfee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_SPLIT', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계 갱신부 - 가맹점 일별 지급 집계 upsert */ + if (pydb_upsert_summary(h_merchant, h_bizdate, h_wnum, h_amount) < 0) { + setl(b, T_RC, 60); + Bchg(b, T_MSG, 0, "가맹점 지급집계(py_summary) 갱신 실패", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 당일 채널별 지급 생성 누계 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_merchant + AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "당일 지급 생성 누계 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE merchant_id = :h_merchant AND biz_date = :h_bizdate + AND status = 'R'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 요청 잔량 누적: 기준=%ld 실적=%ld", "PAY_SPLIT", h_wa1, h_wa3); + } + /* 8) 응답부 */ + setl(b, T_RC, 0); + setl(b, T_COUNT, h_wnum); + Bchg(b, T_MSG, 0, "지급건 생성 완료", 0L); + userlog("PAY_SPLIT 완료: last=%ld 건수=%ld 순액=%ld", h_wlast, h_wnum, h_wnet); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_SPLIT", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); } - setl(b, T_COUNT, n); OK(b); } diff --git a/app/src/py/svc/PAY_SPLITEVEN.pgc b/app/src/py/svc/PAY_SPLITEVEN.pgc index dd32f55..d35c827 100644 --- a/app/src/py/svc/PAY_SPLITEVEN.pgc +++ b/app/src/py/svc/PAY_SPLITEVEN.pgc @@ -1,7 +1,9 @@ /* - * PAY_SPLITEVEN.pgc - py 모듈 서비스 PAY_SPLITEVEN (one service = one file). + * PAY_SPLITEVEN.pgc - py (지급) 모듈 온라인 서비스 PAY_SPLITEVEN. * 균등 분할: 요청(R) 지급건을 N 건으로 균등 분할하되 나머지는 첫 건에 몰아 * 넣고, 원건은 취소(C)한 뒤 자식 지급건(SPLIT)들을 새로 생성한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [derive]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_SPLITEVEN.h" @@ -9,34 +11,180 @@ void PAY_SPLITEVEN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long n, base, rem, k, part, cid; + int gi; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_amt; - char h_merch[64], h_acct[32], h_st[4]; + char h_bizdate[64]; + long h_count; + long h_id1; + char h_wst[8]; + long h_wamt; + long h_wfee; + long h_wnet; + char h_wmerch[64]; + char h_wacct[64]; + long h_wid; + long h_wlast; + long h_wcnt; + long h_wsum; + long h_waff; + long h_wpart; + long h_wrem; + char h_wdt[16]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_ID1); - n = getl(b, T_COUNT); if (n < 2) n = 2; - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - - EXEC SQL SELECT merchant_id, account_no, amount, status - INTO :h_merch, :h_acct, :h_amt, :h_st FROM py_payout WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - if (h_st[0] != 'R') { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "요청 건만 분할 가능", 0L); FAIL(b); } - - base = h_amt / n; - rem = h_amt - base * n; - if (pydb_update_status(h_pid, "C") < 0) FAIL(b); - for (k = 0; k < n; k++) { - part = base + (k == 0 ? rem : 0); - cid = pydb_next_payout_id(); - if (cid < 0) FAIL(b); - if (pydb_insert_payout(cid, h_merch, h_acct, part, 0, part, "R", "SPLIT", bizdate) < 0) FAIL(b); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); } + h_count = getl(b, T_COUNT); + if (h_count < 1) h_count = 1; + if (h_count > 999) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "분할/반복 건수 상한(999) 초과", 0L); + FAIL(b); + } + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 13); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + /* 2) 마스터 조회부 - 원천 지급건 상세 */ + EXEC SQL SELECT status, amount, fee, net, merchant_id, account_no, biz_date + INTO :h_wst, :h_wamt, :h_wfee, :h_wnet, :h_wmerch, :h_wacct, :h_wdt + FROM py_payout WHERE payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "원천 지급건이 존재하지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "원천 지급건 조회 오류", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + userlog("PAY_SPLITEVEN 진입: merch=%s amt=%ld net=%ld", h_wmerch, h_wamt, h_wnet); + /* 3) 업무규칙 분기부 - 파생 생성 적법성 판정 (3분기) */ + if (strcmp(h_wst, "R") != 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "요청(R) 상태 건만 분할 가능", 0L); + FAIL(b); + } + if (h_wnet <= 0) { + setl(b, T_RC, 43); + Bchg(b, T_MSG, 0, "분할 대상 순액이 0 이하", 0L); + FAIL(b); + } + h_wcnt = h_count; + if (h_wcnt < 2) { + setl(b, T_RC, 44); + Bchg(b, T_MSG, 0, "균등분할은 2건 이상이어야 함", 0L); + FAIL(b); + } + h_wpart = h_wnet / h_wcnt; + h_wrem = h_wnet - (h_wpart * h_wcnt); + strcpy(h_pval, "999"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_spliteven_max_child'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 999; + if (h_wcnt > h_thr) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "생성 건수 정책 상한 초과", 0L); + FAIL(b); + } + /* 4) 본처리부 - 파생 지급건 생성 */ + h_wlast = 0; + for (gi = 0; gi < (int)h_wcnt; gi++) { + EXEC SQL SELECT nextval('py_payout_seq') INTO :h_wid; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); Bchg(b, T_MSG, 0, "분할건 채번 실패", 0L); FAIL(b); + } + h_wsum = (gi == 0) ? (h_wpart + h_wrem) : h_wpart; + EXEC SQL INSERT INTO py_payout + (payout_id, merchant_id, account_no, amount, fee, net, + status, channel, biz_date) + VALUES (:h_wid, :h_wmerch, :h_wacct, :h_wsum, 0, + :h_wsum, 'R', 'SPLIT', :h_wdt); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 51); Bchg(b, T_MSG, 0, "분할건 생성 실패", 0L); FAIL(b); + } + h_wlast = h_wid; + } + /* 원천 건은 분할 완료 표시로 취소(C) 전이 */ + EXEC SQL UPDATE py_payout SET status = 'C', reject_reason = '분할대체', + updated_at = now() + WHERE payout_id = :h_id1 AND status = 'R'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 52); + Bchg(b, T_MSG, 0, "원천 지급건 분할대체 처리 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s 원천=%ld 생성=%ld건 last=%ld 대체=%ld", "PAY_SPLITEVEN", (long)(h_wcnt), h_wcnt, h_wlast, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_SPLITEVEN', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 파생 생성 후 가맹점 당일 지급 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_wmerch AND status = 'R'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "파생 생성 후 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE merchant_id = :h_wmerch AND status = 'C'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 취소 대체 건 누적: 기준=%ld 실적=%ld", "PAY_SPLITEVEN", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - setl(b, T_COUNT, n); - setl(b, T_AMOUNT, h_amt); - userlog("PAY_SPLITEVEN src=%ld parts=%ld base=%ld rem=%ld", h_pid, n, base, rem); + setl(b, T_COUNT, h_wcnt); + setl(b, T_AMOUNT, h_wpart); + Bchg(b, T_MSG, 0, "파생 지급건 생성 완료", 0L); + userlog("PAY_SPLITEVEN 완료: 생성=%ld건 last=%ld 대체=%ld건", h_wcnt, h_wlast, h_waff); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_SPLITEVEN", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_SPLITRATIO.pgc b/app/src/py/svc/PAY_SPLITRATIO.pgc index 60be6db..2dbd0b7 100644 --- a/app/src/py/svc/PAY_SPLITRATIO.pgc +++ b/app/src/py/svc/PAY_SPLITRATIO.pgc @@ -1,7 +1,9 @@ /* - * PAY_SPLITRATIO.pgc - py 모듈 서비스 PAY_SPLITRATIO (one service = one file). + * PAY_SPLITRATIO.pgc - py (지급) 모듈 온라인 서비스 PAY_SPLITRATIO. * 비율 분할: 요청(R) 건을 bps 비율로 두 건의 SPLIT 지급건으로 쪼개고 * (수수료도 비례 배분, 끝수는 2번째 건이 흡수) 원건은 취소(C) 처리한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [derive]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_SPLITRATIO.h" @@ -9,45 +11,171 @@ void PAY_SPLITRATIO(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long bps, id1, id2, a1, f1, n1, a2, f2, n2; + int gi; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_amt, h_fee, h_net; - char h_merch[64], h_acct[32], h_st[4], h_bizdate[16]; + long h_arg1; + long h_id1; + char h_wst[8]; + long h_wamt; + long h_wfee; + long h_wnet; + char h_wmerch[64]; + char h_wacct[64]; + long h_wid; + long h_wlast; + long h_wcnt; + long h_wsum; + long h_waff; + long h_wpart; + long h_wrem; + char h_wdt[16]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_ID1); - bps = getl(b, T_ARG1); - if (bps < 1 || bps > 9999) bps = 5000; - - EXEC SQL SELECT merchant_id, account_no, amount, fee, net, status, CAST(biz_date AS text) - INTO :h_merch, :h_acct, :h_amt, :h_fee, :h_net, :h_st, :h_bizdate - FROM py_payout WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - if (h_st[0] != 'R') { - setl(b, T_RC, 1); - Bchg(b, T_MSG, 0, "요청 상태 건만 분할 가능", 0L); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_arg1 = getl(b, T_ARG1); + if (h_arg1 < 0) h_arg1 = 0; + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); FAIL(b); } - a1 = h_amt * bps / 10000; - f1 = h_fee * bps / 10000; - n1 = a1 - f1; - a2 = h_amt - a1; f2 = h_fee - f1; n2 = a2 - f2; - - id1 = pydb_next_payout_id(); - if (id1 < 0) FAIL(b); - if (pydb_insert_payout(id1, h_merch, h_acct, a1, f1, n1, "R", "SPLIT", h_bizdate) < 0) FAIL(b); - id2 = pydb_next_payout_id(); - if (id2 < 0) FAIL(b); - if (pydb_insert_payout(id2, h_merch, h_acct, a2, f2, n2, "R", "SPLIT", h_bizdate) < 0) FAIL(b); - EXEC SQL UPDATE py_payout SET status = 'C', reject_reason = '비율분할', updated_at = now() - WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - + /* 2) 마스터 조회부 - 원천 지급건 상세 */ + EXEC SQL SELECT status, amount, fee, net, merchant_id, account_no, biz_date + INTO :h_wst, :h_wamt, :h_wfee, :h_wnet, :h_wmerch, :h_wacct, :h_wdt + FROM py_payout WHERE payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "원천 지급건이 존재하지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "원천 지급건 조회 오류", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + userlog("PAY_SPLITRATIO 진입: merch=%s amt=%ld net=%ld", h_wmerch, h_wamt, h_wnet); + /* 3) 업무규칙 분기부 - 파생 생성 적법성 판정 (3분기) */ + if (strcmp(h_wst, "R") != 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "요청(R) 상태 건만 분할 가능", 0L); + FAIL(b); + } + if (h_wnet <= 0) { + setl(b, T_RC, 43); + Bchg(b, T_MSG, 0, "분할 대상 순액이 0 이하", 0L); + FAIL(b); + } + if (h_arg1 <= 0 || h_arg1 >= 10000) { + setl(b, T_RC, 44); + Bchg(b, T_MSG, 0, "분할 비율(bps)은 1~9999 범위여야 함", 0L); + FAIL(b); + } + h_wcnt = 2; + h_wpart = h_wnet * h_arg1 / 10000; + h_wrem = h_wnet - h_wpart; + strcpy(h_pval, "999"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_splitratio_max_child'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 999; + if (h_wcnt > h_thr) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "생성 건수 정책 상한 초과", 0L); + FAIL(b); + } + /* 4) 본처리부 - 파생 지급건 생성 */ + h_wlast = 0; + for (gi = 0; gi < (int)h_wcnt; gi++) { + EXEC SQL SELECT nextval('py_payout_seq') INTO :h_wid; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); Bchg(b, T_MSG, 0, "분할건 채번 실패", 0L); FAIL(b); + } + h_wsum = (gi == 0) ? (h_wpart + h_wrem) : h_wpart; + EXEC SQL INSERT INTO py_payout + (payout_id, merchant_id, account_no, amount, fee, net, + status, channel, biz_date) + VALUES (:h_wid, :h_wmerch, :h_wacct, :h_wsum, 0, + :h_wsum, 'R', 'SPLIT', :h_wdt); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 51); Bchg(b, T_MSG, 0, "분할건 생성 실패", 0L); FAIL(b); + } + h_wlast = h_wid; + } + /* 원천 건은 분할 완료 표시로 취소(C) 전이 */ + EXEC SQL UPDATE py_payout SET status = 'C', reject_reason = '분할대체', + updated_at = now() + WHERE payout_id = :h_id1 AND status = 'R'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 52); + Bchg(b, T_MSG, 0, "원천 지급건 분할대체 처리 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s 원천=%ld 생성=%ld건 last=%ld 대체=%ld", "PAY_SPLITRATIO", (long)(h_wcnt), h_wcnt, h_wlast, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_SPLITRATIO', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 파생 생성 후 가맹점 당일 지급 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_wmerch AND status = 'R'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "파생 생성 후 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE merchant_id = :h_wmerch AND status = 'C'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 취소 대체 건 누적: 기준=%ld 실적=%ld", "PAY_SPLITRATIO", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - setl(b, T_ID2, id1); - setl(b, T_ARG2, id2); - setl(b, T_AMT1, n1); - setl(b, T_AMT2, n2); - userlog("PAY_SPLITRATIO id=%ld bps=%ld -> %ld(%ld)/%ld(%ld)", h_pid, bps, id1, n1, id2, n2); + setl(b, T_ID2, h_wlast); + setl(b, T_AMT1, h_wpart); + setl(b, T_AMT2, h_wrem); + setl(b, T_ARG2, h_wrem); + Bchg(b, T_MSG, 0, "파생 지급건 생성 완료", 0L); + userlog("PAY_SPLITRATIO 완료: 생성=%ld건 last=%ld 대체=%ld건", h_wcnt, h_wlast, h_waff); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_SPLITRATIO", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_STATAMT.pgc b/app/src/py/svc/PAY_STATAMT.pgc index 49f5b13..c30d756 100644 --- a/app/src/py/svc/PAY_STATAMT.pgc +++ b/app/src/py/svc/PAY_STATAMT.pgc @@ -1,7 +1,9 @@ /* - * PAY_STATAMT.pgc - py 모듈 서비스 PAY_STATAMT (one service = one file). + * PAY_STATAMT.pgc - py (지급) 모듈 온라인 서비스 PAY_STATAMT. * 금액대별 지급 통계: 당일 지급건을 net 기준 소액/중액/고액 구간으로 CASE * 분류해 구간별 건수/합계를 occurrence 로 회신한다 (금액 분포 파악). + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [cursor]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_STATAMT.h" @@ -9,36 +11,127 @@ void PAY_STATAMT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - int i = 0; - long bands = 0; + int gi = 0; + long tot_cnt = 0, tot_sum = 0, tot_fee = 0, top_cnt = 0, top_sum = 0; + char top_grp[64]; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16], h_band[16]; - long h_cnt, h_amt; + char h_bizdate[64]; + char h_wgrp[64]; + long h_wgcnt; + long h_wgsum; + long h_wgfee; + long h_wtot; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL DECLARE cur_statamt CURSOR FOR - SELECT CASE WHEN net < 100000 THEN '소액' - WHEN net < 1000000 THEN '중액' - ELSE '고액' END AS band, - count(*), coalesce(sum(net),0) - FROM py_payout WHERE biz_date = :h_bizdate GROUP BY 1 ORDER BY 1; - EXEC SQL OPEN cur_statamt; - if (sqlca.sqlcode < 0) FAIL(b); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + top_grp[0] = 0; + /* 2) 사전 규모 확인 - 커서 오픈 전 총건수 파악 */ + EXEC SQL SELECT count(*) INTO :h_wtot FROM py_payout p WHERE p.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "집계 사전 조회 실패", 0L); + FAIL(b); + } + userlog("PAY_STATAMT 집계 시작: 기준일=%s 대상=%ld건 (금액대별)", h_wdate, h_wtot); + /* 커서 정의: 금액대별 건수/금액 집계 */ + EXEC SQL DECLARE cur_pay_statamt CURSOR FOR + SELECT (CASE WHEN p.net < 100000 THEN '소액' WHEN p.net < 1000000 THEN '중액' ELSE '고액' END), count(*), coalesce(sum(p.net),0), coalesce(sum(p.fee),0) + FROM py_payout p WHERE p.biz_date = :h_bizdate + GROUP BY (CASE WHEN p.net < 100000 THEN '소액' WHEN p.net < 1000000 THEN '중액' ELSE '고액' END) ORDER BY 2 DESC, 1; + EXEC SQL OPEN cur_pay_statamt; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "집계 커서 OPEN 실패", 0L); + FAIL(b); + } + /* 3) 본처리부 - 커서 순회하며 그룹별 집계/상위 추출 */ for (;;) { - EXEC SQL FETCH cur_statamt INTO :h_band, :h_cnt, :h_amt; + EXEC SQL FETCH cur_pay_statamt INTO :h_wgrp, :h_wgcnt, :h_wgsum, :h_wgfee; if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_statamt; FAIL(b); } - bands++; - if (i < 6) { - Bchg(b, T_STR7, i, h_band, 0L); - Bchg(b, T_ARG8, i, (char *)&h_cnt, 0L); - Bchg(b, T_AMT1, i, (char *)&h_amt, 0L); - i++; + if (sqlca.sqlcode < 0) { + EXEC SQL CLOSE cur_pay_statamt; + setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "집계 FETCH 오류", 0L); FAIL(b); + } + tot_cnt += h_wgcnt; + tot_sum += h_wgsum; + tot_fee += h_wgfee; + if (h_wgcnt > top_cnt) { + top_cnt = h_wgcnt; + top_sum = h_wgsum; + strncpy(top_grp, h_wgrp, sizeof(top_grp)-1); + top_grp[sizeof(top_grp)-1] = 0; + } + if (gi < 10) { + Bchg(b, T_STR1, gi, h_wgrp, 0L); + Bchg(b, T_AMT1, gi, (char *)&h_wgsum, 0L); + Bchg(b, T_ARG1, gi, (char *)&h_wgcnt, 0L); + gi++; } } - EXEC SQL CLOSE cur_statamt; - setl(b, T_COUNT, bands); - setl(b, T_RC, 0); + EXEC SQL CLOSE cur_pay_statamt; + strcpy(h_pval, "5000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_statamt_concentration'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 5000; + /* 업무규칙 분기부 - 분포 집중도 판정 (3분기) */ + if (gi == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "금액대별 집계 대상 없음", 0L); + } else if (tot_cnt > 0 && top_cnt * 10000 / tot_cnt > h_thr) { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "특정 금액대 집중 - 확인 필요", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "금액대별 정상 분포", 0L); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s %s별 그룹=%d 건수=%ld 금액=%ld 최다=%s", "PAY_STATAMT", "금액대", gi, tot_cnt, tot_sum, top_grp); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_STATAMT', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 8) 응답부 */ + setl(b, T_COUNT, tot_cnt); + setl(b, T_ARG8, (long)gi); + Bchg(b, T_STR7, 0, top_grp, 0L); + userlog("PAY_STATAMT 완료: 그룹=%d 총건=%ld 총액=%ld 최다=%s", + gi, tot_cnt, tot_sum, top_grp); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_STATAMT", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_STATBANK.pgc b/app/src/py/svc/PAY_STATBANK.pgc index 00c23a1..53bffc0 100644 --- a/app/src/py/svc/PAY_STATBANK.pgc +++ b/app/src/py/svc/PAY_STATBANK.pgc @@ -1,7 +1,9 @@ /* - * PAY_STATBANK.pgc - py 모듈 서비스 PAY_STATBANK (one service = one file). + * PAY_STATBANK.pgc - py (지급) 모듈 온라인 서비스 PAY_STATBANK. * 은행별 지급 통계: 당일 지급건을 py_account 조인으로 은행코드별 커서 * 집계해 금액 내림차순 occurrence 로 회신한다 (이체망별 물량 파악). + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [cursor]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_STATBANK.h" @@ -9,35 +11,128 @@ void PAY_STATBANK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - int i = 0; - long banks = 0; + int gi = 0; + long tot_cnt = 0, tot_sum = 0, tot_fee = 0, top_cnt = 0, top_sum = 0; + char top_grp[64]; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16], h_bank[8]; - long h_cnt, h_amt; + char h_bizdate[64]; + char h_wgrp[64]; + long h_wgcnt; + long h_wgsum; + long h_wgfee; + long h_wtot; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL DECLARE cur_statbank CURSOR FOR - SELECT a.bank_code, count(*), coalesce(sum(p.net),0) - FROM py_payout p JOIN py_account a ON p.account_no = a.account_no - WHERE p.biz_date = :h_bizdate - GROUP BY a.bank_code ORDER BY 3 DESC, 1; - EXEC SQL OPEN cur_statbank; - if (sqlca.sqlcode < 0) FAIL(b); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + top_grp[0] = 0; + /* 2) 사전 규모 확인 - 커서 오픈 전 총건수 파악 */ + EXEC SQL SELECT count(*) INTO :h_wtot FROM py_payout p JOIN py_account a ON a.account_no = p.account_no WHERE p.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "집계 사전 조회 실패", 0L); + FAIL(b); + } + userlog("PAY_STATBANK 집계 시작: 기준일=%s 대상=%ld건 (은행별)", h_wdate, h_wtot); + /* 커서 정의: 은행별 건수/금액 집계 */ + EXEC SQL DECLARE cur_pay_statbank CURSOR FOR + SELECT a.bank_code, count(*), coalesce(sum(p.net),0), coalesce(sum(p.fee),0) + FROM py_payout p JOIN py_account a ON a.account_no = p.account_no WHERE p.biz_date = :h_bizdate + GROUP BY a.bank_code ORDER BY 2 DESC, 1; + EXEC SQL OPEN cur_pay_statbank; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "집계 커서 OPEN 실패", 0L); + FAIL(b); + } + /* 3) 본처리부 - 커서 순회하며 그룹별 집계/상위 추출 */ for (;;) { - EXEC SQL FETCH cur_statbank INTO :h_bank, :h_cnt, :h_amt; + EXEC SQL FETCH cur_pay_statbank INTO :h_wgrp, :h_wgcnt, :h_wgsum, :h_wgfee; if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_statbank; FAIL(b); } - banks++; - if (i < 10) { - Bchg(b, T_STR2, i, h_bank, 0L); - Bchg(b, T_ARG3, i, (char *)&h_cnt, 0L); - Bchg(b, T_AMT2, i, (char *)&h_amt, 0L); - i++; + if (sqlca.sqlcode < 0) { + EXEC SQL CLOSE cur_pay_statbank; + setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "집계 FETCH 오류", 0L); FAIL(b); + } + tot_cnt += h_wgcnt; + tot_sum += h_wgsum; + tot_fee += h_wgfee; + if (h_wgcnt > top_cnt) { + top_cnt = h_wgcnt; + top_sum = h_wgsum; + strncpy(top_grp, h_wgrp, sizeof(top_grp)-1); + top_grp[sizeof(top_grp)-1] = 0; + } + if (gi < 10) { + Bchg(b, T_STR1, gi, h_wgrp, 0L); + Bchg(b, T_AMT1, gi, (char *)&h_wgsum, 0L); + Bchg(b, T_ARG1, gi, (char *)&h_wgcnt, 0L); + gi++; } } - EXEC SQL CLOSE cur_statbank; - setl(b, T_COUNT, banks); - setl(b, T_RC, 0); + EXEC SQL CLOSE cur_pay_statbank; + strcpy(h_pval, "5000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_statbank_concentration'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 5000; + /* 업무규칙 분기부 - 분포 집중도 판정 (3분기) */ + if (gi == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "은행별 집계 대상 없음", 0L); + } else if (tot_cnt > 0 && top_cnt * 10000 / tot_cnt > h_thr) { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "특정 은행 집중 - 확인 필요", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "은행별 정상 분포", 0L); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s %s별 그룹=%d 건수=%ld 금액=%ld 최다=%s", "PAY_STATBANK", "은행", gi, tot_cnt, tot_sum, top_grp); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_STATBANK', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 8) 응답부 */ + setl(b, T_COUNT, tot_cnt); + setl(b, T_AMT2, top_sum); + setl(b, T_ARG3, (long)gi); + Bchg(b, T_STR2, 0, top_grp, 0L); + userlog("PAY_STATBANK 완료: 그룹=%d 총건=%ld 총액=%ld 최다=%s", + gi, tot_cnt, tot_sum, top_grp); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_STATBANK", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_STATCHAN.pgc b/app/src/py/svc/PAY_STATCHAN.pgc index 67fbdb9..99005da 100644 --- a/app/src/py/svc/PAY_STATCHAN.pgc +++ b/app/src/py/svc/PAY_STATCHAN.pgc @@ -1,7 +1,9 @@ /* - * PAY_STATCHAN.pgc - py 모듈 서비스 PAY_STATCHAN (one service = one file). + * PAY_STATCHAN.pgc - py (지급) 모듈 온라인 서비스 PAY_STATCHAN. * 채널별 지급 통계: 당일 지급건을 채널(STD/SPLIT/BULK/ADVANCE/URGENT)별 * 커서 집계해 occurrence 로 회신하고 총건수/총액을 함께 준다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [cursor]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_STATCHAN.h" @@ -9,35 +11,127 @@ void PAY_STATCHAN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - int i = 0; - long gcnt = 0, gamt = 0; + int gi = 0; + long tot_cnt = 0, tot_sum = 0, tot_fee = 0, top_cnt = 0, top_sum = 0; + char top_grp[64]; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16], h_chan[16]; - long h_cnt, h_amt; + char h_bizdate[64]; + char h_wgrp[64]; + long h_wgcnt; + long h_wgsum; + long h_wgfee; + long h_wtot; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL DECLARE cur_statchan CURSOR FOR - SELECT channel, count(*), coalesce(sum(net),0) - FROM py_payout WHERE biz_date = :h_bizdate - GROUP BY channel ORDER BY channel; - EXEC SQL OPEN cur_statchan; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH cur_statchan INTO :h_chan, :h_cnt, :h_amt; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_statchan; FAIL(b); } - if (i < 8) { - Bchg(b, T_STR1, i, h_chan, 0L); - Bchg(b, T_ARG1, i, (char *)&h_cnt, 0L); - Bchg(b, T_AMT1, i, (char *)&h_amt, 0L); - } - gcnt += h_cnt; gamt += h_amt; i++; + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + top_grp[0] = 0; + /* 2) 사전 규모 확인 - 커서 오픈 전 총건수 파악 */ + EXEC SQL SELECT count(*) INTO :h_wtot FROM py_payout p WHERE p.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "집계 사전 조회 실패", 0L); + FAIL(b); + } + userlog("PAY_STATCHAN 집계 시작: 기준일=%s 대상=%ld건 (채널별)", h_wdate, h_wtot); + /* 커서 정의: 채널별 건수/금액 집계 */ + EXEC SQL DECLARE cur_pay_statchan CURSOR FOR + SELECT p.channel, count(*), coalesce(sum(p.net),0), coalesce(sum(p.fee),0) + FROM py_payout p WHERE p.biz_date = :h_bizdate + GROUP BY p.channel ORDER BY 2 DESC, 1; + EXEC SQL OPEN cur_pay_statchan; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "집계 커서 OPEN 실패", 0L); + FAIL(b); + } + /* 3) 본처리부 - 커서 순회하며 그룹별 집계/상위 추출 */ + for (;;) { + EXEC SQL FETCH cur_pay_statchan INTO :h_wgrp, :h_wgcnt, :h_wgsum, :h_wgfee; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { + EXEC SQL CLOSE cur_pay_statchan; + setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "집계 FETCH 오류", 0L); FAIL(b); + } + tot_cnt += h_wgcnt; + tot_sum += h_wgsum; + tot_fee += h_wgfee; + if (h_wgcnt > top_cnt) { + top_cnt = h_wgcnt; + top_sum = h_wgsum; + strncpy(top_grp, h_wgrp, sizeof(top_grp)-1); + top_grp[sizeof(top_grp)-1] = 0; + } + if (gi < 10) { + Bchg(b, T_STR1, gi, h_wgrp, 0L); + Bchg(b, T_AMT1, gi, (char *)&h_wgsum, 0L); + Bchg(b, T_ARG1, gi, (char *)&h_wgcnt, 0L); + gi++; + } + } + EXEC SQL CLOSE cur_pay_statchan; + strcpy(h_pval, "5000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_statchan_concentration'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 5000; + /* 업무규칙 분기부 - 분포 집중도 판정 (3분기) */ + if (gi == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "채널별 집계 대상 없음", 0L); + } else if (tot_cnt > 0 && top_cnt * 10000 / tot_cnt > h_thr) { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "특정 채널 집중 - 확인 필요", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "채널별 정상 분포", 0L); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s %s별 그룹=%d 건수=%ld 금액=%ld 최다=%s", "PAY_STATCHAN", "채널", gi, tot_cnt, tot_sum, top_grp); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_STATCHAN', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 8) 응답부 */ + setl(b, T_COUNT, tot_cnt); + setl(b, T_GROSS, tot_sum); + setl(b, T_ARG2, (long)gi); + userlog("PAY_STATCHAN 완료: 그룹=%d 총건=%ld 총액=%ld 최다=%s", + gi, tot_cnt, tot_sum, top_grp); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_STATCHAN", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); } - EXEC SQL CLOSE cur_statchan; - setl(b, T_COUNT, gcnt); - setl(b, T_GROSS, gamt); - setl(b, T_ARG2, (long)i); - setl(b, T_RC, 0); OK(b); } diff --git a/app/src/py/svc/PAY_STATDAILY.pgc b/app/src/py/svc/PAY_STATDAILY.pgc index 56b6d22..7cd07e3 100644 --- a/app/src/py/svc/PAY_STATDAILY.pgc +++ b/app/src/py/svc/PAY_STATDAILY.pgc @@ -1,7 +1,9 @@ /* - * PAY_STATDAILY.pgc - py 모듈 서비스 PAY_STATDAILY (one service = one file). + * PAY_STATDAILY.pgc - py (지급) 모듈 온라인 서비스 PAY_STATDAILY. * 일자별 지급 추이: 시작일부터 N일 구간을 일자별 커서 집계해 occurrence 로 * 회신하고 구간 총액과 일평균(총액/영업일수)을 산출한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [cursor]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_STATDAILY.h" @@ -9,42 +11,131 @@ void PAY_STATDAILY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - int i = 0; - long days_hit = 0, total = 0; + int gi = 0; + long tot_cnt = 0, tot_sum = 0, tot_fee = 0, top_cnt = 0, top_sum = 0; + char top_grp[64]; EXEC SQL BEGIN DECLARE SECTION; - char h_from[16], h_date[16]; - long h_span, h_cnt, h_amt; + long h_arg1; + char h_bizdate[64]; + char h_wgrp[64]; + long h_wgcnt; + long h_wgsum; + long h_wgfee; + long h_wtot; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, h_from, sizeof(h_from)); - h_span = getl(b, T_ARG1); - if (h_span <= 0 || h_span > 31) h_span = 7; - - EXEC SQL DECLARE cur_statdaily CURSOR FOR - SELECT CAST(biz_date AS text), count(*), coalesce(sum(net),0) - FROM py_payout - WHERE biz_date >= CAST(:h_from AS date) - AND biz_date < CAST(:h_from AS date) + CAST(:h_span AS integer) - GROUP BY biz_date ORDER BY biz_date; - EXEC SQL OPEN cur_statdaily; - if (sqlca.sqlcode < 0) FAIL(b); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_arg1 = getl(b, T_ARG1); + if (h_arg1 < 0) h_arg1 = 0; + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + top_grp[0] = 0; + /* 2) 사전 규모 확인 - 커서 오픈 전 총건수 파악 */ + EXEC SQL SELECT count(*) INTO :h_wtot FROM py_payout p WHERE p.biz_date >= :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "집계 사전 조회 실패", 0L); + FAIL(b); + } + userlog("PAY_STATDAILY 집계 시작: 기준일=%s 대상=%ld건 (일자별)", h_wdate, h_wtot); + /* 커서 정의: 일자별 건수/금액 집계 */ + EXEC SQL DECLARE cur_pay_statdaily CURSOR FOR + SELECT to_char(p.biz_date, 'YYYY-MM-DD'), count(*), coalesce(sum(p.net),0), coalesce(sum(p.fee),0) + FROM py_payout p WHERE p.biz_date >= :h_bizdate + GROUP BY to_char(p.biz_date, 'YYYY-MM-DD') ORDER BY 2 DESC, 1; + EXEC SQL OPEN cur_pay_statdaily; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "집계 커서 OPEN 실패", 0L); + FAIL(b); + } + /* 3) 본처리부 - 커서 순회하며 그룹별 집계/상위 추출 */ for (;;) { - EXEC SQL FETCH cur_statdaily INTO :h_date, :h_cnt, :h_amt; + EXEC SQL FETCH cur_pay_statdaily INTO :h_wgrp, :h_wgcnt, :h_wgsum, :h_wgfee; if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_statdaily; FAIL(b); } - days_hit++; - total += h_amt; - if (i < 31) { - Bchg(b, T_STR1, i, h_date, 0L); - Bchg(b, T_ARG2, i, (char *)&h_cnt, 0L); - Bchg(b, T_AMT1, i, (char *)&h_amt, 0L); - i++; + if (sqlca.sqlcode < 0) { + EXEC SQL CLOSE cur_pay_statdaily; + setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "집계 FETCH 오류", 0L); FAIL(b); + } + tot_cnt += h_wgcnt; + tot_sum += h_wgsum; + tot_fee += h_wgfee; + if (h_wgcnt > top_cnt) { + top_cnt = h_wgcnt; + top_sum = h_wgsum; + strncpy(top_grp, h_wgrp, sizeof(top_grp)-1); + top_grp[sizeof(top_grp)-1] = 0; + } + if (gi < 10) { + Bchg(b, T_STR1, gi, h_wgrp, 0L); + Bchg(b, T_AMT1, gi, (char *)&h_wgsum, 0L); + Bchg(b, T_ARG1, gi, (char *)&h_wgcnt, 0L); + gi++; } } - EXEC SQL CLOSE cur_statdaily; - setl(b, T_COUNT, days_hit); - setl(b, T_GROSS, total); - setl(b, T_AMT2, days_hit > 0 ? total / days_hit : 0); - setl(b, T_RC, 0); + EXEC SQL CLOSE cur_pay_statdaily; + strcpy(h_pval, "5000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_statdaily_concentration'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 5000; + /* 업무규칙 분기부 - 분포 집중도 판정 (3분기) */ + if (gi == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "일자별 집계 대상 없음", 0L); + } else if (tot_cnt > 0 && top_cnt * 10000 / tot_cnt > h_thr) { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "특정 일자 집중 - 확인 필요", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "일자별 정상 분포", 0L); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s %s별 그룹=%d 건수=%ld 금액=%ld 최다=%s", "PAY_STATDAILY", "일자", gi, tot_cnt, tot_sum, top_grp); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_STATDAILY', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 8) 응답부 */ + setl(b, T_COUNT, tot_cnt); + setl(b, T_GROSS, tot_sum); + setl(b, T_AMT2, top_sum); + setl(b, T_ARG2, (long)gi); + userlog("PAY_STATDAILY 완료: 그룹=%d 총건=%ld 총액=%ld 최다=%s", + gi, tot_cnt, tot_sum, top_grp); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_STATDAILY", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_STATEMENT.pgc b/app/src/py/svc/PAY_STATEMENT.pgc index d2f37ac..ba9364a 100644 --- a/app/src/py/svc/PAY_STATEMENT.pgc +++ b/app/src/py/svc/PAY_STATEMENT.pgc @@ -1,30 +1,145 @@ /* - * PAY_STATEMENT.pgc - py 모듈 서비스 PAY_STATEMENT (one service = one file). + * PAY_STATEMENT.pgc - py (지급) 모듈 온라인 서비스 PAY_STATEMENT. * 가맹점 지급 명세: 당일 지급완료(P) 건의 건수/금액/수수료/순액을 집계. - * Moved verbatim from the former inline py_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by py_svr at tpsvrinit. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [scalar]. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_STATEMENT.h" void PAY_STATEMENT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_amt, h_fee, h_net; - char h_merch[64], h_bizdate[16]; + char h_bizdate[64]; + char h_merchant[64]; + long h_wcnt; + long h_wsum; + long h_wfee; + long h_wnet; + long h_wrej; + long h_wlim; + long h_wrate; + long h_wpaid; + long h_whold; + char h_wtop[64]; + long h_wdays; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - gets_(b, T_MERCHANT, merch, sizeof(merch)); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) - INTO :h_cnt, :h_amt, :h_fee, :h_net FROM py_payout - WHERE merchant_id = :h_merch AND biz_date = :h_bizdate AND status = 'P'; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_amt); - setl(b, T_FEE, h_fee); - setl(b, T_NET, h_net); + + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + gets_(b, T_MERCHANT, h_merchant, sizeof(h_merchant)); + if (h_merchant[0] == 0) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "가맹점 ID는 필수 입력", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + /* 2) 집계 조회부 - 건수/총액/수수료/순액 스칼라 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) + INTO :h_wcnt, :h_wsum, :h_wfee, :h_wnet + FROM py_payout WHERE merchant_id = :h_merchant AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "지급 집계 조회 오류", 0L); + FAIL(b); + } + /* 상태별 부가 지표 - 반려/지급완료/보류 */ + EXEC SQL SELECT + count(*) FILTER (WHERE status = 'J'), + count(*) FILTER (WHERE status = 'P'), + count(*) FILTER (WHERE status = 'H') + INTO :h_wrej, :h_wpaid, :h_whold + FROM py_payout WHERE merchant_id = :h_merchant AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { h_wrej = 0; h_wpaid = 0; h_whold = 0; } + EXEC SQL SELECT coalesce(daily_limit,0) INTO :h_wlim + FROM merchant WHERE merchant_id = :h_merchant; + if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) h_wlim = 0; + /* 최다 실적 가맹점 1건 추출 */ + EXEC SQL SELECT coalesce(merchant_id,'') INTO :h_wtop + FROM py_payout WHERE merchant_id = :h_merchant AND biz_date = :h_bizdate + GROUP BY merchant_id ORDER BY count(*) DESC LIMIT 1; + if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) strcpy(h_wtop, ""); + h_wdays = 30; + userlog("PAY_STATEMENT 집계: 건수=%ld 총액=%ld 반려=%ld 보류=%ld", + h_wcnt, h_wsum, h_wrej, h_whold); + /* 3) 업무규칙 분기부 - 지표 임계 판정 (3분기) */ + h_wrate = (h_wcnt > 0) ? (h_wrej * 10000 / h_wcnt) : 0; + strcpy(h_pval, "2000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_statement_fail_bps'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 2000; + if (h_wcnt == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "대상 지급건 없음", 0L); + } else if (h_wlim > 0 && h_wnet > h_wlim) { + setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "일 지급한도 초과 상태", 0L); + } else if (h_wrate > h_thr) { + setl(b, T_RC, 3); Bchg(b, T_MSG, 0, "실패율이 임계치를 초과", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "정상 범위", 0L); + } + if (h_whold > 0 && h_whold * 2 > h_wcnt) { + userlog("PAY_STATEMENT 보류 과다 경고: 보류=%ld/전체=%ld", h_whold, h_wcnt); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s 건수=%ld 총액=%ld 반려=%ld 실패율=%ldbps 최다=%s", "PAY_STATEMENT", h_wcnt, h_wsum, h_wrej, h_wrate, h_wtop); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_STATEMENT', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계 갱신부 - py_summary upsert */ + if (h_wcnt > 0) { + if (pydb_upsert_summary(h_merchant, h_wdate, h_wcnt, h_wsum) < 0) { + setl(b, T_RC, 60); + Bchg(b, T_MSG, 0, "가맹점 지급집계 갱신 실패", 0L); + FAIL(b); + } + } + /* 8) 응답부 */ + setl(b, T_COUNT, h_wcnt); + setl(b, T_GROSS, h_wsum); + setl(b, T_NET, h_wnet); + setl(b, T_FEE, h_wfee); + userlog("PAY_STATEMENT 완료: 건수=%ld 총액=%ld 실패율=%ldbps", + h_wcnt, h_wsum, h_wrate); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_STATEMENT", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_STATFEE.pgc b/app/src/py/svc/PAY_STATFEE.pgc index 2162e6d..7507dcf 100644 --- a/app/src/py/svc/PAY_STATFEE.pgc +++ b/app/src/py/svc/PAY_STATFEE.pgc @@ -1,7 +1,9 @@ /* - * PAY_STATFEE.pgc - py 모듈 서비스 PAY_STATFEE (one service = one file). + * PAY_STATFEE.pgc - py (지급) 모듈 온라인 서비스 PAY_STATFEE. * 채널별 수수료 통계: 당일 지급건을 channel 별로 수수료합/net합 집계하되 * 수수료가 발생한 채널만 HAVING 으로 남겨 occurrence 로 회신한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [cursor]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_STATFEE.h" @@ -9,34 +11,128 @@ void PAY_STATFEE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - int i = 0; - long rows = 0; + int gi = 0; + long tot_cnt = 0, tot_sum = 0, tot_fee = 0, top_cnt = 0, top_sum = 0; + char top_grp[64]; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16], h_chan[16]; - long h_fee, h_net; + char h_bizdate[64]; + char h_wgrp[64]; + long h_wgcnt; + long h_wgsum; + long h_wgfee; + long h_wtot; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL DECLARE cur_statfee CURSOR FOR - SELECT channel, coalesce(sum(fee),0), coalesce(sum(net),0) - FROM py_payout WHERE biz_date = :h_bizdate - GROUP BY channel HAVING coalesce(sum(fee),0) > 0 ORDER BY 2 DESC; - EXEC SQL OPEN cur_statfee; - if (sqlca.sqlcode < 0) FAIL(b); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + top_grp[0] = 0; + /* 2) 사전 규모 확인 - 커서 오픈 전 총건수 파악 */ + EXEC SQL SELECT count(*) INTO :h_wtot FROM py_payout p WHERE p.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "집계 사전 조회 실패", 0L); + FAIL(b); + } + userlog("PAY_STATFEE 집계 시작: 기준일=%s 대상=%ld건 (채널수수료별)", h_wdate, h_wtot); + /* 커서 정의: 채널수수료별 건수/금액 집계 */ + EXEC SQL DECLARE cur_pay_statfee CURSOR FOR + SELECT p.channel, count(*), coalesce(sum(p.net),0), coalesce(sum(p.fee),0) + FROM py_payout p WHERE p.biz_date = :h_bizdate + GROUP BY p.channel ORDER BY 2 DESC, 1; + EXEC SQL OPEN cur_pay_statfee; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "집계 커서 OPEN 실패", 0L); + FAIL(b); + } + /* 3) 본처리부 - 커서 순회하며 그룹별 집계/상위 추출 */ for (;;) { - EXEC SQL FETCH cur_statfee INTO :h_chan, :h_fee, :h_net; + EXEC SQL FETCH cur_pay_statfee INTO :h_wgrp, :h_wgcnt, :h_wgsum, :h_wgfee; if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_statfee; FAIL(b); } - rows++; - if (i < 8) { - Bchg(b, T_STR3, i, h_chan, 0L); - Bchg(b, T_AMT2, i, (char *)&h_fee, 0L); - Bchg(b, T_AMT3, i, (char *)&h_net, 0L); - i++; + if (sqlca.sqlcode < 0) { + EXEC SQL CLOSE cur_pay_statfee; + setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "집계 FETCH 오류", 0L); FAIL(b); + } + tot_cnt += h_wgcnt; + tot_sum += h_wgsum; + tot_fee += h_wgfee; + if (h_wgcnt > top_cnt) { + top_cnt = h_wgcnt; + top_sum = h_wgsum; + strncpy(top_grp, h_wgrp, sizeof(top_grp)-1); + top_grp[sizeof(top_grp)-1] = 0; + } + if (gi < 10) { + Bchg(b, T_STR1, gi, h_wgrp, 0L); + Bchg(b, T_AMT1, gi, (char *)&h_wgsum, 0L); + Bchg(b, T_ARG1, gi, (char *)&h_wgcnt, 0L); + gi++; } } - EXEC SQL CLOSE cur_statfee; - setl(b, T_COUNT, rows); - setl(b, T_RC, 0); + EXEC SQL CLOSE cur_pay_statfee; + strcpy(h_pval, "5000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_statfee_concentration'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 5000; + /* 업무규칙 분기부 - 분포 집중도 판정 (3분기) */ + if (gi == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "채널수수료별 집계 대상 없음", 0L); + } else if (tot_cnt > 0 && top_cnt * 10000 / tot_cnt > h_thr) { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "특정 채널수수료 집중 - 확인 필요", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "채널수수료별 정상 분포", 0L); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s %s별 그룹=%d 건수=%ld 금액=%ld 최다=%s", "PAY_STATFEE", "채널수수료", gi, tot_cnt, tot_sum, top_grp); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_STATFEE', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 8) 응답부 */ + setl(b, T_COUNT, tot_cnt); + setl(b, T_AMT2, top_sum); + setl(b, T_AMT3, top_sum); + Bchg(b, T_STR3, 0, top_grp, 0L); + userlog("PAY_STATFEE 완료: 그룹=%d 총건=%ld 총액=%ld 최다=%s", + gi, tot_cnt, tot_sum, top_grp); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_STATFEE", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_STATMONTH.pgc b/app/src/py/svc/PAY_STATMONTH.pgc index e21832f..f4e23ae 100644 --- a/app/src/py/svc/PAY_STATMONTH.pgc +++ b/app/src/py/svc/PAY_STATMONTH.pgc @@ -1,7 +1,9 @@ /* - * PAY_STATMONTH.pgc - py 모듈 서비스 PAY_STATMONTH (one service = one file). + * PAY_STATMONTH.pgc - py (지급) 모듈 온라인 서비스 PAY_STATMONTH. * 월별 지급 통계: 지정 기간(T_BIZDATE~T_KEY2)의 지급건을 'YYYY-MM' 단위로 * 묶어 월별 건수/net합을 occurrence 로 회신한다 (월 추이 파악). + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [cursor]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_STATMONTH.h" @@ -9,37 +11,135 @@ void PAY_STATMONTH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - int i = 0; - long months = 0; + int gi = 0; + long tot_cnt = 0, tot_sum = 0, tot_fee = 0, top_cnt = 0, top_sum = 0; + char top_grp[64]; EXEC SQL BEGIN DECLARE SECTION; - char h_from[16], h_to[16], h_mon[16]; - long h_cnt, h_amt; + char h_bizdate[64]; + char h_key2[64]; + char h_wgrp[64]; + long h_wgcnt; + long h_wgsum; + long h_wgfee; + long h_wtot; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, h_from, sizeof(h_from)); - gets_(b, T_KEY2, h_to, sizeof(h_to)); - if (h_to[0] == 0) strcpy(h_to, h_from); - - EXEC SQL DECLARE cur_statmonth CURSOR FOR - SELECT to_char(biz_date, 'YYYY-MM'), count(*), coalesce(sum(net),0) - FROM py_payout WHERE biz_date BETWEEN :h_from AND :h_to - GROUP BY 1 ORDER BY 1; - EXEC SQL OPEN cur_statmonth; - if (sqlca.sqlcode < 0) FAIL(b); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + gets_(b, T_KEY2, h_key2, sizeof(h_key2)); + if (h_key2[0] == 0 || strlen(h_key2) < 3) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "계좌번호/키 값이 유효하지 않음", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + top_grp[0] = 0; + /* 2) 사전 규모 확인 - 커서 오픈 전 총건수 파악 */ + EXEC SQL SELECT count(*) INTO :h_wtot FROM py_payout p WHERE p.biz_date >= :h_bizdate AND to_char(p.biz_date,'YYYY-MM-DD') <= :h_key2; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "집계 사전 조회 실패", 0L); + FAIL(b); + } + userlog("PAY_STATMONTH 집계 시작: 기준일=%s 대상=%ld건 (월별)", h_wdate, h_wtot); + /* 커서 정의: 월별 건수/금액 집계 */ + EXEC SQL DECLARE cur_pay_statmonth CURSOR FOR + SELECT to_char(p.biz_date, 'YYYY-MM'), count(*), coalesce(sum(p.net),0), coalesce(sum(p.fee),0) + FROM py_payout p WHERE p.biz_date >= :h_bizdate AND to_char(p.biz_date,'YYYY-MM-DD') <= :h_key2 + GROUP BY to_char(p.biz_date, 'YYYY-MM') ORDER BY 2 DESC, 1; + EXEC SQL OPEN cur_pay_statmonth; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "집계 커서 OPEN 실패", 0L); + FAIL(b); + } + /* 3) 본처리부 - 커서 순회하며 그룹별 집계/상위 추출 */ for (;;) { - EXEC SQL FETCH cur_statmonth INTO :h_mon, :h_cnt, :h_amt; + EXEC SQL FETCH cur_pay_statmonth INTO :h_wgrp, :h_wgcnt, :h_wgsum, :h_wgfee; if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_statmonth; FAIL(b); } - months++; - if (i < 12) { - Bchg(b, T_STR4, i, h_mon, 0L); - Bchg(b, T_ARG3, i, (char *)&h_cnt, 0L); - Bchg(b, T_AMT2, i, (char *)&h_amt, 0L); - i++; + if (sqlca.sqlcode < 0) { + EXEC SQL CLOSE cur_pay_statmonth; + setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "집계 FETCH 오류", 0L); FAIL(b); + } + tot_cnt += h_wgcnt; + tot_sum += h_wgsum; + tot_fee += h_wgfee; + if (h_wgcnt > top_cnt) { + top_cnt = h_wgcnt; + top_sum = h_wgsum; + strncpy(top_grp, h_wgrp, sizeof(top_grp)-1); + top_grp[sizeof(top_grp)-1] = 0; + } + if (gi < 10) { + Bchg(b, T_STR1, gi, h_wgrp, 0L); + Bchg(b, T_AMT1, gi, (char *)&h_wgsum, 0L); + Bchg(b, T_ARG1, gi, (char *)&h_wgcnt, 0L); + gi++; } } - EXEC SQL CLOSE cur_statmonth; - setl(b, T_COUNT, months); - setl(b, T_RC, 0); + EXEC SQL CLOSE cur_pay_statmonth; + strcpy(h_pval, "5000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_statmonth_concentration'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 5000; + /* 업무규칙 분기부 - 분포 집중도 판정 (3분기) */ + if (gi == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "월별 집계 대상 없음", 0L); + } else if (tot_cnt > 0 && top_cnt * 10000 / tot_cnt > h_thr) { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "특정 월 집중 - 확인 필요", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "월별 정상 분포", 0L); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s %s별 그룹=%d 건수=%ld 금액=%ld 최다=%s", "PAY_STATMONTH", "월", gi, tot_cnt, tot_sum, top_grp); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_STATMONTH', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 8) 응답부 */ + setl(b, T_COUNT, tot_cnt); + setl(b, T_AMT2, top_sum); + setl(b, T_ARG3, (long)gi); + Bchg(b, T_STR4, 0, top_grp, 0L); + userlog("PAY_STATMONTH 완료: 그룹=%d 총건=%ld 총액=%ld 최다=%s", + gi, tot_cnt, tot_sum, top_grp); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_STATMONTH", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_STATRANGE.pgc b/app/src/py/svc/PAY_STATRANGE.pgc index f502fed..dccf1d8 100644 --- a/app/src/py/svc/PAY_STATRANGE.pgc +++ b/app/src/py/svc/PAY_STATRANGE.pgc @@ -1,7 +1,9 @@ /* - * PAY_STATRANGE.pgc - py 모듈 서비스 PAY_STATRANGE (one service = one file). + * PAY_STATRANGE.pgc - py (지급) 모듈 온라인 서비스 PAY_STATRANGE. * 금액 구간별 분포: 당일 지급건을 소액(<10만)/중액(10만~100만)/고액(100만+) * 3개 구간으로 CASE 집계해 건수 분포와 전체 건수를 회신한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [cursor]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_STATRANGE.h" @@ -9,24 +11,127 @@ void PAY_STATRANGE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + int gi = 0; + long tot_cnt = 0, tot_sum = 0, tot_fee = 0, top_cnt = 0, top_sum = 0; + char top_grp[64]; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_small, h_mid, h_large, h_tot; + char h_bizdate[64]; + char h_wgrp[64]; + long h_wgcnt; + long h_wgsum; + long h_wgfee; + long h_wtot; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT coalesce(sum(CASE WHEN net < 100000 THEN 1 ELSE 0 END),0), - coalesce(sum(CASE WHEN net >= 100000 AND net < 1000000 THEN 1 ELSE 0 END),0), - coalesce(sum(CASE WHEN net >= 1000000 THEN 1 ELSE 0 END),0), - count(*) - INTO :h_small, :h_mid, :h_large, :h_tot - FROM py_payout WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - - setl(b, T_ARG1, h_small); - setl(b, T_ARG2, h_mid); - setl(b, T_ARG3, h_large); - setl(b, T_COUNT, h_tot); - setl(b, T_RC, 0); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + top_grp[0] = 0; + /* 2) 사전 규모 확인 - 커서 오픈 전 총건수 파악 */ + EXEC SQL SELECT count(*) INTO :h_wtot FROM py_payout p WHERE p.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "집계 사전 조회 실패", 0L); + FAIL(b); + } + userlog("PAY_STATRANGE 집계 시작: 기준일=%s 대상=%ld건 (금액구간별)", h_wdate, h_wtot); + /* 커서 정의: 금액구간별 건수/금액 집계 */ + EXEC SQL DECLARE cur_pay_statrange CURSOR FOR + SELECT (CASE WHEN p.net < 100000 THEN 'L' WHEN p.net < 1000000 THEN 'M' ELSE 'H' END), count(*), coalesce(sum(p.net),0), coalesce(sum(p.fee),0) + FROM py_payout p WHERE p.biz_date = :h_bizdate + GROUP BY (CASE WHEN p.net < 100000 THEN 'L' WHEN p.net < 1000000 THEN 'M' ELSE 'H' END) ORDER BY 2 DESC, 1; + EXEC SQL OPEN cur_pay_statrange; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "집계 커서 OPEN 실패", 0L); + FAIL(b); + } + /* 3) 본처리부 - 커서 순회하며 그룹별 집계/상위 추출 */ + for (;;) { + EXEC SQL FETCH cur_pay_statrange INTO :h_wgrp, :h_wgcnt, :h_wgsum, :h_wgfee; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { + EXEC SQL CLOSE cur_pay_statrange; + setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "집계 FETCH 오류", 0L); FAIL(b); + } + tot_cnt += h_wgcnt; + tot_sum += h_wgsum; + tot_fee += h_wgfee; + if (h_wgcnt > top_cnt) { + top_cnt = h_wgcnt; + top_sum = h_wgsum; + strncpy(top_grp, h_wgrp, sizeof(top_grp)-1); + top_grp[sizeof(top_grp)-1] = 0; + } + if (gi < 10) { + Bchg(b, T_STR1, gi, h_wgrp, 0L); + Bchg(b, T_AMT1, gi, (char *)&h_wgsum, 0L); + Bchg(b, T_ARG1, gi, (char *)&h_wgcnt, 0L); + gi++; + } + } + EXEC SQL CLOSE cur_pay_statrange; + strcpy(h_pval, "5000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_statrange_concentration'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 5000; + /* 업무규칙 분기부 - 분포 집중도 판정 (3분기) */ + if (gi == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "금액구간별 집계 대상 없음", 0L); + } else if (tot_cnt > 0 && top_cnt * 10000 / tot_cnt > h_thr) { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "특정 금액구간 집중 - 확인 필요", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "금액구간별 정상 분포", 0L); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s %s별 그룹=%d 건수=%ld 금액=%ld 최다=%s", "PAY_STATRANGE", "금액구간", gi, tot_cnt, tot_sum, top_grp); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_STATRANGE', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 8) 응답부 */ + setl(b, T_COUNT, tot_cnt); + setl(b, T_ARG2, (long)gi); + setl(b, T_ARG3, (long)gi); + userlog("PAY_STATRANGE 완료: 그룹=%d 총건=%ld 총액=%ld 최다=%s", + gi, tot_cnt, tot_sum, top_grp); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_STATRANGE", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_STATSTATUS.pgc b/app/src/py/svc/PAY_STATSTATUS.pgc index c2fcbec..ca712c0 100644 --- a/app/src/py/svc/PAY_STATSTATUS.pgc +++ b/app/src/py/svc/PAY_STATSTATUS.pgc @@ -1,7 +1,9 @@ /* - * PAY_STATSTATUS.pgc - py 모듈 서비스 PAY_STATSTATUS (one service = one file). + * PAY_STATSTATUS.pgc - py (지급) 모듈 온라인 서비스 PAY_STATSTATUS. * 상태별 지급 통계: 당일 지급건을 status 별 커서로 건수/net합/수수료합까지 * 4열 집계해 occurrence 로 회신한다 (요청/승인/지급/취소 흐름 분포 파악). + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [cursor]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_STATSTATUS.h" @@ -9,34 +11,129 @@ void PAY_STATSTATUS(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - int i = 0; - long rows = 0; + int gi = 0; + long tot_cnt = 0, tot_sum = 0, tot_fee = 0, top_cnt = 0, top_sum = 0; + char top_grp[64]; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16], h_st[4]; - long h_cnt, h_net, h_fee; + char h_bizdate[64]; + char h_wgrp[64]; + long h_wgcnt; + long h_wgsum; + long h_wgfee; + long h_wtot; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL DECLARE cur_statstatus CURSOR FOR - SELECT status, count(*), coalesce(sum(net),0), coalesce(sum(fee),0) - FROM py_payout WHERE biz_date = :h_bizdate GROUP BY status ORDER BY status; - EXEC SQL OPEN cur_statstatus; - if (sqlca.sqlcode < 0) FAIL(b); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + top_grp[0] = 0; + /* 2) 사전 규모 확인 - 커서 오픈 전 총건수 파악 */ + EXEC SQL SELECT count(*) INTO :h_wtot FROM py_payout p WHERE p.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "집계 사전 조회 실패", 0L); + FAIL(b); + } + userlog("PAY_STATSTATUS 집계 시작: 기준일=%s 대상=%ld건 (상태별)", h_wdate, h_wtot); + /* 커서 정의: 상태별 건수/금액 집계 */ + EXEC SQL DECLARE cur_pay_statstatus CURSOR FOR + SELECT p.status, count(*), coalesce(sum(p.net),0), coalesce(sum(p.fee),0) + FROM py_payout p WHERE p.biz_date = :h_bizdate + GROUP BY p.status ORDER BY 2 DESC, 1; + EXEC SQL OPEN cur_pay_statstatus; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "집계 커서 OPEN 실패", 0L); + FAIL(b); + } + /* 3) 본처리부 - 커서 순회하며 그룹별 집계/상위 추출 */ for (;;) { - EXEC SQL FETCH cur_statstatus INTO :h_st, :h_cnt, :h_net, :h_fee; + EXEC SQL FETCH cur_pay_statstatus INTO :h_wgrp, :h_wgcnt, :h_wgsum, :h_wgfee; if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_statstatus; FAIL(b); } - rows++; - if (i < 9) { - Bchg(b, T_STR3, i, h_st, 0L); - Bchg(b, T_ARG4, i, (char *)&h_cnt, 0L); - Bchg(b, T_AMT3, i, (char *)&h_net, 0L); - Bchg(b, T_AMT4, i, (char *)&h_fee, 0L); - i++; + if (sqlca.sqlcode < 0) { + EXEC SQL CLOSE cur_pay_statstatus; + setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "집계 FETCH 오류", 0L); FAIL(b); + } + tot_cnt += h_wgcnt; + tot_sum += h_wgsum; + tot_fee += h_wgfee; + if (h_wgcnt > top_cnt) { + top_cnt = h_wgcnt; + top_sum = h_wgsum; + strncpy(top_grp, h_wgrp, sizeof(top_grp)-1); + top_grp[sizeof(top_grp)-1] = 0; + } + if (gi < 10) { + Bchg(b, T_STR1, gi, h_wgrp, 0L); + Bchg(b, T_AMT1, gi, (char *)&h_wgsum, 0L); + Bchg(b, T_ARG1, gi, (char *)&h_wgcnt, 0L); + gi++; } } - EXEC SQL CLOSE cur_statstatus; - setl(b, T_COUNT, rows); - setl(b, T_RC, 0); + EXEC SQL CLOSE cur_pay_statstatus; + strcpy(h_pval, "5000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_statstatus_concentration'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 5000; + /* 업무규칙 분기부 - 분포 집중도 판정 (3분기) */ + if (gi == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "상태별 집계 대상 없음", 0L); + } else if (tot_cnt > 0 && top_cnt * 10000 / tot_cnt > h_thr) { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "특정 상태 집중 - 확인 필요", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "상태별 정상 분포", 0L); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s %s별 그룹=%d 건수=%ld 금액=%ld 최다=%s", "PAY_STATSTATUS", "상태", gi, tot_cnt, tot_sum, top_grp); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_STATSTATUS', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 8) 응답부 */ + setl(b, T_COUNT, tot_cnt); + setl(b, T_AMT3, top_sum); + setl(b, T_AMT4, top_sum); + setl(b, T_ARG4, (long)gi); + Bchg(b, T_STR3, 0, top_grp, 0L); + userlog("PAY_STATSTATUS 완료: 그룹=%d 총건=%ld 총액=%ld 최다=%s", + gi, tot_cnt, tot_sum, top_grp); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_STATSTATUS", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_STATUS.pgc b/app/src/py/svc/PAY_STATUS.pgc index d1e3b7f..8699786 100644 --- a/app/src/py/svc/PAY_STATUS.pgc +++ b/app/src/py/svc/PAY_STATUS.pgc @@ -1,17 +1,141 @@ /* - * PAY_STATUS.pgc - py 모듈 서비스 PAY_STATUS (one service = one file). + * PAY_STATUS.pgc - py (지급) 모듈 온라인 서비스 PAY_STATUS. * 지급 상태 조회: dbio 로 단건 상태를 읽어 회신. - * Moved verbatim from the former inline py_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by py_svr at tpsvrinit. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [lookup]. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_STATUS.h" void PAY_STATUS(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long payout_id = getl(b, T_ID1); - char st[4]; - if (pydb_get_status(payout_id, st) < 0) FAIL(b); - Bchg(b, T_STATUS, 0, st, 0L); + EXEC SQL BEGIN DECLARE SECTION; + long h_id1; + char h_wst[8]; + long h_wamt; + long h_wfee; + long h_wnet; + char h_wmerch[64]; + char h_wacct[64]; + char h_wchan[16]; + long h_wfile; + long h_wretry; + char h_wrsn[128]; + char h_wdt[16]; + long h_wadj; + long h_wled; + char h_wholder[64]; + char h_wdate[16]; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; + EXEC SQL END DECLARE SECTION; + + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + /* 2) 마스터 조회부 - 지급건 전체 상세 */ + EXEC SQL SELECT status, amount, fee, net, merchant_id, account_no, + channel, file_id, retry_count, reject_reason, biz_date + INTO :h_wst, :h_wamt, :h_wfee, :h_wnet, :h_wmerch, :h_wacct, + :h_wchan, :h_wfile, :h_wretry, :h_wrsn, :h_wdt + FROM py_payout WHERE payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "조회 대상 지급건이 존재하지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급건 조회 중 DB 오류", 0L); + FAIL(b); + } + /* 예금주 및 조정/원장 누계 부가 조회 */ + EXEC SQL SELECT coalesce(holder,'') INTO :h_wholder + FROM py_account WHERE account_no = :h_wacct; + if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) strcpy(h_wholder, "미등록"); + h_wadj = pydb_adjust_delta_sum(h_id1); + if (h_wadj < 0) h_wadj = 0; + h_wled = pydb_ledger_balance(h_id1); + if (h_wled < 0) h_wled = 0; + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + userlog("PAY_STATUS 조회: payout=%ld status=%s net=%ld chan=%s", + h_id1, h_wst, h_wnet, h_wchan); + /* 3) 업무규칙 분기부 - 진행 단계 판정 (3분기) */ + if (strcmp(h_wst, "P") == 0 || strcmp(h_wst, "X") == 0) { + setl(b, T_ARG3, 3); Bchg(b, T_MSG, 0, "지급 완료 단계", 0L); + } else if (strcmp(h_wst, "F") == 0 || strcmp(h_wst, "A") == 0) { + setl(b, T_ARG3, 2); Bchg(b, T_MSG, 0, "지급 진행 단계", 0L); + } else if (strcmp(h_wst, "J") == 0 || strcmp(h_wst, "C") == 0) { + setl(b, T_ARG3, 9); Bchg(b, T_MSG, 0, "반려/취소 종결 단계", 0L); + } else { + setl(b, T_ARG3, 1); Bchg(b, T_MSG, 0, "접수/보류 단계", 0L); + } + /* 정합 점검: amount - fee 와 net 의 불일치 여부 */ + if (h_wamt - h_wfee != h_wnet) { + userlog("PAY_STATUS 순액 불일치 경고: payout=%ld amt=%ld fee=%ld net=%ld", + h_id1, h_wamt, h_wfee, h_wnet); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s payout=%ld status=%s net=%ld 조정=%ld", "PAY_STATUS", h_id1, h_wst, h_wnet, h_wadj); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_STATUS', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 가맹점 당일 지급 실적 참조 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_wmerch AND biz_date = :h_wdt; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "가맹점 실적 참조 집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_ledger + WHERE payout_id = :h_id1; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 원장 기표 다수: 기준=%ld 실적=%ld", "PAY_STATUS", h_wa1, h_wa3); + } + /* 8) 응답부 - 상세 필드 회신 */ + setl(b, T_RC, 0); + Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_CHANNEL, 0, h_wchan, 0L); + Bchg(b, T_REASON, 0, h_wrsn, 0L); + setl(b, T_ID2, h_wfile); + setl(b, T_ARG1, h_wretry); + setl(b, T_ARG2, h_wadj); + Bchg(b, T_BIZDATE, 0, h_wdt, 0L); + Bchg(b, T_STR2, 0, h_wholder, 0L); + userlog("PAY_STATUS 완료: payout=%ld status=%s", h_id1, h_wst); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_STATUS", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_SUMMARY.pgc b/app/src/py/svc/PAY_SUMMARY.pgc index 1bfa74b..fee84a9 100644 --- a/app/src/py/svc/PAY_SUMMARY.pgc +++ b/app/src/py/svc/PAY_SUMMARY.pgc @@ -1,28 +1,143 @@ /* - * PAY_SUMMARY.pgc - py 모듈 서비스 PAY_SUMMARY (one service = one file). + * PAY_SUMMARY.pgc - py (지급) 모듈 온라인 서비스 PAY_SUMMARY. * 가맹점 일별 지급 집계 조회: py_summary 에서 건수/금액을 읽어 회신(무행 시 0). - * Moved verbatim from the former inline py_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by py_svr at tpsvrinit. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [scalar]. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_SUMMARY.h" void PAY_SUMMARY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_amt; - char h_merch[64], h_bizdate[16]; + char h_bizdate[64]; + char h_merchant[64]; + long h_wcnt; + long h_wsum; + long h_wfee; + long h_wnet; + long h_wrej; + long h_wlim; + long h_wrate; + long h_wpaid; + long h_whold; + char h_wtop[64]; + long h_wdays; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - gets_(b, T_MERCHANT, merch, sizeof(merch)); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT coalesce(payout_count,0), coalesce(payout_amount,0) INTO :h_cnt, :h_amt - FROM py_summary WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlcode == 100) { h_cnt = 0; h_amt = 0; } - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_amt); + + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + gets_(b, T_MERCHANT, h_merchant, sizeof(h_merchant)); + if (h_merchant[0] == 0) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "가맹점 ID는 필수 입력", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + /* 2) 집계 조회부 - 건수/총액/수수료/순액 스칼라 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) + INTO :h_wcnt, :h_wsum, :h_wfee, :h_wnet + FROM py_payout WHERE merchant_id = :h_merchant AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "지급 집계 조회 오류", 0L); + FAIL(b); + } + /* 상태별 부가 지표 - 반려/지급완료/보류 */ + EXEC SQL SELECT + count(*) FILTER (WHERE status = 'J'), + count(*) FILTER (WHERE status = 'P'), + count(*) FILTER (WHERE status = 'H') + INTO :h_wrej, :h_wpaid, :h_whold + FROM py_payout WHERE merchant_id = :h_merchant AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { h_wrej = 0; h_wpaid = 0; h_whold = 0; } + EXEC SQL SELECT coalesce(daily_limit,0) INTO :h_wlim + FROM merchant WHERE merchant_id = :h_merchant; + if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) h_wlim = 0; + /* 최다 실적 가맹점 1건 추출 */ + EXEC SQL SELECT coalesce(merchant_id,'') INTO :h_wtop + FROM py_payout WHERE merchant_id = :h_merchant AND biz_date = :h_bizdate + GROUP BY merchant_id ORDER BY count(*) DESC LIMIT 1; + if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) strcpy(h_wtop, ""); + h_wdays = 30; + userlog("PAY_SUMMARY 집계: 건수=%ld 총액=%ld 반려=%ld 보류=%ld", + h_wcnt, h_wsum, h_wrej, h_whold); + /* 3) 업무규칙 분기부 - 지표 임계 판정 (3분기) */ + h_wrate = (h_wcnt > 0) ? (h_wrej * 10000 / h_wcnt) : 0; + strcpy(h_pval, "2000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_summary_fail_bps'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 2000; + if (h_wcnt == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "대상 지급건 없음", 0L); + } else if (h_wlim > 0 && h_wnet > h_wlim) { + setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "일 지급한도 초과 상태", 0L); + } else if (h_wrate > h_thr) { + setl(b, T_RC, 3); Bchg(b, T_MSG, 0, "실패율이 임계치를 초과", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "정상 범위", 0L); + } + if (h_whold > 0 && h_whold * 2 > h_wcnt) { + userlog("PAY_SUMMARY 보류 과다 경고: 보류=%ld/전체=%ld", h_whold, h_wcnt); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s 건수=%ld 총액=%ld 반려=%ld 실패율=%ldbps 최다=%s", "PAY_SUMMARY", h_wcnt, h_wsum, h_wrej, h_wrate, h_wtop); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_SUMMARY', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계 갱신부 - py_summary upsert */ + if (h_wcnt > 0) { + if (pydb_upsert_summary(h_merchant, h_wdate, h_wcnt, h_wsum) < 0) { + setl(b, T_RC, 60); + Bchg(b, T_MSG, 0, "가맹점 지급집계 갱신 실패", 0L); + FAIL(b); + } + } + /* 8) 응답부 */ + setl(b, T_COUNT, h_wcnt); + setl(b, T_GROSS, h_wsum); + userlog("PAY_SUMMARY 완료: 건수=%ld 총액=%ld 실패율=%ldbps", + h_wcnt, h_wsum, h_wrate); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_SUMMARY", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_URGENT.pgc b/app/src/py/svc/PAY_URGENT.pgc index 273aecf..03305b0 100644 --- a/app/src/py/svc/PAY_URGENT.pgc +++ b/app/src/py/svc/PAY_URGENT.pgc @@ -1,7 +1,9 @@ /* - * PAY_URGENT.pgc - py 모듈 서비스 PAY_URGENT (one service = one file). + * PAY_URGENT.pgc - py (지급) 모듈 온라인 서비스 PAY_URGENT. * 긴급지급: 긴급 수수료(0.5%) 공제 후 잔액 확인 -> 즉시 지급완료(P) 건을 * URGENT 채널로 생성하고 계좌 차감 + py_ledger 에 URGENT 기표까지 원샷 처리. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [mkpay]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_URGENT.h" @@ -9,38 +11,221 @@ void PAY_URGENT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], acct[32], st[8]; - long amount, fee, net, bal = 0; + int gi; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_net; - char h_bizdate[16]; + long h_amount; + char h_bizdate[64]; + char h_key1[64]; + char h_merchant[64]; + long h_wmdr; + char h_wmst[16]; + long h_wlim; + char h_wast[16]; + long h_wbal; + long h_wfee; + long h_wnet; + long h_wpart; + long h_wnum; + long h_wid; + long h_wlast; + long h_wused; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - gets_(b, T_MERCHANT, merch, sizeof(merch)); - gets_(b, T_KEY1, acct, sizeof(acct)); - amount = getl(b, T_AMOUNT); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_amount = getl(b, T_AMOUNT); + if (h_amount < 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "금액 항목은 음수가 될 수 없음", 0L); + FAIL(b); + } gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - if (amount <= 0) { setl(b, T_RC, 3); FAIL(b); } - - fee = acq_fee(amount, 50); /* 긴급지급 수수료 0.5% */ - net = amount - fee; - if (pydb_account_check(acct, st, &bal) < 0) FAIL(b); - if (strcmp(st, "ACTIVE") != 0) { setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "지급 불가 상태 계좌", 0L); FAIL(b); } - if (bal < net) { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "재원 잔액 부족", 0L); FAIL(b); } - - h_pid = pydb_next_payout_id(); - if (h_pid < 0) FAIL(b); - if (pydb_insert_payout(h_pid, merch, acct, amount, fee, net, "P", "URGENT", h_bizdate) < 0) FAIL(b); - if (pydb_account_add(acct, -net) < 0) FAIL(b); - h_net = net; - EXEC SQL INSERT INTO py_ledger (ledger_id, payout_id, entry_type, amount, biz_date) - VALUES (nextval('py_ledger_seq'), :h_pid, 'URGENT', :h_net, :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); - + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + gets_(b, T_KEY1, h_key1, sizeof(h_key1)); + if (h_key1[0] == 0 || strlen(h_key1) < 3) { + setl(b, T_RC, 13); + Bchg(b, T_MSG, 0, "계좌번호/키 값이 유효하지 않음", 0L); + FAIL(b); + } + gets_(b, T_MERCHANT, h_merchant, sizeof(h_merchant)); + if (h_merchant[0] == 0) { + setl(b, T_RC, 14); + Bchg(b, T_MSG, 0, "가맹점 ID는 필수 입력", 0L); + FAIL(b); + } + /* 2) 마스터 조회부 - 가맹점 계약요율/상태/일한도 */ + EXEC SQL SELECT mdr_bps, status, daily_limit INTO :h_wmdr, :h_wmst, :h_wlim + FROM merchant WHERE merchant_id = :h_merchant; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "등록되지 않은 가맹점", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); + FAIL(b); + } + if (strcmp(h_wmst, "ACTIVE") != 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "비활성/폐업 가맹점은 지급 생성 불가", 0L); + FAIL(b); + } + /* 지급계좌 상태/잔액 확인 */ + EXEC SQL SELECT status, balance INTO :h_wast, :h_wbal + FROM py_account WHERE account_no = :h_key1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 43); + Bchg(b, T_MSG, 0, "지급계좌 미등록", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 44); + Bchg(b, T_MSG, 0, "지급계좌 조회 오류", 0L); + FAIL(b); + } + if (strcmp(h_wast, "ACTIVE") != 0) { + setl(b, T_RC, 45); + Bchg(b, T_MSG, 0, "정상 상태 계좌가 아니어서 지급 불가", 0L); + FAIL(b); + } + /* 당일 기 사용액 조회 - 일한도 통제용 */ + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_wused FROM py_payout + WHERE merchant_id = :h_merchant AND biz_date = :h_bizdate + AND status IN ('A','F','P'); + if (sqlca.sqlcode < 0) h_wused = 0; + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + userlog("PAY_URGENT 진입: merch=%s amt=%ld mdr=%ldbps 기사용=%ld", + h_merchant, h_amount, h_wmdr, h_wused); + /* 3) 업무규칙 분기부 - 금액 구간별 수수료 산정 (3분기) */ + if (h_amount < 100000) { + h_wfee = acq_fee(h_amount, 50); + } else if (h_amount < 10000000) { + h_wfee = acq_fee(h_amount, 50) + acq_vat(acq_fee(h_amount, 50)); + } else { + h_wfee = acq_fee(h_amount, 50 + 20) + acq_vat(acq_fee(h_amount, 50)); + } + if (h_wfee < 0) h_wfee = 0; + h_wnet = h_amount - h_wfee; + if (h_wnet < 0) h_wnet = 0; + /* 일한도 통제 */ + if (h_wlim > 0 && h_wused + h_wnet > h_wlim) { + setl(b, T_RC, 8); + Bchg(b, T_MSG, 0, "가맹점 일 지급한도 초과", 0L); + FAIL(b); + } + strcpy(h_pval, "500000000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_urgent_single_max'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 500000000; + if (h_amount > h_thr) { + setl(b, T_RC, 9); + Bchg(b, T_MSG, 0, "1건 지급 상한 정책 초과", 0L); + FAIL(b); + } + h_wnum = 1; + h_wpart = h_amount; + /* 긴급지급은 즉시 출금이므로 잔액 선확인 */ + if (h_wbal < h_wnet) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "긴급지급 계좌 잔액 부족", 0L); + FAIL(b); + } + /* 4) 본처리부 - 지급건 생성 (분할 시 반복 채번) */ + h_wlast = 0; + for (gi = 0; gi < (int)h_wnum; gi++) { + EXEC SQL SELECT nextval('py_payout_seq') INTO :h_wid; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); Bchg(b, T_MSG, 0, "지급 일련번호 채번 실패", 0L); FAIL(b); + } + EXEC SQL INSERT INTO py_payout + (payout_id, merchant_id, account_no, amount, fee, net, + status, channel, biz_date) + VALUES (:h_wid, :h_merchant, :h_key1, :h_wpart, :h_wfee, + :h_wpart, 'P', 'URGENT', :h_bizdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 51); Bchg(b, T_MSG, 0, "지급건 생성 INSERT 실패", 0L); FAIL(b); + } + h_wlast = h_wid; + } + if (pydb_account_add(h_key1, -h_wnet) < 0) { + setl(b, T_RC, 52); + Bchg(b, T_MSG, 0, "긴급지급 계좌 차감 실패", 0L); + FAIL(b); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s merch=%s 생성=%ld건 총액=%ld 수수료=%ld", "PAY_URGENT", h_merchant, h_wnum, h_amount, h_wfee); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_URGENT', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계 갱신부 - 가맹점 일별 지급 집계 upsert */ + if (pydb_upsert_summary(h_merchant, h_bizdate, h_wnum, h_amount) < 0) { + setl(b, T_RC, 60); + Bchg(b, T_MSG, 0, "가맹점 지급집계(py_summary) 갱신 실패", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 당일 채널별 지급 생성 누계 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_merchant + AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "당일 지급 생성 누계 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE merchant_id = :h_merchant AND biz_date = :h_bizdate + AND status = 'R'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 요청 잔량 누적: 기준=%ld 실적=%ld", "PAY_URGENT", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - setl(b, T_ID1, h_pid); - setl(b, T_FEE, fee); - setl(b, T_NET, net); - userlog("PAY_URGENT id=%ld merch=%s net=%ld", h_pid, merch, net); + setl(b, T_ID1, h_wlast); + setl(b, T_FEE, h_wfee); + setl(b, T_NET, h_wnet); + Bchg(b, T_MSG, 0, "지급건 생성 완료", 0L); + userlog("PAY_URGENT 완료: last=%ld 건수=%ld 순액=%ld", h_wlast, h_wnum, h_wnet); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_URGENT", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_URGENTQ.pgc b/app/src/py/svc/PAY_URGENTQ.pgc index 241c180..afad79c 100644 --- a/app/src/py/svc/PAY_URGENTQ.pgc +++ b/app/src/py/svc/PAY_URGENTQ.pgc @@ -1,7 +1,9 @@ /* - * PAY_URGENTQ.pgc - py 모듈 서비스 PAY_URGENTQ (one service = one file). + * PAY_URGENTQ.pgc - py (지급) 모듈 온라인 서비스 PAY_URGENTQ. * 긴급 지급 큐: 당일 요청(R) 건 중 금액이 임계치(T_AMOUNT) 이상인 건을 금액 * 내림차순 커서로 뽑아 긴급 처리 후보 목록을 occurrence 로 회신한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [scalar]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_URGENTQ.h" @@ -9,36 +11,130 @@ void PAY_URGENTQ(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - int i = 0; - long hits = 0, thr; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_thr, h_pid, h_amt; + long h_amount; + char h_bizdate[64]; + long h_wcnt; + long h_wsum; + long h_wfee; + long h_wnet; + long h_wrej; + long h_wlim; + long h_wrate; + long h_wpaid; + long h_whold; + char h_wtop[64]; + long h_wdays; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - thr = getl(b, T_AMOUNT); - h_thr = (thr > 0) ? thr : 1000000; - - EXEC SQL DECLARE cur_urgentq CURSOR FOR - SELECT payout_id, amount FROM py_payout - WHERE biz_date = :h_bizdate AND status = 'R' AND amount >= :h_thr - ORDER BY amount DESC; - EXEC SQL OPEN cur_urgentq; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH cur_urgentq INTO :h_pid, :h_amt; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_urgentq; FAIL(b); } - hits++; - if (i < 12) { - Bchg(b, T_ARG1, i, (char *)&h_pid, 0L); - Bchg(b, T_AMT1, i, (char *)&h_amt, 0L); - i++; - } + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_amount = getl(b, T_AMOUNT); + if (h_amount < 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "금액 항목은 음수가 될 수 없음", 0L); + FAIL(b); + } + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + /* 2) 집계 조회부 - 건수/총액/수수료/순액 스칼라 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) + INTO :h_wcnt, :h_wsum, :h_wfee, :h_wnet + FROM py_payout WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "지급 집계 조회 오류", 0L); + FAIL(b); + } + /* 상태별 부가 지표 - 반려/지급완료/보류 */ + EXEC SQL SELECT + count(*) FILTER (WHERE status = 'J'), + count(*) FILTER (WHERE status = 'P'), + count(*) FILTER (WHERE status = 'H') + INTO :h_wrej, :h_wpaid, :h_whold + FROM py_payout WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { h_wrej = 0; h_wpaid = 0; h_whold = 0; } + EXEC SQL SELECT coalesce(max(daily_limit),0) INTO :h_wlim FROM merchant; + if (sqlca.sqlcode < 0) h_wlim = 0; + /* 최다 실적 가맹점 1건 추출 */ + EXEC SQL SELECT coalesce(merchant_id,'') INTO :h_wtop + FROM py_payout WHERE biz_date = :h_bizdate + GROUP BY merchant_id ORDER BY count(*) DESC LIMIT 1; + if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) strcpy(h_wtop, ""); + h_wdays = 30; + userlog("PAY_URGENTQ 집계: 건수=%ld 총액=%ld 반려=%ld 보류=%ld", + h_wcnt, h_wsum, h_wrej, h_whold); + /* 3) 업무규칙 분기부 - 지표 임계 판정 (3분기) */ + h_wrate = (h_wcnt > 0) ? (h_wrej * 10000 / h_wcnt) : 0; + strcpy(h_pval, "2000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_urgentq_fail_bps'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 2000; + if (h_wcnt == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "대상 지급건 없음", 0L); + } else if (h_wlim > 0 && h_wnet > h_wlim) { + setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "일 지급한도 초과 상태", 0L); + } else if (h_wrate > h_thr) { + setl(b, T_RC, 3); Bchg(b, T_MSG, 0, "실패율이 임계치를 초과", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "정상 범위", 0L); + } + if (h_whold > 0 && h_whold * 2 > h_wcnt) { + userlog("PAY_URGENTQ 보류 과다 경고: 보류=%ld/전체=%ld", h_whold, h_wcnt); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s 건수=%ld 총액=%ld 반려=%ld 실패율=%ldbps 최다=%s", "PAY_URGENTQ", h_wcnt, h_wsum, h_wrej, h_wrate, h_wtop); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_URGENTQ', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계 재산정부 - 당일 지급완료 총액 재확인 */ + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_wpaid FROM py_payout + WHERE biz_date = :h_wdate AND status = 'P'; + if (sqlca.sqlcode < 0) h_wpaid = 0; + /* 8) 응답부 */ + setl(b, T_COUNT, h_wcnt); + setl(b, T_AMT1, h_wsum); + setl(b, T_ARG1, h_wrej); + userlog("PAY_URGENTQ 완료: 건수=%ld 총액=%ld 실패율=%ldbps", + h_wcnt, h_wsum, h_wrate); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_URGENTQ", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); } - EXEC SQL CLOSE cur_urgentq; - setl(b, T_COUNT, hits); - setl(b, T_RC, 0); OK(b); } diff --git a/app/src/py/svc/PAY_VOIDONE.pgc b/app/src/py/svc/PAY_VOIDONE.pgc index 1ed3830..4fb5866 100644 --- a/app/src/py/svc/PAY_VOIDONE.pgc +++ b/app/src/py/svc/PAY_VOIDONE.pgc @@ -1,7 +1,9 @@ /* - * PAY_VOIDONE.pgc - py 모듈 서비스 PAY_VOIDONE (one service = one file). + * PAY_VOIDONE.pgc - py (지급) 모듈 온라인 서비스 PAY_VOIDONE. * 지급 무효(단건): 아직 지급 전인 요청/승인(R/A) 건만 무효 취소(C)로 전이하고 * 무효 사유를 남긴다. 파일편성/지급완료 이후 건은 무효 대상이 아니다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [txn]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_VOIDONE.h" @@ -9,25 +11,155 @@ void PAY_VOIDONE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long voided; EXEC SQL BEGIN DECLARE SECTION; - long h_pid; - char h_rsn[128]; + long h_id1; + char h_reason[64]; + char h_wst[8]; + long h_wamt; + long h_wnet; + char h_wchan[16]; + long h_wretry; + long h_waff; + char h_wmerch[64]; + char h_wacct[64]; + long h_wcnt; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_ID1); - gets_(b, T_REASON, h_rsn, sizeof(h_rsn)); - if (h_rsn[0] == 0) strcpy(h_rsn, "무효처리"); - - EXEC SQL UPDATE py_payout SET status = 'C', reject_reason = :h_rsn, updated_at = now() - WHERE payout_id = :h_pid AND status IN ('R','A'); - if (sqlca.sqlcode < 0) FAIL(b); - voided = sqlca.sqlerrd[2]; - if (voided == 0) { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "무효 대상 아님(지급 전 건만)", 0L); FAIL(b); } - + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + gets_(b, T_REASON, h_reason, sizeof(h_reason)); + if (h_reason[0] == 0) strncpy(h_reason, "사유미기재", sizeof(h_reason)-1); + /* 2) 마스터 조회부 - 지급건 현재 상태/금액/채널/재시도 이력 */ + EXEC SQL SELECT status, amount, net, channel, retry_count, merchant_id, account_no + INTO :h_wst, :h_wamt, :h_wnet, :h_wchan, :h_wretry, :h_wmerch, :h_wacct + FROM py_payout WHERE payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 지급건이 존재하지 않음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급건 조회 중 DB 오류", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + userlog("PAY_VOIDONE 진입: payout=%ld status=%s amt=%ld chan=%s", + h_id1, h_wst, h_wamt, h_wchan); + /* 3) 업무규칙 분기부 - 상태 전이 가능성/한도/재시도 판정 */ + if (strcmp(h_wst, "P") == 0 || strcmp(h_wst, "X") == 0) { + setl(b, T_RC, 2); Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "이미 종결(지급완료/마감)된 건은 단건 무효 불가", 0L); FAIL(b); + } + if (strcmp(h_wst, "R") != 0) { + setl(b, T_RC, 3); Bchg(b, T_STATUS, 0, h_wst, 0L); + Bchg(b, T_MSG, 0, "현재 상태에서는 단건 무효 전이가 허용되지 않음", 0L); FAIL(b); + } + if (strcmp(h_wchan, "ADVANCE") == 0 && h_wnet > 50000000) { + userlog("PAY_VOIDONE 고액 선지급 건 주의: payout=%ld net=%ld", h_id1, h_wnet); + } + strcpy(h_pval, "100000000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_voidone_amt_max'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 100000000; + if (h_wamt > h_thr) { + userlog("PAY_VOIDONE 정책 상한 초과: amt=%ld thr=%ld", h_wamt, h_thr); + } + /* 4) 본처리부 - 가드 조건부 상태 전이 UPDATE */ + EXEC SQL UPDATE py_payout SET status = 'C', updated_at = now() + WHERE payout_id = :h_id1 AND status = 'R'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "단건 무효 상태 갱신 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + if (h_waff == 0) { + setl(b, T_RC, 4); + Bchg(b, T_MSG, 0, "동시성 충돌 - 전이 대상 건이 없음", 0L); + FAIL(b); + } + EXEC SQL UPDATE py_payout SET reject_reason = :h_reason, updated_at = now() + WHERE payout_id = :h_id1; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 51); + Bchg(b, T_MSG, 0, "처리 사유 기록 실패", 0L); + FAIL(b); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s payout=%ld %s->%s amt=%ld aff=%ld", "PAY_VOIDONE", h_id1, "R", "C", h_wamt, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_VOIDONE', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계 재산정부 - 당일 동일 상태 건수 재집계 */ + EXEC SQL SELECT count(*) INTO :h_wcnt FROM py_payout + WHERE biz_date = :h_wdate AND status = 'C'; + if (sqlca.sqlcode < 0) h_wcnt = 0; + /* 6) 집계/요약 갱신부 - 가맹점 당일 확정성 지급 실적 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wa1, :h_wa2 + FROM py_payout WHERE merchant_id = :h_wmerch + AND biz_date = :h_wdate AND status IN ('A','F','P'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "가맹점 지급 실적 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE merchant_id = :h_wmerch AND biz_date = :h_wdate + AND status IN ('R','H'); + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 미지급 잔량 과다: 기준=%ld 실적=%ld", "PAY_VOIDONE", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); Bchg(b, T_STATUS, 0, "C", 0L); - Bchg(b, T_REASON, 0, h_rsn, 0L); - userlog("PAY_VOIDONE payout=%ld reason=%s", h_pid, h_rsn); + Bchg(b, T_REASON, 0, h_reason, 0L); + Bchg(b, T_MSG, 0, "단건 무효 정상 처리", 0L); + userlog("PAY_VOIDONE 완료: payout=%ld -> %s (당일 %s상태 %ld건)", + h_id1, "C", "C", h_wcnt); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_VOIDONE", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_XFERCONF.pgc b/app/src/py/svc/PAY_XFERCONF.pgc index 189a1bf..3a8d211 100644 --- a/app/src/py/svc/PAY_XFERCONF.pgc +++ b/app/src/py/svc/PAY_XFERCONF.pgc @@ -1,7 +1,9 @@ /* - * PAY_XFERCONF.pgc - py 모듈 서비스 PAY_XFERCONF (one service = one file). + * PAY_XFERCONF.pgc - py (지급) 모듈 온라인 서비스 PAY_XFERCONF. * 이체 확정: 지급완료(P) 건에 대해 이체 실행 결과를 py_ledger 에 XFER 기표로 * 확정한다. 이미 XFER 기표가 있으면 중복 확정을 막고 사유코드로 거절한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [xfer]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_XFERCONF.h" @@ -10,28 +12,132 @@ void PAY_XFERCONF(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_net, h_exist; - char h_st[4], h_bizdate[16]; + long h_id1; + char h_wst[8]; + long h_wnet; + long h_wxfer; + long h_wdiff; + long h_wlid; + long h_wamt; + long h_wcnt; + long h_wsum; + char h_wmerch[64]; + char h_wdate[16]; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_ID1); - EXEC SQL SELECT net, status, CAST(biz_date AS text) INTO :h_net, :h_st, :h_bizdate - FROM py_payout WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - if (h_st[0] != 'P') { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "지급완료 건만 이체확정 가능", 0L); FAIL(b); } - - EXEC SQL SELECT count(*) INTO :h_exist - FROM py_ledger WHERE payout_id = :h_pid AND entry_type = 'XFER'; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_exist > 0) { setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "이미 이체확정된 건", 0L); FAIL(b); } - - EXEC SQL INSERT INTO py_ledger (ledger_id, payout_id, entry_type, amount, biz_date) - VALUES (nextval('py_ledger_seq'), :h_pid, 'XFER', :h_net, :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); - + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + /* 2) 마스터 조회부 - 지급건 순액 + XFER 기표 누계 */ + EXEC SQL SELECT status, net, merchant_id + INTO :h_wst, :h_wnet, :h_wmerch + FROM py_payout WHERE payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 지급건 없음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급건 조회 오류", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_wcnt, :h_wxfer + FROM py_ledger WHERE payout_id = :h_id1 AND entry_type = 'XFER'; + if (sqlca.sqlcode < 0) { h_wcnt = 0; h_wxfer = 0; } + h_wdiff = h_wnet - h_wxfer; + userlog("PAY_XFERCONF 진입: payout=%ld net=%ld 기표=%ld 차액=%ld", + h_id1, h_wnet, h_wxfer, h_wdiff); + /* 3) 업무규칙 분기부 - 이체 확정 가능 여부 (3분기) */ + if (strcmp(h_wst, "P") != 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "지급완료(P) 건만 이체 확정 대상", 0L); + FAIL(b); + } else if (h_wxfer >= h_wnet) { + setl(b, T_RC, 43); + Bchg(b, T_MSG, 0, "이미 전액 이체 기표 완료", 0L); + FAIL(b); + } else if (h_wdiff <= 0) { + setl(b, T_RC, 44); + Bchg(b, T_MSG, 0, "확정할 잔여 기표 금액이 없음", 0L); + FAIL(b); + } + h_wamt = h_wdiff; + /* 4) 본처리부 - 잔여 금액 XFER 확정 기표 */ + EXEC SQL SELECT nextval('py_ledger_seq') INTO :h_wlid; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "원장 채번 실패", 0L); + FAIL(b); + } + EXEC SQL INSERT INTO py_ledger + (ledger_id, payout_id, entry_type, amount, biz_date) + VALUES (:h_wlid, :h_id1, 'XFER', :h_wamt, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 51); + Bchg(b, T_MSG, 0, "XFER 확정 기표 실패", 0L); + FAIL(b); + } + h_wdiff = 0; + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s 대상=%ld 금액=%ld 차액=%ld", "PAY_XFERCONF", h_wcnt, h_wamt, h_wdiff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_XFERCONF', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 당일 이체 기표 총계 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_wa1, :h_wa2 + FROM py_ledger WHERE biz_date = :h_wdate AND entry_type = 'XFER'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "이체 기표 총계 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE biz_date = :h_wdate AND status = 'P'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 지급완료 대비 기표 부족: 기준=%ld 실적=%ld", "PAY_XFERCONF", h_wa1, h_wa3); + } + /* 8) 응답부 */ setl(b, T_RC, 0); - setl(b, T_NET, h_net); - Bchg(b, T_MSG, 0, "이체 확정 완료", 0L); - userlog("PAY_XFERCONF payout=%ld net=%ld", h_pid, h_net); + setl(b, T_NET, h_wnet); + Bchg(b, T_MSG, 0, "이체 원장 기표 완료", 0L); + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + userlog("PAY_XFERCONF 완료: 금액=%ld 차액=%ld", h_wamt, h_wdiff); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_XFERCONF", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_XFERDIFF.pgc b/app/src/py/svc/PAY_XFERDIFF.pgc index 72220dc..3054cc7 100644 --- a/app/src/py/svc/PAY_XFERDIFF.pgc +++ b/app/src/py/svc/PAY_XFERDIFF.pgc @@ -1,7 +1,9 @@ /* - * PAY_XFERDIFF.pgc - py 모듈 서비스 PAY_XFERDIFF (one service = one file). + * PAY_XFERDIFF.pgc - py (지급) 모듈 온라인 서비스 PAY_XFERDIFF. * 이체 차액 목록: 당일 지급건의 net 과 py_ledger XFER 기표 합이 어긋나는 * 건을 커서로 뽑아 지급ID/net/이체합/차액을 occurrence 로 회신한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [cursor]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_XFERDIFF.h" @@ -9,38 +11,126 @@ void PAY_XFERDIFF(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - int i = 0; - long diffs = 0; + int gi = 0; + long tot_cnt = 0, tot_sum = 0, tot_fee = 0, top_cnt = 0, top_sum = 0; + char top_grp[64]; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_pid, h_net, h_xfer; + char h_bizdate[64]; + char h_wgrp[64]; + long h_wgcnt; + long h_wgsum; + long h_wgfee; + long h_wtot; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL DECLARE cur_xferdiff CURSOR FOR - SELECT p.payout_id, p.net, - coalesce((SELECT sum(amount) FROM py_ledger l - WHERE l.payout_id = p.payout_id AND l.entry_type = 'XFER'), 0) - FROM py_payout p WHERE p.biz_date = :h_bizdate AND p.status = 'P' - ORDER BY p.payout_id; - EXEC SQL OPEN cur_xferdiff; - if (sqlca.sqlcode < 0) FAIL(b); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + top_grp[0] = 0; + /* 2) 사전 규모 확인 - 커서 오픈 전 총건수 파악 */ + EXEC SQL SELECT count(*) INTO :h_wtot FROM py_payout p WHERE p.biz_date = :h_bizdate AND p.status = 'P'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "집계 사전 조회 실패", 0L); + FAIL(b); + } + userlog("PAY_XFERDIFF 집계 시작: 기준일=%s 대상=%ld건 (이체차액별)", h_wdate, h_wtot); + /* 커서 정의: 이체차액별 건수/금액 집계 */ + EXEC SQL DECLARE cur_pay_xferdiff CURSOR FOR + SELECT p.merchant_id, count(*), coalesce(sum(p.net),0), coalesce(sum(p.fee),0) + FROM py_payout p WHERE p.biz_date = :h_bizdate AND p.status = 'P' + GROUP BY p.merchant_id ORDER BY 2 DESC, 1; + EXEC SQL OPEN cur_pay_xferdiff; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "집계 커서 OPEN 실패", 0L); + FAIL(b); + } + /* 3) 본처리부 - 커서 순회하며 그룹별 집계/상위 추출 */ for (;;) { - EXEC SQL FETCH cur_xferdiff INTO :h_pid, :h_net, :h_xfer; + EXEC SQL FETCH cur_pay_xferdiff INTO :h_wgrp, :h_wgcnt, :h_wgsum, :h_wgfee; if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_xferdiff; FAIL(b); } - if (h_net == h_xfer) continue; - diffs++; - if (i < 12) { - long d = h_net - h_xfer; - Bchg(b, T_ARG1, i, (char *)&h_pid, 0L); - Bchg(b, T_AMT1, i, (char *)&h_net, 0L); - Bchg(b, T_AMT2, i, (char *)&d, 0L); - i++; + if (sqlca.sqlcode < 0) { + EXEC SQL CLOSE cur_pay_xferdiff; + setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "집계 FETCH 오류", 0L); FAIL(b); + } + tot_cnt += h_wgcnt; + tot_sum += h_wgsum; + tot_fee += h_wgfee; + if (h_wgcnt > top_cnt) { + top_cnt = h_wgcnt; + top_sum = h_wgsum; + strncpy(top_grp, h_wgrp, sizeof(top_grp)-1); + top_grp[sizeof(top_grp)-1] = 0; + } + if (gi < 10) { + Bchg(b, T_STR1, gi, h_wgrp, 0L); + Bchg(b, T_AMT1, gi, (char *)&h_wgsum, 0L); + Bchg(b, T_ARG1, gi, (char *)&h_wgcnt, 0L); + gi++; } } - EXEC SQL CLOSE cur_xferdiff; - setl(b, T_COUNT, diffs); - setl(b, T_RC, diffs > 0 ? 1 : 0); + EXEC SQL CLOSE cur_pay_xferdiff; + strcpy(h_pval, "5000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_xferdiff_concentration'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 5000; + /* 업무규칙 분기부 - 분포 집중도 판정 (3분기) */ + if (gi == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "이체차액별 집계 대상 없음", 0L); + } else if (tot_cnt > 0 && top_cnt * 10000 / tot_cnt > h_thr) { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "특정 이체차액 집중 - 확인 필요", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "이체차액별 정상 분포", 0L); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s %s별 그룹=%d 건수=%ld 금액=%ld 최다=%s", "PAY_XFERDIFF", "이체차액", gi, tot_cnt, tot_sum, top_grp); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_XFERDIFF', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 8) 응답부 */ + setl(b, T_COUNT, tot_cnt); + setl(b, T_AMT2, top_sum); + userlog("PAY_XFERDIFF 완료: 그룹=%d 총건=%ld 총액=%ld 최다=%s", + gi, tot_cnt, tot_sum, top_grp); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_XFERDIFF", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_XFERFILE.pgc b/app/src/py/svc/PAY_XFERFILE.pgc index 6f68e4a..f7b867d 100644 --- a/app/src/py/svc/PAY_XFERFILE.pgc +++ b/app/src/py/svc/PAY_XFERFILE.pgc @@ -1,7 +1,9 @@ /* - * PAY_XFERFILE.pgc - py 모듈 서비스 PAY_XFERFILE (one service = one file). + * PAY_XFERFILE.pgc - py (지급) 모듈 온라인 서비스 PAY_XFERFILE. * 이체유형별 지급파일 생성: 지정 채널(STD/SPLIT/BULK/ADVANCE)의 승인(A) 건만 * 모아 집계 -> py_file 생성 -> 대상 건을 파일편성(F)+file_id 로 일괄 전이한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [file]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_XFERFILE.h" @@ -9,37 +11,154 @@ void PAY_XFERFILE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char fname[64]; EXEC SQL BEGIN DECLARE SECTION; - char h_chan[16], h_bizdate[16], h_fname[64]; - long h_fid, h_cnt, h_sum; + char h_bizdate[64]; + char h_channel[64]; + char h_wfst[8]; + long h_whcnt; + long h_whamt; + long h_wrcnt; + long h_wramt; + long h_waff; + long h_wnew; + char h_wfname[64]; + char h_wfdate[16]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - gets_(b, T_CHANNEL, h_chan, sizeof(h_chan)); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - if (h_chan[0] == 0) strcpy(h_chan, "STD"); - - EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_cnt, :h_sum - FROM py_payout WHERE biz_date = :h_bizdate AND status = 'A' AND channel = :h_chan; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_cnt == 0) { setl(b, T_RC, 1); setl(b, T_COUNT, 0); OK(b); } - - EXEC SQL SELECT nextval('py_file_seq') INTO :h_fid; - if (sqlca.sqlcode < 0) FAIL(b); - snprintf(fname, sizeof(fname), "XFER_%s_%s.dat", h_chan, h_bizdate); - strncpy(h_fname, fname, sizeof(h_fname)-1); h_fname[sizeof(h_fname)-1] = 0; - EXEC SQL INSERT INTO py_file (file_id, biz_date, file_name, record_count, total_amount, status) - VALUES (:h_fid, :h_bizdate, :h_fname, :h_cnt, :h_sum, 'G'); - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL UPDATE py_payout SET status = 'F', file_id = :h_fid, updated_at = now() - WHERE biz_date = :h_bizdate AND status = 'A' AND channel = :h_chan; - if (sqlca.sqlcode < 0) FAIL(b); - - setl(b, T_RC, 0); - setl(b, T_ID2, h_fid); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_sum); - Bchg(b, T_STR1, 0, fname, 0L); - userlog("PAY_XFERFILE chan=%s file=%ld recs=%ld amt=%ld", h_chan, h_fid, h_cnt, h_sum); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + gets_(b, T_CHANNEL, h_channel, sizeof(h_channel)); + if (h_channel[0] == 0) strncpy(h_channel, "STD", sizeof(h_channel)-1); + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + /* 2) 마스터 조회부 - 당일 승인(A) 편성 대상 집계 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) + INTO :h_wrcnt, :h_wramt + FROM py_payout WHERE biz_date = :h_bizdate + AND status = 'A' AND channel = :h_channel; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "편성 대상 집계 오류", 0L); + FAIL(b); + } + h_whcnt = h_wrcnt; h_whamt = h_wramt; + strcpy(h_wfst, "G"); + strncpy(h_wfdate, h_bizdate, sizeof(h_wfdate)-1); + h_wfdate[sizeof(h_wfdate)-1] = 0; + userlog("PAY_XFERFILE 진입: 기준일=%s 편성대상=%ld건 %ld원", + h_bizdate, h_wrcnt, h_wramt); + /* 3) 업무규칙 분기부 - 파일 상태/정합 판정 (3분기) */ + if (h_wrcnt == 0) { + setl(b, T_RC, 43); + Bchg(b, T_MSG, 0, "편성 대상 승인건이 없음", 0L); + FAIL(b); + } else if (h_wramt <= 0) { + setl(b, T_RC, 44); + Bchg(b, T_MSG, 0, "편성 총액이 비정상", 0L); + FAIL(b); + } + strcpy(h_pval, "100000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_xferfile_max_rec'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 100000; + if (h_wrcnt > h_thr) { + setl(b, T_RC, 29); + Bchg(b, T_MSG, 0, "파일 최대 레코드 수 초과 - 분할 편성 필요", 0L); + FAIL(b); + } + /* 4) 본처리부 - 파일 채번/헤더 생성 후 대상건 편성 */ + EXEC SQL SELECT nextval('py_file_seq') INTO :h_wnew; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "지급파일 채번 실패", 0L); + FAIL(b); + } + snprintf(h_wfname, sizeof(h_wfname), "XFER_%.10s_%ld.dat", + h_bizdate, h_wnew); + EXEC SQL INSERT INTO py_file + (file_id, biz_date, file_name, record_count, total_amount, status) + VALUES (:h_wnew, :h_bizdate, :h_wfname, :h_wrcnt, :h_wramt, 'G'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 51); + Bchg(b, T_MSG, 0, "지급파일 헤더 생성 실패", 0L); + FAIL(b); + } + EXEC SQL UPDATE py_payout SET file_id = :h_wnew, status = 'F', + updated_at = now() + WHERE biz_date = :h_bizdate AND status = 'A' + AND channel = :h_channel; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 52); + Bchg(b, T_MSG, 0, "지급건 파일 편성 실패", 0L); + FAIL(b); + } + h_waff = sqlca.sqlerrd[2]; + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s file=%ld 상세=%ld건 %ld원 반영=%ld", "PAY_XFERFILE", h_wnew, h_wrcnt, h_wramt, h_waff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_XFERFILE', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 당일 지급파일 전체 현황 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(total_amount),0) INTO :h_wa1, :h_wa2 + FROM py_file WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "당일 파일 현황 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE biz_date = :h_wdate AND status = 'F'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 편성 건수 초과: 기준=%ld 실적=%ld", "PAY_XFERFILE", h_wa1, h_wa3); + } + /* 8) 응답부 */ + setl(b, T_ID2, h_wnew); + setl(b, T_COUNT, h_wrcnt); + setl(b, T_GROSS, h_wramt); + Bchg(b, T_STR1, 0, h_wfname, 0L); + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + userlog("PAY_XFERFILE 완료: file=%ld 상세=%ld건 %ld원", h_wnew, h_wrcnt, h_wramt); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_XFERFILE", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_XFERLINK.pgc b/app/src/py/svc/PAY_XFERLINK.pgc index b1534de..550fd4e 100644 --- a/app/src/py/svc/PAY_XFERLINK.pgc +++ b/app/src/py/svc/PAY_XFERLINK.pgc @@ -1,24 +1,154 @@ /* - * PAY_XFERLINK.pgc - py 모듈 서비스 PAY_XFERLINK (one service = one file). + * PAY_XFERLINK.pgc - py (지급) 모듈 온라인 서비스 PAY_XFERLINK. * 이체 연계: 지급건에 대한 XFER 원장 엔트리를 py_ledger 에 기록. - * Moved verbatim from the former inline py_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by py_svr at tpsvrinit. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [xfer]. + * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_XFERLINK.h" void PAY_XFERLINK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; EXEC SQL BEGIN DECLARE SECTION; - long h_pay, h_amt; - char h_bizdate[16]; + long h_amount; + char h_bizdate[64]; + long h_id1; + char h_wst[8]; + long h_wnet; + long h_wxfer; + long h_wdiff; + long h_wlid; + long h_wamt; + long h_wcnt; + long h_wsum; + char h_wmerch[64]; + char h_wdate[16]; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pay = getl(b, T_ID1); h_amt = getl(b, T_AMOUNT); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL INSERT INTO py_ledger (ledger_id, payout_id, entry_type, amount, biz_date) - VALUES (nextval('py_ledger_seq'), :h_pay, 'XFER', :h_amt, :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); + + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_amount = getl(b, T_AMOUNT); + if (h_amount < 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "금액 항목은 음수가 될 수 없음", 0L); + FAIL(b); + } + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 12); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 13); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + /* 2) 마스터 조회부 - 지급건 순액 + XFER 기표 누계 */ + EXEC SQL SELECT status, net, merchant_id + INTO :h_wst, :h_wnet, :h_wmerch + FROM py_payout WHERE payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 지급건 없음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급건 조회 오류", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_wcnt, :h_wxfer + FROM py_ledger WHERE payout_id = :h_id1 AND entry_type = 'XFER'; + if (sqlca.sqlcode < 0) { h_wcnt = 0; h_wxfer = 0; } + h_wdiff = h_wnet - h_wxfer; + userlog("PAY_XFERLINK 진입: payout=%ld net=%ld 기표=%ld 차액=%ld", + h_id1, h_wnet, h_wxfer, h_wdiff); + /* 3) 업무규칙 분기부 - 이체 연계 적법성 (3분기) */ + if (strcmp(h_wst, "P") != 0 && strcmp(h_wst, "F") != 0) { + setl(b, T_RC, 42); + Bchg(b, T_MSG, 0, "지급완료(P)/편성(F) 건만 이체 연계 가능", 0L); + FAIL(b); + } else if (h_amount <= 0) { + setl(b, T_RC, 43); + Bchg(b, T_MSG, 0, "이체 기표 금액은 1 이상이어야 함", 0L); + FAIL(b); + } else if (h_amount > h_wdiff) { + setl(b, T_RC, 44); + Bchg(b, T_MSG, 0, "기표 금액이 미기표 잔여 순액을 초과", 0L); + FAIL(b); + } + h_wamt = h_amount; + /* 4) 본처리부 - py_ledger XFER 기표 */ + EXEC SQL SELECT nextval('py_ledger_seq') INTO :h_wlid; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 50); + Bchg(b, T_MSG, 0, "원장 일련번호 채번 실패", 0L); + FAIL(b); + } + EXEC SQL INSERT INTO py_ledger + (ledger_id, payout_id, entry_type, amount, biz_date) + VALUES (:h_wlid, :h_id1, 'XFER', :h_wamt, :h_bizdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 51); + Bchg(b, T_MSG, 0, "XFER 원장 기표 실패", 0L); + FAIL(b); + } + h_wdiff = h_wdiff - h_wamt; + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s 대상=%ld 금액=%ld 차액=%ld", "PAY_XFERLINK", h_wcnt, h_wamt, h_wdiff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_XFERLINK', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 당일 이체 기표 총계 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_wa1, :h_wa2 + FROM py_ledger WHERE biz_date = :h_wdate AND entry_type = 'XFER'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "이체 기표 총계 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE biz_date = :h_wdate AND status = 'P'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 지급완료 대비 기표 부족: 기준=%ld 실적=%ld", "PAY_XFERLINK", h_wa1, h_wa3); + } + /* 8) 응답부 */ + setl(b, T_RC, 0); + Bchg(b, T_MSG, 0, "이체 원장 기표 완료", 0L); + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + userlog("PAY_XFERLINK 완료: 금액=%ld 차액=%ld", h_wamt, h_wdiff); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_XFERLINK", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_XFERMATCH.pgc b/app/src/py/svc/PAY_XFERMATCH.pgc index be0b4d7..d00d9f2 100644 --- a/app/src/py/svc/PAY_XFERMATCH.pgc +++ b/app/src/py/svc/PAY_XFERMATCH.pgc @@ -1,7 +1,9 @@ /* - * PAY_XFERMATCH.pgc - py 모듈 서비스 PAY_XFERMATCH (one service = one file). + * PAY_XFERMATCH.pgc - py (지급) 모듈 온라인 서비스 PAY_XFERMATCH. * 이체결과 대사(단건): 지급건 net 과 py_ledger 의 XFER 기표 합계를 비교해 * 일치/불일치와 차액을 회신한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [xfer]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_XFERMATCH.h" @@ -10,26 +12,124 @@ void PAY_XFERMATCH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_net, h_led; + long h_id1; + char h_wst[8]; + long h_wnet; + long h_wxfer; + long h_wdiff; + long h_wlid; + long h_wamt; + long h_wcnt; + long h_wsum; + char h_wmerch[64]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_ID1); - EXEC SQL SELECT net INTO :h_net FROM py_payout WHERE payout_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_led - FROM py_ledger WHERE payout_id = :h_pid AND entry_type = 'XFER'; - if (sqlca.sqlcode < 0) FAIL(b); - - setl(b, T_NET, h_net); - setl(b, T_AMT1, h_led); - setl(b, T_DELTA, h_net - h_led); - if (h_net == h_led) { - setl(b, T_RC, 0); - Bchg(b, T_MSG, 0, "이체결과 대사 일치", 0L); + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ + h_id1 = getl(b, T_ID1); + if (h_id1 <= 0) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "식별자(ID)는 1 이상이어야 함", 0L); + FAIL(b); + } + EXEC SQL SELECT to_char(current_date, 'YYYY-MM-DD') INTO :h_wdate; + if (sqlca.sqlcode < 0) strcpy(h_wdate, "1970-01-01"); + /* 2) 마스터 조회부 - 지급건 순액 + XFER 기표 누계 */ + EXEC SQL SELECT status, net, merchant_id + INTO :h_wst, :h_wnet, :h_wmerch + FROM py_payout WHERE payout_id = :h_id1; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "대상 지급건 없음", 0L); + FAIL(b); + } + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "지급건 조회 오류", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) + INTO :h_wcnt, :h_wxfer + FROM py_ledger WHERE payout_id = :h_id1 AND entry_type = 'XFER'; + if (sqlca.sqlcode < 0) { h_wcnt = 0; h_wxfer = 0; } + h_wdiff = h_wnet - h_wxfer; + userlog("PAY_XFERMATCH 진입: payout=%ld net=%ld 기표=%ld 차액=%ld", + h_id1, h_wnet, h_wxfer, h_wdiff); + strcpy(h_pval, "0"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_xfermatch_tol'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 0; + /* 3) 업무규칙 분기부 - 단건 대사 결과 판정 (3분기) */ + if (h_wdiff == 0) { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "이체 대사 일치", 0L); + } else if (h_wdiff > h_thr) { + setl(b, T_RC, 5); Bchg(b, T_MSG, 0, "이체 부족 - 미기표 잔액 존재", 0L); + } else if (h_wdiff < 0) { + setl(b, T_RC, 6); Bchg(b, T_MSG, 0, "이체 초과 기표", 0L); } else { - setl(b, T_RC, 1); - Bchg(b, T_MSG, 0, "이체결과 대사 불일치", 0L); - userlog("PAY_XFERMATCH 불일치 id=%ld net=%ld xfer=%ld", h_pid, h_net, h_led); + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "허용 오차 내 일치", 0L); + } + h_wamt = h_wxfer; + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s 대상=%ld 금액=%ld 차액=%ld", "PAY_XFERMATCH", h_wcnt, h_wamt, h_wdiff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_XFERMATCH', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 당일 이체 기표 총계 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_wa1, :h_wa2 + FROM py_ledger WHERE biz_date = :h_wdate AND entry_type = 'XFER'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "이체 기표 총계 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE biz_date = :h_wdate AND status = 'P'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 지급완료 대비 기표 부족: 기준=%ld 실적=%ld", "PAY_XFERMATCH", h_wa1, h_wa3); + } + /* 8) 응답부 */ + setl(b, T_NET, h_wnet); + setl(b, T_AMT1, h_wxfer); + setl(b, T_DELTA, h_wdiff); + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + userlog("PAY_XFERMATCH 완료: 금액=%ld 차액=%ld", h_wamt, h_wdiff); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_XFERMATCH", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); } OK(b); } diff --git a/app/src/py/svc/PAY_XFERMISS.pgc b/app/src/py/svc/PAY_XFERMISS.pgc index c6b0bee..3d50859 100644 --- a/app/src/py/svc/PAY_XFERMISS.pgc +++ b/app/src/py/svc/PAY_XFERMISS.pgc @@ -1,7 +1,9 @@ /* - * PAY_XFERMISS.pgc - py 모듈 서비스 PAY_XFERMISS (one service = one file). + * PAY_XFERMISS.pgc - py (지급) 모듈 온라인 서비스 PAY_XFERMISS. * 이체 기표 누락 검출: 당일 지급완료(P)인데 py_ledger 에 XFER 기표가 없는 * 건수/금액을 NOT EXISTS 안티조인으로 집출한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [xfer]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_XFERMISS.h" @@ -10,27 +12,117 @@ void PAY_XFERMISS(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_cnt, h_sum; + char h_bizdate[64]; + char h_wst[8]; + long h_wnet; + long h_wxfer; + long h_wdiff; + long h_wlid; + long h_wamt; + long h_wcnt; + long h_wsum; + char h_wmerch[64]; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wa1; + long h_wa2; + long h_wa3; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL SELECT count(*), coalesce(sum(p.net),0) INTO :h_cnt, :h_sum + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + /* 2) 마스터 조회부 - 지급완료인데 XFER 기표 없는 건 (NOT EXISTS) */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) + INTO :h_wcnt, :h_wsum FROM py_payout p WHERE p.biz_date = :h_bizdate AND p.status = 'P' AND NOT EXISTS (SELECT 1 FROM py_ledger l - WHERE l.payout_id = p.payout_id AND l.entry_type = 'XFER'); - if (sqlca.sqlcode < 0) FAIL(b); - - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_sum); - if (h_cnt > 0) { - setl(b, T_RC, 1); - Bchg(b, T_MSG, 0, "이체 기표 누락 건 존재", 0L); - userlog("PAY_XFERMISS date=%s missing=%ld amt=%ld", h_bizdate, h_cnt, h_sum); + WHERE l.payout_id = p.payout_id + AND l.entry_type = 'XFER'); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "이체 누락 집계 오류", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wxfer FROM py_payout + WHERE biz_date = :h_bizdate AND status = 'P'; + if (sqlca.sqlcode < 0) h_wxfer = 0; + strcpy(h_pval, "100"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_xfermiss_alert'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 100; + /* 3) 업무규칙 분기부 - 누락 규모 판정 (3분기) */ + if (h_wcnt == 0) { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "이체 기표 누락 없음", 0L); + } else if (h_wcnt > h_thr) { + setl(b, T_RC, 7); Bchg(b, T_MSG, 0, "대량 이체 기표 누락 - 긴급 확인", 0L); } else { - setl(b, T_RC, 0); - Bchg(b, T_MSG, 0, "이체 기표 누락 없음", 0L); + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "이체 기표 누락 건 존재", 0L); + } + h_wamt = h_wsum; + h_wdiff = h_wcnt; + h_wnet = h_wsum; + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s 대상=%ld 금액=%ld 차액=%ld", "PAY_XFERMISS", h_wcnt, h_wamt, h_wdiff); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_XFERMISS', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 6) 집계/요약 갱신부 - 당일 이체 기표 총계 재집계 */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_wa1, :h_wa2 + FROM py_ledger WHERE biz_date = :h_wdate AND entry_type = 'XFER'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 61); + Bchg(b, T_MSG, 0, "이체 기표 총계 재집계 실패", 0L); + FAIL(b); + } + EXEC SQL SELECT count(*) INTO :h_wa3 FROM py_payout + WHERE biz_date = :h_wdate AND status = 'P'; + if (sqlca.sqlcode < 0) h_wa3 = 0; + if (h_wa1 > 0 && h_wa3 > h_wa1) { + userlog("%s 지급완료 대비 기표 부족: 기준=%ld 실적=%ld", "PAY_XFERMISS", h_wa1, h_wa3); + } + /* 8) 응답부 */ + setl(b, T_COUNT, h_wcnt); + setl(b, T_GROSS, h_wsum); + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + userlog("PAY_XFERMISS 완료: 금액=%ld 차액=%ld", h_wamt, h_wdiff); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_XFERMISS", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); } OK(b); } diff --git a/app/src/py/svc/PAY_XFERTYPE.pgc b/app/src/py/svc/PAY_XFERTYPE.pgc index f093d89..21fd7f1 100644 --- a/app/src/py/svc/PAY_XFERTYPE.pgc +++ b/app/src/py/svc/PAY_XFERTYPE.pgc @@ -1,7 +1,9 @@ /* - * PAY_XFERTYPE.pgc - py 모듈 서비스 PAY_XFERTYPE (one service = one file). + * PAY_XFERTYPE.pgc - py (지급) 모듈 온라인 서비스 PAY_XFERTYPE. * 이체유형별 지급 집계: 당일 지급건을 channel 별 커서로 건수/net 합 집계해 * occurrence 로 회신한다 (표준/분할/대량/선지급 물량 분포 파악). + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [cursor]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_XFERTYPE.h" @@ -9,34 +11,128 @@ void PAY_XFERTYPE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - int i = 0; - long kinds = 0; + int gi = 0; + long tot_cnt = 0, tot_sum = 0, tot_fee = 0, top_cnt = 0, top_sum = 0; + char top_grp[64]; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16], h_chan[16]; - long h_cnt, h_amt; + char h_bizdate[64]; + char h_wgrp[64]; + long h_wgcnt; + long h_wgsum; + long h_wgfee; + long h_wtot; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL DECLARE cur_xfertype CURSOR FOR - SELECT channel, count(*), coalesce(sum(net),0) - FROM py_payout WHERE biz_date = :h_bizdate - GROUP BY channel ORDER BY 1; - EXEC SQL OPEN cur_xfertype; - if (sqlca.sqlcode < 0) FAIL(b); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + top_grp[0] = 0; + /* 2) 사전 규모 확인 - 커서 오픈 전 총건수 파악 */ + EXEC SQL SELECT count(*) INTO :h_wtot FROM py_payout p WHERE p.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "집계 사전 조회 실패", 0L); + FAIL(b); + } + userlog("PAY_XFERTYPE 집계 시작: 기준일=%s 대상=%ld건 (이체유형별)", h_wdate, h_wtot); + /* 커서 정의: 이체유형별 건수/금액 집계 */ + EXEC SQL DECLARE cur_pay_xfertype CURSOR FOR + SELECT p.channel, count(*), coalesce(sum(p.net),0), coalesce(sum(p.fee),0) + FROM py_payout p WHERE p.biz_date = :h_bizdate + GROUP BY p.channel ORDER BY 2 DESC, 1; + EXEC SQL OPEN cur_pay_xfertype; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "집계 커서 OPEN 실패", 0L); + FAIL(b); + } + /* 3) 본처리부 - 커서 순회하며 그룹별 집계/상위 추출 */ for (;;) { - EXEC SQL FETCH cur_xfertype INTO :h_chan, :h_cnt, :h_amt; + EXEC SQL FETCH cur_pay_xfertype INTO :h_wgrp, :h_wgcnt, :h_wgsum, :h_wgfee; if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_xfertype; FAIL(b); } - kinds++; - if (i < 8) { - Bchg(b, T_STR3, i, h_chan, 0L); - Bchg(b, T_ARG4, i, (char *)&h_cnt, 0L); - Bchg(b, T_AMT3, i, (char *)&h_amt, 0L); - i++; + if (sqlca.sqlcode < 0) { + EXEC SQL CLOSE cur_pay_xfertype; + setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "집계 FETCH 오류", 0L); FAIL(b); + } + tot_cnt += h_wgcnt; + tot_sum += h_wgsum; + tot_fee += h_wgfee; + if (h_wgcnt > top_cnt) { + top_cnt = h_wgcnt; + top_sum = h_wgsum; + strncpy(top_grp, h_wgrp, sizeof(top_grp)-1); + top_grp[sizeof(top_grp)-1] = 0; + } + if (gi < 10) { + Bchg(b, T_STR1, gi, h_wgrp, 0L); + Bchg(b, T_AMT1, gi, (char *)&h_wgsum, 0L); + Bchg(b, T_ARG1, gi, (char *)&h_wgcnt, 0L); + gi++; } } - EXEC SQL CLOSE cur_xfertype; - setl(b, T_COUNT, kinds); - setl(b, T_RC, 0); + EXEC SQL CLOSE cur_pay_xfertype; + strcpy(h_pval, "5000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_xfertype_concentration'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 5000; + /* 업무규칙 분기부 - 분포 집중도 판정 (3분기) */ + if (gi == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "이체유형별 집계 대상 없음", 0L); + } else if (tot_cnt > 0 && top_cnt * 10000 / tot_cnt > h_thr) { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "특정 이체유형 집중 - 확인 필요", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "이체유형별 정상 분포", 0L); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s %s별 그룹=%d 건수=%ld 금액=%ld 최다=%s", "PAY_XFERTYPE", "이체유형", gi, tot_cnt, tot_sum, top_grp); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_XFERTYPE', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 8) 응답부 */ + setl(b, T_COUNT, tot_cnt); + setl(b, T_AMT3, top_sum); + setl(b, T_ARG4, (long)gi); + Bchg(b, T_STR3, 0, top_grp, 0L); + userlog("PAY_XFERTYPE 완료: 그룹=%d 총건=%ld 총액=%ld 최다=%s", + gi, tot_cnt, tot_sum, top_grp); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_XFERTYPE", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/py/svc/PAY_XFERUNMATCH.pgc b/app/src/py/svc/PAY_XFERUNMATCH.pgc index 8cc2373..c4cec1e 100644 --- a/app/src/py/svc/PAY_XFERUNMATCH.pgc +++ b/app/src/py/svc/PAY_XFERUNMATCH.pgc @@ -1,7 +1,9 @@ /* - * PAY_XFERUNMATCH.pgc - py 모듈 서비스 PAY_XFERUNMATCH (one service = one file). + * PAY_XFERUNMATCH.pgc - py (지급) 모듈 온라인 서비스 PAY_XFERUNMATCH. * 미대사 목록: 지급완료(P) 이면서 이체 기표(py_ledger XFER)가 전혀 없는 * 건을 NOT EXISTS 커서로 뽑아 지급ID/계좌/net 을 occurrence 로 회신한다. + * 처리 구성: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 감사이력 적재 -> 집계 갱신 -> 예외보상 -> 응답 -> 로깅 [cursor]. * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by py_svr. */ #include "PAY_XFERUNMATCH.h" @@ -9,36 +11,126 @@ void PAY_XFERUNMATCH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - int i = 0; - long miss = 0; + int gi = 0; + long tot_cnt = 0, tot_sum = 0, tot_fee = 0, top_cnt = 0, top_sum = 0; + char top_grp[64]; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16], h_acct[32]; - long h_pid, h_net; + char h_bizdate[64]; + char h_wgrp[64]; + long h_wgcnt; + long h_wgsum; + long h_wgfee; + long h_wtot; + char h_wdate[16]; + char h_pval[32]; + long h_thr; + char h_lvl[4]; + char h_amsg[256]; + long h_wz1; + long h_wz2; + long h_wz3; EXEC SQL END DECLARE SECTION; + /* 1) 입력 검증부 - 모든 입력 필드의 필수/범위/형식 검사 */ gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); - EXEC SQL DECLARE cur_xferunmatch CURSOR FOR - SELECT payout_id, account_no, net FROM py_payout p - WHERE p.biz_date = :h_bizdate AND p.status = 'P' - AND NOT EXISTS (SELECT 1 FROM py_ledger l - WHERE l.payout_id = p.payout_id AND l.entry_type = 'XFER') - ORDER BY payout_id; - EXEC SQL OPEN cur_xferunmatch; - if (sqlca.sqlcode < 0) FAIL(b); + if (strlen(h_bizdate) != 10) { + setl(b, T_RC, 11); + Bchg(b, T_MSG, 0, "영업일은 YYYY-MM-DD 형식이어야 함", 0L); + FAIL(b); + } + strncpy(h_wdate, h_bizdate, sizeof(h_wdate)-1); h_wdate[sizeof(h_wdate)-1] = 0; + top_grp[0] = 0; + /* 2) 사전 규모 확인 - 커서 오픈 전 총건수 파악 */ + EXEC SQL SELECT count(*) INTO :h_wtot FROM py_payout p WHERE p.biz_date = :h_bizdate AND p.status = 'P'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 40); + Bchg(b, T_MSG, 0, "집계 사전 조회 실패", 0L); + FAIL(b); + } + userlog("PAY_XFERUNMATCH 집계 시작: 기준일=%s 대상=%ld건 (미대사별)", h_wdate, h_wtot); + /* 커서 정의: 미대사별 건수/금액 집계 */ + EXEC SQL DECLARE cur_pay_xferunmatch CURSOR FOR + SELECT p.merchant_id, count(*), coalesce(sum(p.net),0), coalesce(sum(p.fee),0) + FROM py_payout p WHERE p.biz_date = :h_bizdate AND p.status = 'P' + GROUP BY p.merchant_id ORDER BY 2 DESC, 1; + EXEC SQL OPEN cur_pay_xferunmatch; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 41); + Bchg(b, T_MSG, 0, "집계 커서 OPEN 실패", 0L); + FAIL(b); + } + /* 3) 본처리부 - 커서 순회하며 그룹별 집계/상위 추출 */ for (;;) { - EXEC SQL FETCH cur_xferunmatch INTO :h_pid, :h_acct, :h_net; + EXEC SQL FETCH cur_pay_xferunmatch INTO :h_wgrp, :h_wgcnt, :h_wgsum, :h_wgfee; if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_xferunmatch; FAIL(b); } - miss++; - if (i < 12) { - Bchg(b, T_ARG1, i, (char *)&h_pid, 0L); - Bchg(b, T_STR2, i, h_acct, 0L); - Bchg(b, T_AMT1, i, (char *)&h_net, 0L); - i++; + if (sqlca.sqlcode < 0) { + EXEC SQL CLOSE cur_pay_xferunmatch; + setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "집계 FETCH 오류", 0L); FAIL(b); + } + tot_cnt += h_wgcnt; + tot_sum += h_wgsum; + tot_fee += h_wgfee; + if (h_wgcnt > top_cnt) { + top_cnt = h_wgcnt; + top_sum = h_wgsum; + strncpy(top_grp, h_wgrp, sizeof(top_grp)-1); + top_grp[sizeof(top_grp)-1] = 0; + } + if (gi < 10) { + Bchg(b, T_STR1, gi, h_wgrp, 0L); + Bchg(b, T_AMT1, gi, (char *)&h_wgsum, 0L); + Bchg(b, T_ARG1, gi, (char *)&h_wgcnt, 0L); + gi++; } } - EXEC SQL CLOSE cur_xferunmatch; - setl(b, T_COUNT, miss); - setl(b, T_RC, miss > 0 ? 1 : 0); + EXEC SQL CLOSE cur_pay_xferunmatch; + strcpy(h_pval, "5000"); + EXEC SQL SELECT coalesce(param_val, :h_pval) INTO :h_pval + FROM cm_param WHERE param_key = 'pay_xferunmatch_concentration'; + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "정책 파라미터(cm_param) 조회 실패", 0L); + FAIL(b); + } + h_thr = atol(h_pval); + if (h_thr <= 0) h_thr = 5000; + /* 업무규칙 분기부 - 분포 집중도 판정 (3분기) */ + if (gi == 0) { + setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "미대사별 집계 대상 없음", 0L); + } else if (tot_cnt > 0 && top_cnt * 10000 / tot_cnt > h_thr) { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "특정 미대사 집중 - 확인 필요", 0L); + } else { + setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "미대사별 정상 분포", 0L); + } + strcpy(h_lvl, "I"); + snprintf(h_amsg, sizeof(h_amsg), "%s %s별 그룹=%d 건수=%ld 금액=%ld 최다=%s", "PAY_XFERUNMATCH", "미대사", gi, tot_cnt, tot_sum, top_grp); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (nextval('cm_log_seq'), 'PAY_XFERUNMATCH', :h_lvl, :h_amsg, :h_wdate); + if (sqlca.sqlcode < 0) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "감사로그(cm_log) 적재 실패 - 처리 롤백", 0L); + FAIL(b); + } + /* 8) 응답부 */ + setl(b, T_COUNT, tot_cnt); + Bchg(b, T_STR2, 0, top_grp, 0L); + userlog("PAY_XFERUNMATCH 완료: 그룹=%d 총건=%ld 총액=%ld 최다=%s", + gi, tot_cnt, tot_sum, top_grp); + /* 7) 예외/보상 점검부 - 당일 지급 원장 사후 정합 확인 */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_wz1, :h_wz2 + FROM py_payout WHERE biz_date = :h_wdate; + if (sqlca.sqlcode < 0) { h_wz1 = 0; h_wz2 = 0; } + EXEC SQL SELECT count(*) INTO :h_wz3 FROM py_payout + WHERE biz_date = :h_wdate AND amount - fee <> net; + if (sqlca.sqlcode < 0) h_wz3 = 0; + if (h_wz3 > 0) { + userlog("%s 순액 정합 위반 감지: %ld건 / 당일 %ld건", + "PAY_XFERUNMATCH", h_wz3, h_wz1); + } + if (h_wz2 < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "당일 지급 순액 합계가 음수 - 원장 정합 오류", 0L); + FAIL(b); + } OK(b); } diff --git a/app/src/rc/svc/RC_3WAY.pgc b/app/src/rc/svc/RC_3WAY.pgc index cd17b09..4e0d965 100644 --- a/app/src/rc/svc/RC_3WAY.pgc +++ b/app/src/rc/svc/RC_3WAY.pgc @@ -1,31 +1,159 @@ /* - * RC_3WAY.pgc - rc 모듈 서비스 RC_3WAY (one service = one file). - * Moved verbatim from the former inline rc_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by rc_svr at tpsvrinit. + * RC_3WAY.pgc - rc 모듈 서비스 RC_3WAY (대사 온라인 프로그램, 심화판). + * 업무: 3-way 대사: 승인+매입+원장 모두 존재 시 MATCHED. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_3WAY.h" +#include "acq_common.h" -/* 7. RC_3WAY - 3-way 대사: 승인+매입+원장 모두 존재 시 MATCHED. */ +/* RC_3WAY - 3-way 대사: 승인+매입+원장 모두 존재 시 MATCHED. */ void RC_3WAY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long mid; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_appr, h_led, h_net; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); - h_net = getl(b, T_NET); + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "3WAY_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_3WAY"); + snprintf(h_msg, sizeof(h_msg), "[RC_3WAY] 3-way 대사: 승인+매입+원장 모두 존재 시 MATCHED."); + strcpy(h_key, "-"); + userlog("[RC_3WAY] 대사 진입: 3-way 대사: 승인+매입+원장 모두 존재 시 MATCHED."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - EXEC SQL SELECT count(*) INTO :h_appr FROM approval WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_led FROM ledger WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - mid = rcdb_insert_match(h_pid, "3WAY", h_net, - (h_appr > 0 && h_led > 0) ? "MATCHED" : "EXCEPTION", bizdate); - if (mid < 0) FAIL(b); - setl(b, T_RC, (h_appr > 0 && h_led > 0) ? 0 : 1); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_3WAY] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_3WAY] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + h_net = getl(b, T_NET); + if (h_net < -100000000000L || h_net > 100000000000L) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "순액 범위 초과", 0L); + userlog("[RC_3WAY] 순액 범위 초과 net=%ld", h_net); FAIL(b); + } + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8004); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_3WAY] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_3WAY] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_3WAY] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_3WAY] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_3WAY] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_3WAY] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_3WAY] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_3way CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_3way; + for (;;) { + EXEC SQL FETCH cur_rc_3way INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_3way; userlog("[RC_3WAY] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_3way; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_3way; + userlog("[RC_3WAY] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_3WAY] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_3WAY] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_3WAY] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 대사 로그 적재 (rc_match_log) */ + h_id = rcdb_insert_match(h_pid, "3WAY", h_net, + (unmatched_rows == 0 ? "MATCHED" : "EXCEPTION"), bizdate); + if (h_id < 0) { userlog("[RC_3WAY] 대사 로그 적재 실패 pid=%ld", h_pid); FAIL(b); } + out_cnt = matched_rows > 0 ? matched_rows : 1; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_3WAY] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_3WAY scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_3WAY] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_3WAY] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_3WAY] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_RC, rc_code); + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_3WAY] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_4WAY.pgc b/app/src/rc/svc/RC_4WAY.pgc index e50d60e..6ebbb33 100644 --- a/app/src/rc/svc/RC_4WAY.pgc +++ b/app/src/rc/svc/RC_4WAY.pgc @@ -1,41 +1,160 @@ /* - * RC_4WAY.pgc - rc 모듈 서비스 RC_4WAY (one service = one file). - * 승인-매입-입금-지급 4-way 대사: 승인(approval), 입금(settlement), 지급 - * (ledger SETTLE) 세 축의 증빙이 모두 존재해야 MATCHED, 하나라도 빠지면 - * EXCEPTION 으로 기록하고 예외 원장을 남긴다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_4WAY.pgc - rc 모듈 서비스 RC_4WAY (대사 온라인 프로그램, 심화판). + * 업무: 승인-매입-입금-지급 4-way 대사. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_4WAY.h" +#include "acq_common.h" -/* 31. RC_4WAY - 승인-매입-입금-지급 4-way 대사. */ +/* RC_4WAY - 승인-매입-입금-지급 4-way 대사. */ void RC_4WAY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long mid, ok; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_net, h_appr, h_dep, h_pay; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); - h_net = getl(b, T_NET); + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "4WAY_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_4WAY"); + snprintf(h_msg, sizeof(h_msg), "[RC_4WAY] 승인-매입-입금-지급 4-way 대사."); + strcpy(h_key, "-"); + userlog("[RC_4WAY] 대사 진입: 승인-매입-입금-지급 4-way 대사."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_4WAY] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_4WAY] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + h_net = getl(b, T_NET); + if (h_net < -100000000000L || h_net > 100000000000L) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "순액 범위 초과", 0L); + userlog("[RC_4WAY] 순액 범위 초과 net=%ld", h_net); FAIL(b); + } - EXEC SQL SELECT count(*) INTO :h_appr FROM approval - WHERE purchase_id = :h_pid AND approved = true; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_dep FROM settlement WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_pay FROM ledger - WHERE purchase_id = :h_pid AND entry_type = 'SETTLE'; - if (sqlca.sqlcode < 0) FAIL(b); + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8004); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_4WAY] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_4WAY] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_4WAY] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_4WAY] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_4WAY] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_4WAY] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_4WAY] 예외 집계 오류"); FAIL(b); } - ok = (h_appr > 0 && h_dep > 0 && h_pay > 0); - mid = rcdb_insert_match(h_pid, "4WAY", h_net, ok ? "MATCHED" : "EXCEPTION", bizdate); - if (mid < 0) FAIL(b); - if (!ok && rcdb_insert_exception(h_pid, "4WAY LEG MISSING", bizdate) < 0) FAIL(b); - setl(b, T_ID1, mid); - setl(b, T_RC, ok ? 0 : 1); + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_4way CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_4way; + for (;;) { + EXEC SQL FETCH cur_rc_4way INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_4way; userlog("[RC_4WAY] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_4way; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_4way; + userlog("[RC_4WAY] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_4WAY] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_4WAY] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_4WAY] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 대사 로그 적재 (rc_match_log) */ + h_id = rcdb_insert_match(h_pid, "4WAY", h_net, + (unmatched_rows == 0 ? "MATCHED" : "EXCEPTION"), bizdate); + if (h_id < 0) { userlog("[RC_4WAY] 대사 로그 적재 실패 pid=%ld", h_pid); FAIL(b); } + out_cnt = matched_rows > 0 ? matched_rows : 1; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_4WAY] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_4WAY scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_4WAY] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_4WAY] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_4WAY] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_ID1, h_id); + setl(b, T_RC, rc_code); + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_4WAY] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_ACQMATCH.pgc b/app/src/rc/svc/RC_ACQMATCH.pgc index bd169d4..905669d 100644 --- a/app/src/rc/svc/RC_ACQMATCH.pgc +++ b/app/src/rc/svc/RC_ACQMATCH.pgc @@ -1,32 +1,133 @@ /* - * RC_ACQMATCH.pgc - rc 모듈 서비스 RC_ACQMATCH (one service = one file). - * 매입-입금 축 대사: 당일 매입 순액 합계와 원장 RECON 입금 합계를 비교해 차액을 산출. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_ACQMATCH.pgc - rc 모듈 서비스 RC_ACQMATCH (대사 온라인 프로그램, 심화판). + * 업무: 매입-입금 축 대사: 당일 매입 순액 합계와 원장 RECON 입금 합계를 비교해 차액을 산출. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_ACQMATCH.h" +#include "acq_common.h" -/* 77. RC_ACQMATCH - 매입-입금 축 대사: 당일 매입 순액 합계와 원장 RECON 입금 합계를 비교해 차액을 산출. */ +/* RC_ACQMATCH - 매입-입금 축 대사: 당일 매입 순액 합계와 원장 RECON 입금 합계를 비교해 차액을 산출. */ void RC_ACQMATCH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_net, h_recon; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "ACQMATCH_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_ACQMATCH"); + snprintf(h_msg, sizeof(h_msg), "[RC_ACQMATCH] 매입-입금 축 대사: 당일 매입 순액 합계와 원장 RECON 입금 합계를 비교해 차액을 산출."); + strcpy(h_key, "-"); + userlog("[RC_ACQMATCH] 대사 진입: 매입-입금 축 대사: 당일 매입 순액 합계와 원장 RECON 입금 합계를 비교해 차액을 산출."); - EXEC SQL SELECT coalesce(sum(net),0) INTO :h_net FROM purchase - WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_recon FROM ledger - WHERE biz_date = :h_bizdate AND entry_type = 'RECON'; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_ARG1, h_net); - setl(b, T_ARG2, h_recon); - setl(b, T_DELTA, h_net - h_recon); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_ACQMATCH] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_ACQMATCH] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_ACQMATCH] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_ACQMATCH] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_acqmatch CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_acqmatch; + for (;;) { + EXEC SQL FETCH cur_rc_acqmatch INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_acqmatch; userlog("[RC_ACQMATCH] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_acqmatch; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_acqmatch; + userlog("[RC_ACQMATCH] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_ACQMATCH] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_ACQMATCH] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_ACQMATCH] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_ACQMATCH] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_ACQMATCH scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_ACQMATCH] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_ACQMATCH] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_ACQMATCH] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_ARG1, matched_rows); + setl(b, T_ARG2, unmatched_rows); + setl(b, T_DELTA, h_diff); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_ACQMATCH] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_ACQ_DEP.pgc b/app/src/rc/svc/RC_ACQ_DEP.pgc index 709148a..f6b54d6 100644 --- a/app/src/rc/svc/RC_ACQ_DEP.pgc +++ b/app/src/rc/svc/RC_ACQ_DEP.pgc @@ -1,31 +1,160 @@ /* - * RC_ACQ_DEP.pgc - rc 모듈 서비스 RC_ACQ_DEP (one service = one file). - * Moved verbatim from the former inline rc_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by rc_svr at tpsvrinit. + * RC_ACQ_DEP.pgc - rc 모듈 서비스 RC_ACQ_DEP (대사 온라인 프로그램, 심화판). + * 업무: 매입-입금(원장) 대사: 원장 합계와 net 비교. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_ACQ_DEP.h" +#include "acq_common.h" -/* 3. RC_ACQ_DEP - 매입-입금(원장) 대사: 원장 합계와 net 비교. */ +/* RC_ACQ_DEP - 매입-입금(원장) 대사: 원장 합계와 net 비교. */ void RC_ACQ_DEP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long mid; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_net, h_dep; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); - h_net = getl(b, T_NET); + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "ACQ_DEP_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_ACQ_DEP"); + snprintf(h_msg, sizeof(h_msg), "[RC_ACQ_DEP] 매입-입금(원장) 대사: 원장 합계와 net 비교."); + strcpy(h_key, "-"); + userlog("[RC_ACQ_DEP] 대사 진입: 매입-입금(원장) 대사: 원장 합계와 net 비교."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_dep FROM ledger - WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); - if (sqlca.sqlcode < 0) FAIL(b); - mid = rcdb_insert_match(h_pid, "ACQ_DEP", h_dep, - (h_dep == h_net) ? "MATCHED" : "EXCEPTION", bizdate); - if (mid < 0) FAIL(b); - setl(b, T_GROSS, h_dep); - setl(b, T_RC, (h_dep == h_net) ? 0 : 1); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_ACQ_DEP] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_ACQ_DEP] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + h_net = getl(b, T_NET); + if (h_net < -100000000000L || h_net > 100000000000L) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "순액 범위 초과", 0L); + userlog("[RC_ACQ_DEP] 순액 범위 초과 net=%ld", h_net); FAIL(b); + } + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8004); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_ACQ_DEP] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_ACQ_DEP] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_ACQ_DEP] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_ACQ_DEP] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_ACQ_DEP] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_ACQ_DEP] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_ACQ_DEP] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_acq_dep CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_acq_dep; + for (;;) { + EXEC SQL FETCH cur_rc_acq_dep INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_acq_dep; userlog("[RC_ACQ_DEP] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_acq_dep; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_acq_dep; + userlog("[RC_ACQ_DEP] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_ACQ_DEP] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_ACQ_DEP] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_ACQ_DEP] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 대사 로그 적재 (rc_match_log) */ + h_id = rcdb_insert_match(h_pid, "ACQ_DEP", h_net, + (unmatched_rows == 0 ? "MATCHED" : "EXCEPTION"), bizdate); + if (h_id < 0) { userlog("[RC_ACQ_DEP] 대사 로그 적재 실패 pid=%ld", h_pid); FAIL(b); } + out_cnt = matched_rows > 0 ? matched_rows : 1; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_ACQ_DEP] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_ACQ_DEP scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_ACQ_DEP] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_ACQ_DEP] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_ACQ_DEP] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_GROSS, h_gross); + setl(b, T_RC, rc_code); + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_ACQ_DEP] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_AGEING.pgc b/app/src/rc/svc/RC_AGEING.pgc index efdf16a..1e1c00c 100644 --- a/app/src/rc/svc/RC_AGEING.pgc +++ b/app/src/rc/svc/RC_AGEING.pgc @@ -1,39 +1,134 @@ /* - * RC_AGEING.pgc - rc 모듈 서비스 RC_AGEING (one service = one file). - * 미대사 에이징: 미해결 예외를 당일 / 최근 7일 / 7일 초과 세 버킷으로 나눠 - * 건수를 집계하여 미대사 잔량의 노후도를 보여준다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_AGEING.pgc - rc 모듈 서비스 RC_AGEING (대사 온라인 프로그램, 심화판). + * 업무: 미해결 예외 에이징 (당일/7일/초과). + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_AGEING.h" +#include "acq_common.h" -/* 64. RC_AGEING - 미해결 예외 에이징 (당일/7일/초과). */ +/* RC_AGEING - 미해결 예외 에이징 (당일/7일/초과). */ void RC_AGEING(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_b0, h_b7, h_bx; - char h_bizdate[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "AGEING_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_AGEING"); + snprintf(h_msg, sizeof(h_msg), "[RC_AGEING] 미해결 예외 에이징 (당일/7일/초과)."); + strcpy(h_key, "-"); + userlog("[RC_AGEING] 대사 진입: 미해결 예외 에이징 (당일/7일/초과)."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_AGEING] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); - EXEC SQL SELECT count(*) INTO :h_b0 FROM rc_exception - WHERE resolved = false AND biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_b7 FROM rc_exception - WHERE resolved = false AND biz_date < CAST(:h_bizdate AS date) - AND biz_date >= CAST(:h_bizdate AS date) - 7; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_bx FROM rc_exception - WHERE resolved = false AND biz_date < CAST(:h_bizdate AS date) - 7; - if (sqlca.sqlcode < 0) FAIL(b); + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_AGEING] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_AGEING] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_AGEING] 예외 집계 오류"); FAIL(b); } - setl(b, T_ARG1, h_b0); - setl(b, T_ARG2, h_b7); - setl(b, T_ARG3, h_bx); - setl(b, T_COUNT, h_b0 + h_b7 + h_bx); + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_ageing CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_ageing; + for (;;) { + EXEC SQL FETCH cur_rc_ageing INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_ageing; userlog("[RC_AGEING] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_ageing; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_ageing; + userlog("[RC_AGEING] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_AGEING] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_AGEING] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_AGEING] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_AGEING] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_AGEING scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_AGEING] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_AGEING] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_AGEING] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_ARG1, matched_rows); + setl(b, T_ARG2, unmatched_rows); + setl(b, T_ARG3, scan_rows); + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_AGEING] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_ALERT.pgc b/app/src/rc/svc/RC_ALERT.pgc index 2592295..071b0a1 100644 --- a/app/src/rc/svc/RC_ALERT.pgc +++ b/app/src/rc/svc/RC_ALERT.pgc @@ -1,32 +1,164 @@ /* - * RC_ALERT.pgc - rc 모듈 서비스 RC_ALERT (one service = one file). - * Moved verbatim from the former inline rc_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by rc_svr at tpsvrinit. + * RC_ALERT.pgc - rc 모듈 서비스 RC_ALERT (대사 온라인 프로그램, 심화판). + * 업무: 대사 경보 등록. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_ALERT.h" +#include "acq_common.h" -/* 26. RC_ALERT - 대사 경보 등록. */ +/* RC_ALERT - 대사 경보 등록. */ void RC_ALERT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], msg[128], lvl[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_id, h_pid; - char h_bizdate[16], h_msg[128], h_lvl[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); + + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "ALERT_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_ALERT"); + snprintf(h_msg, sizeof(h_msg), "[RC_ALERT] 대사 경보 등록."); + strcpy(h_key, "-"); + userlog("[RC_ALERT] 대사 진입: 대사 경보 등록."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - gets_(b, T_MSG, msg, sizeof(msg)); if (msg[0] == 0) strcpy(msg, "대사 지연 경보"); - gets_(b, T_STR1, lvl, sizeof(lvl)); if (lvl[0] == 0) strcpy(lvl, "WARN"); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_ALERT] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - strncpy(h_msg, msg, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; - strncpy(h_lvl, lvl, sizeof(h_lvl)-1); h_lvl[sizeof(h_lvl)-1] = 0; + strcpy(h_key, bizdate); + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_ALERT] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + gets_(b, T_MSG, tmpin, sizeof(tmpin)); if (tmpin[0]) { strncpy(h_msg, tmpin, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1]=0; } + gets_(b, T_STR1, tmpin, sizeof(tmpin)); + if (tmpin[0]) { strncpy(h_nbizdate, tmpin, sizeof(h_nbizdate)-1); h_nbizdate[sizeof(h_nbizdate)-1]=0; strncpy(h_lvl, tmpin, sizeof(h_lvl)-1); h_lvl[sizeof(h_lvl)-1]=0; } + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_ALERT] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_ALERT] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_ALERT] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_ALERT] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_ALERT] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_ALERT] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_ALERT] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_alert CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_alert; + for (;;) { + EXEC SQL FETCH cur_rc_alert INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_alert; userlog("[RC_ALERT] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_alert; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_alert; + userlog("[RC_ALERT] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_ALERT] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_ALERT] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_ALERT] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 경보 등록 (rc_alert) */ + if (unmatched_rows > 0 || h_ecnt > 0) strcpy(h_lvl, "WARN"); + if (unmatched_rows > 10 || h_ecnt > 20) strcpy(h_lvl, "CRIT"); + snprintf(sbuf, sizeof(sbuf), "대사 경보 등록. unmatch=%ld exc=%ld", unmatched_rows, h_ecnt); + strncpy(h_msg, sbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; EXEC SQL SELECT nextval('rc_alert_seq') INTO :h_id; if (sqlca.sqlcode < 0) FAIL(b); EXEC SQL INSERT INTO rc_alert (alert_id, purchase_id, lvl, message, biz_date) VALUES (:h_id, :h_pid, :h_lvl, :h_msg, :h_bizdate); - if (sqlca.sqlcode < 0) { userlog("RC_ALERT INSERT FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + if (sqlca.sqlcode < 0) { userlog("[RC_ALERT] 경보 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + out_cnt = h_ecnt; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_ALERT] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_ALERT scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_ALERT] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_ALERT] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_ALERT] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ setl(b, T_ID1, h_id); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_ALERT] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_AMTDIFF.pgc b/app/src/rc/svc/RC_AMTDIFF.pgc index cfe542e..ea0aa18 100644 --- a/app/src/rc/svc/RC_AMTDIFF.pgc +++ b/app/src/rc/svc/RC_AMTDIFF.pgc @@ -1,34 +1,163 @@ /* - * RC_AMTDIFF.pgc - rc 모듈 서비스 RC_AMTDIFF (one service = one file). - * Moved verbatim from the former inline rc_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by rc_svr at tpsvrinit. + * RC_AMTDIFF.pgc - rc 모듈 서비스 RC_AMTDIFF (대사 온라인 프로그램, 심화판). + * 업무: 금액 차이 대사: 기대(net) vs 실제(원장합) 차이 기록. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_AMTDIFF.h" +#include "acq_common.h" -/* 5. RC_AMTDIFF - 금액 차이 대사: 기대(net) vs 실제(원장합) 차이 기록. */ +/* RC_AMTDIFF - 금액 차이 대사: 기대(net) vs 실제(원장합) 차이 기록. */ void RC_AMTDIFF(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_id, h_pid, h_exp, h_act, h_diff; - char h_bizdate[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); - h_exp = getl(b, T_NET); + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "AMTDIFF_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_AMTDIFF"); + snprintf(h_msg, sizeof(h_msg), "[RC_AMTDIFF] 금액 차이 대사: 기대(net) vs 실제(원장합) 차이 기록."); + strcpy(h_key, "-"); + userlog("[RC_AMTDIFF] 대사 진입: 금액 차이 대사: 기대(net) vs 실제(원장합) 차이 기록."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_AMTDIFF] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger - WHERE purchase_id = :h_pid AND entry_type = 'SETTLE'; - if (sqlca.sqlcode < 0) FAIL(b); - h_diff = h_exp - h_act; + strcpy(h_key, bizdate); + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_AMTDIFF] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + h_net = getl(b, T_NET); + if (h_net < -100000000000L || h_net > 100000000000L) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "순액 범위 초과", 0L); + userlog("[RC_AMTDIFF] 순액 범위 초과 net=%ld", h_net); FAIL(b); + } + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8004); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_AMTDIFF] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_AMTDIFF] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_AMTDIFF] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_AMTDIFF] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_AMTDIFF] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_AMTDIFF] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_AMTDIFF] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_amtdiff CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_amtdiff; + for (;;) { + EXEC SQL FETCH cur_rc_amtdiff INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_amtdiff; userlog("[RC_AMTDIFF] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_amtdiff; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_amtdiff; + userlog("[RC_AMTDIFF] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_AMTDIFF] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_AMTDIFF] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_AMTDIFF] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 차이 라인 적재 (rc_diff) */ + h_exp = h_net; h_act = h_led; h_diff = h_exp - h_act; EXEC SQL SELECT nextval('rc_diff_seq') INTO :h_id; if (sqlca.sqlcode < 0) FAIL(b); EXEC SQL INSERT INTO rc_diff (diff_id, purchase_id, expected_amt, actual_amt, diff_amt, diff_kind, biz_date) - VALUES (:h_id, :h_pid, :h_exp, :h_act, :h_diff, 'AMT', :h_bizdate); - if (sqlca.sqlcode < 0) { userlog("RC_AMTDIFF INSERT FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + VALUES (:h_id, :h_pid, :h_exp, :h_act, :h_diff, :h_dtype, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_AMTDIFF] 차이 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + out_cnt = (h_diff != 0 ? 1 : 0); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_AMTDIFF] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_AMTDIFF scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_AMTDIFF] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_AMTDIFF] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_AMTDIFF] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ setl(b, T_DELTA, h_diff); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_AMTDIFF] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_APPRGAP.pgc b/app/src/rc/svc/RC_APPRGAP.pgc index 40e071b..dfbf0ab 100644 --- a/app/src/rc/svc/RC_APPRGAP.pgc +++ b/app/src/rc/svc/RC_APPRGAP.pgc @@ -1,41 +1,135 @@ /* - * RC_APPRGAP.pgc - rc 모듈 서비스 RC_APPRGAP (one service = one file). - * 승인 누락 감지: 당일 매입 중 승인 원장이 없는 건을 예외로 등록하고 누락 건수를 반환. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_APPRGAP.pgc - rc 모듈 서비스 RC_APPRGAP (대사 온라인 프로그램, 심화판). + * 업무: 승인 누락 감지: 당일 매입 중 승인 원장이 없는 건을 예외로 등록하고 누락 건수를 반환. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_APPRGAP.h" +#include "acq_common.h" -/* 81. RC_APPRGAP - 승인 누락 감지: 당일 매입 중 승인 원장이 없는 건을 예외로 등록하고 누락 건수를 반환. */ +/* RC_APPRGAP - 승인 누락 감지: 당일 매입 중 승인 원장이 없는 건을 예외로 등록하고 누락 건수를 반환. */ void RC_APPRGAP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_id, h_pid, h_cnt = 0; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "APPRGAP_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_APPRGAP"); + snprintf(h_msg, sizeof(h_msg), "[RC_APPRGAP] 승인 누락 감지: 당일 매입 중 승인 원장이 없는 건을 예외로 등록하고 누락 건수를 반환."); + strcpy(h_key, "-"); + userlog("[RC_APPRGAP] 대사 진입: 승인 누락 감지: 당일 매입 중 승인 원장이 없는 건을 예외로 등록하고 누락 건수를 반환."); - EXEC SQL DECLARE cur_appr CURSOR FOR - SELECT p.purchase_id FROM purchase p - WHERE p.biz_date = :h_bizdate - AND NOT EXISTS (SELECT 1 FROM approval a WHERE a.purchase_id = p.purchase_id); - EXEC SQL OPEN cur_appr; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH cur_appr INTO :h_pid; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_appr; FAIL(b); } - EXEC SQL SELECT nextval('rc_exc_seq') INTO :h_id; - EXEC SQL INSERT INTO rc_exception (exc_id, purchase_id, reason, biz_date) - VALUES (:h_id, :h_pid, 'APPR_GAP', :h_bizdate); - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_appr; FAIL(b); } - h_cnt++; + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_APPRGAP] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); } - EXEC SQL CLOSE cur_appr; - setl(b, T_COUNT, h_cnt); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_APPRGAP] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_APPRGAP] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_APPRGAP] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_apprgap CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_apprgap; + for (;;) { + EXEC SQL FETCH cur_rc_apprgap INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_apprgap; userlog("[RC_APPRGAP] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_apprgap; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_apprgap; + userlog("[RC_APPRGAP] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_APPRGAP] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_APPRGAP] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_APPRGAP] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 예외 적재 (rc_exception) */ + if (h_ecnt >= 0) { + h_id = rcdb_insert_exception(h_pid, h_reason, bizdate); + if (h_id < 0) { userlog("[RC_APPRGAP] 예외 적재 실패 pid=%ld", h_pid); FAIL(b); } + } + out_cnt = h_ecnt + (h_pid > 0 ? 1 : 0); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_APPRGAP] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_APPRGAP scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_APPRGAP] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_APPRGAP] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_APPRGAP] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_APPRGAP] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_APPRMATCH.pgc b/app/src/rc/svc/RC_APPRMATCH.pgc index 557e4fe..fe48f50 100644 --- a/app/src/rc/svc/RC_APPRMATCH.pgc +++ b/app/src/rc/svc/RC_APPRMATCH.pgc @@ -1,34 +1,133 @@ /* - * RC_APPRMATCH.pgc - rc 모듈 서비스 RC_APPRMATCH (one service = one file). - * 승인-매입 축 대사: 당일 매입 중 승인건과 매칭된 건수와 승인 누락 건수를 분리 집계. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_APPRMATCH.pgc - rc 모듈 서비스 RC_APPRMATCH (대사 온라인 프로그램, 심화판). + * 업무: 승인-매입 축 대사: 당일 매입 중 승인건과 매칭된 건수와 승인 누락 건수를 분리 집계. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_APPRMATCH.h" +#include "acq_common.h" -/* 76. RC_APPRMATCH - 승인-매입 축 대사: 당일 매입 중 승인건과 매칭된 건수와 승인 누락 건수를 분리 집계. */ +/* RC_APPRMATCH - 승인-매입 축 대사: 당일 매입 중 승인건과 매칭된 건수와 승인 누락 건수를 분리 집계. */ void RC_APPRMATCH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_matched, h_missing; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "APPRMATCH_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_APPRMATCH"); + snprintf(h_msg, sizeof(h_msg), "[RC_APPRMATCH] 승인-매입 축 대사: 당일 매입 중 승인건과 매칭된 건수와 승인 누락 건수를 분리 집계."); + strcpy(h_key, "-"); + userlog("[RC_APPRMATCH] 대사 진입: 승인-매입 축 대사: 당일 매입 중 승인건과 매칭된 건수와 승인 누락 건수를 분리 집계."); - EXEC SQL SELECT count(*) INTO :h_matched FROM purchase p - JOIN approval a ON a.purchase_id = p.purchase_id - WHERE p.biz_date = :h_bizdate AND a.approved = true; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_missing FROM purchase p + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_APPRMATCH] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_APPRMATCH] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_APPRMATCH] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_APPRMATCH] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_apprmatch CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_apprmatch; + for (;;) { + EXEC SQL FETCH cur_rc_apprmatch INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_apprmatch; userlog("[RC_APPRMATCH] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_apprmatch; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_apprmatch; + userlog("[RC_APPRMATCH] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_APPRMATCH] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_APPRMATCH] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_APPRMATCH] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_APPRMATCH] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_APPRMATCH scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_APPRMATCH] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p WHERE p.biz_date = :h_bizdate - AND NOT EXISTS (SELECT 1 FROM approval a WHERE a.purchase_id = p.purchase_id); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_ARG1, h_matched); - setl(b, T_ARG2, h_missing); - setl(b, T_COUNT, h_matched + h_missing); + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_APPRMATCH] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_APPRMATCH] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_ARG1, matched_rows); + setl(b, T_ARG2, unmatched_rows); + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_APPRMATCH] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_APPR_ACQ.pgc b/app/src/rc/svc/RC_APPR_ACQ.pgc index a5135bf..645b30e 100644 --- a/app/src/rc/svc/RC_APPR_ACQ.pgc +++ b/app/src/rc/svc/RC_APPR_ACQ.pgc @@ -1,32 +1,160 @@ /* - * RC_APPR_ACQ.pgc - rc 모듈 서비스 RC_APPR_ACQ (one service = one file). - * Moved verbatim from the former inline rc_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by rc_svr at tpsvrinit. + * RC_APPR_ACQ.pgc - rc 모듈 서비스 RC_APPR_ACQ (대사 온라인 프로그램, 심화판). + * 업무: 승인-매입 대사 (승인 존재 여부 확인 후 대사 로그). + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_APPR_ACQ.h" +#include "acq_common.h" -/* 2. RC_APPR_ACQ - 승인-매입 대사 (승인 존재 여부 확인 후 대사 로그). */ +/* RC_APPR_ACQ - 승인-매입 대사 (승인 존재 여부 확인 후 대사 로그). */ void RC_APPR_ACQ(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long mid; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_cnt, h_net; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); - h_net = getl(b, T_NET); + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "APPR_ACQ_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_APPR_ACQ"); + snprintf(h_msg, sizeof(h_msg), "[RC_APPR_ACQ] 승인-매입 대사 (승인 존재 여부 확인 후 대사 로그)."); + strcpy(h_key, "-"); + userlog("[RC_APPR_ACQ] 대사 진입: 승인-매입 대사 (승인 존재 여부 확인 후 대사 로그)."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - EXEC SQL SELECT count(*) INTO :h_cnt FROM approval WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_cnt > 0) - mid = rcdb_insert_match(h_pid, "APPR_ACQ", h_net, "MATCHED", bizdate); - else - mid = rcdb_insert_match(h_pid, "APPR_ACQ", h_net, "EXCEPTION", bizdate); - if (mid < 0) FAIL(b); - setl(b, T_ID1, mid); - setl(b, T_RC, h_cnt > 0 ? 0 : 1); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_APPR_ACQ] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_APPR_ACQ] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + h_net = getl(b, T_NET); + if (h_net < -100000000000L || h_net > 100000000000L) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "순액 범위 초과", 0L); + userlog("[RC_APPR_ACQ] 순액 범위 초과 net=%ld", h_net); FAIL(b); + } + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8004); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_APPR_ACQ] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_APPR_ACQ] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_APPR_ACQ] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_APPR_ACQ] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_APPR_ACQ] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_APPR_ACQ] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_APPR_ACQ] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_appr_acq CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_appr_acq; + for (;;) { + EXEC SQL FETCH cur_rc_appr_acq INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_appr_acq; userlog("[RC_APPR_ACQ] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_appr_acq; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_appr_acq; + userlog("[RC_APPR_ACQ] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_APPR_ACQ] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_APPR_ACQ] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_APPR_ACQ] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 대사 로그 적재 (rc_match_log) */ + h_id = rcdb_insert_match(h_pid, "APPR_ACQ", h_net, + (unmatched_rows == 0 ? "MATCHED" : "EXCEPTION"), bizdate); + if (h_id < 0) { userlog("[RC_APPR_ACQ] 대사 로그 적재 실패 pid=%ld", h_pid); FAIL(b); } + out_cnt = matched_rows > 0 ? matched_rows : 1; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_APPR_ACQ] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_APPR_ACQ scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_APPR_ACQ] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_APPR_ACQ] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_APPR_ACQ] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_ID1, h_id); + setl(b, T_RC, rc_code); + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_APPR_ACQ] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_AUDITADD.pgc b/app/src/rc/svc/RC_AUDITADD.pgc index 970989e..532063e 100644 --- a/app/src/rc/svc/RC_AUDITADD.pgc +++ b/app/src/rc/svc/RC_AUDITADD.pgc @@ -1,29 +1,155 @@ /* - * RC_AUDITADD.pgc - rc 모듈 서비스 RC_AUDITADD (one service = one file). - * 대사 감사추적 기록: 지정 매입의 대사 처리 이력을 AUDIT 유형 로그로 감사 추적에 적재. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_AUDITADD.pgc - rc 모듈 서비스 RC_AUDITADD (대사 온라인 프로그램, 심화판). + * 업무: 대사 감사추적 기록: 지정 매입의 대사 처리 이력을 AUDIT 유형 로그로 감사 추적에 적재. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_AUDITADD.h" +#include "acq_common.h" -/* 132. RC_AUDITADD - 대사 감사추적 기록: 지정 매입의 대사 처리 이력을 AUDIT 유형 로그로 감사 추적에 적재. */ +/* RC_AUDITADD - 대사 감사추적 기록: 지정 매입의 대사 처리 이력을 AUDIT 유형 로그로 감사 추적에 적재. */ void RC_AUDITADD(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_id, h_pid; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "AUDITADD_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_AUDITADD"); + snprintf(h_msg, sizeof(h_msg), "[RC_AUDITADD] 대사 감사추적 기록: 지정 매입의 대사 처리 이력을 AUDIT 유형 로그로 감사 추적에 적재."); + strcpy(h_key, "-"); + userlog("[RC_AUDITADD] 대사 진입: 대사 감사추적 기록: 지정 매입의 대사 처리 이력을 AUDIT 유형 로그로 감사 추적에 적재."); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_AUDITADD] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); h_pid = getl(b, T_PURCHASE_ID); - EXEC SQL SELECT nextval('rc_match_seq') INTO :h_id; - EXEC SQL INSERT INTO rc_match_log (match_id, purchase_id, match_type, amount, status, biz_date) - VALUES (:h_id, :h_pid, 'AUDIT', 0, 'CONFIRMED', :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_AUDITADD] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_AUDITADD] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_AUDITADD] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_AUDITADD] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_AUDITADD] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_AUDITADD] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_AUDITADD] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_AUDITADD] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_auditadd CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_auditadd; + for (;;) { + EXEC SQL FETCH cur_rc_auditadd INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_auditadd; userlog("[RC_AUDITADD] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_auditadd; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_auditadd; + userlog("[RC_AUDITADD] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_AUDITADD] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_AUDITADD] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_AUDITADD] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 대사 로그 적재 (rc_match_log) */ + h_id = rcdb_insert_match(h_pid, "AUDITADD", h_net, + (unmatched_rows == 0 ? "MATCHED" : "EXCEPTION"), bizdate); + if (h_id < 0) { userlog("[RC_AUDITADD] 대사 로그 적재 실패 pid=%ld", h_pid); FAIL(b); } + out_cnt = matched_rows > 0 ? matched_rows : 1; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_AUDITADD] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_AUDITADD scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_AUDITADD] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_AUDITADD] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_AUDITADD] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ setl(b, T_ID1, h_id); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_AUDITADD] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_AUDITLOG.pgc b/app/src/rc/svc/RC_AUDITLOG.pgc index 3b9d99d..6004f60 100644 --- a/app/src/rc/svc/RC_AUDITLOG.pgc +++ b/app/src/rc/svc/RC_AUDITLOG.pgc @@ -1,36 +1,155 @@ /* - * RC_AUDITLOG.pgc - rc 모듈 서비스 RC_AUDITLOG (one service = one file). - * 대사 감사 로그 기록: 호출 서비스명과 사유를 받아 원장에 RC_AUDIT 증적을 - * 남기고 동시에 INFO 경보로 감사 이벤트를 적재한다 (이중 기록). - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_AUDITLOG.pgc - rc 모듈 서비스 RC_AUDITLOG (대사 온라인 프로그램, 심화판). + * 업무: 감사 이벤트 이중 기록 (원장 + 경보). + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_AUDITLOG.h" +#include "acq_common.h" -/* 63. RC_AUDITLOG - 감사 이벤트 이중 기록 (원장 + 경보). */ +/* RC_AUDITLOG - 감사 이벤트 이중 기록 (원장 + 경보). */ void RC_AUDITLOG(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], svcname[32], reason[64]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid; - char h_bizdate[16], h_msg[120]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "AUDITLOG_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_AUDITLOG"); + snprintf(h_msg, sizeof(h_msg), "[RC_AUDITLOG] 감사 이벤트 이중 기록 (원장 + 경보)."); + strcpy(h_key, "-"); + userlog("[RC_AUDITLOG] 대사 진입: 감사 이벤트 이중 기록 (원장 + 경보)."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - gets_(b, T_SVCNAME, svcname, sizeof(svcname)); - gets_(b, T_REASON, reason, sizeof(reason)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_AUDITLOG] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_AUDITLOG] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + gets_(b, T_REASON, tmpin, sizeof(tmpin)); if (tmpin[0]) { strncpy(h_reason, tmpin, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1]=0; } - EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) - VALUES (nextval('ledger_seq'), :h_pid, 'RC_AUDIT', 0, :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_AUDITLOG] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_AUDITLOG] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_AUDITLOG] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_AUDITLOG] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_AUDITLOG] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_AUDITLOG] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_AUDITLOG] 예외 집계 오류"); FAIL(b); } - snprintf(h_msg, sizeof(h_msg), "audit %.28s: %.60s", svcname, reason); - EXEC SQL INSERT INTO rc_alert (alert_id, purchase_id, lvl, message, biz_date) - VALUES (nextval('rc_alert_seq'), :h_pid, 'INFO', :h_msg, :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_RC, 0); + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_auditlog CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_auditlog; + for (;;) { + EXEC SQL FETCH cur_rc_auditlog INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_auditlog; userlog("[RC_AUDITLOG] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_auditlog; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_auditlog; + userlog("[RC_AUDITLOG] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_AUDITLOG] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_AUDITLOG] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_AUDITLOG] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 대사 로그 적재 (rc_match_log) */ + h_id = rcdb_insert_match(h_pid, "AUDITLOG", h_net, + (unmatched_rows == 0 ? "MATCHED" : "EXCEPTION"), bizdate); + if (h_id < 0) { userlog("[RC_AUDITLOG] 대사 로그 적재 실패 pid=%ld", h_pid); FAIL(b); } + out_cnt = matched_rows > 0 ? matched_rows : 1; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_AUDITLOG] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_AUDITLOG scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_AUDITLOG] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_AUDITLOG] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_AUDITLOG] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_RC, rc_code); + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_AUDITLOG] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_AUDITTRAIL.pgc b/app/src/rc/svc/RC_AUDITTRAIL.pgc index ed8d19f..b5c0513 100644 --- a/app/src/rc/svc/RC_AUDITTRAIL.pgc +++ b/app/src/rc/svc/RC_AUDITTRAIL.pgc @@ -1,36 +1,135 @@ /* - * RC_AUDITTRAIL.pgc - rc 모듈 서비스 RC_AUDITTRAIL (one service = one file). - * 대사 감사추적 조회: 특정 매입의 대사 이력 건수, 대사 관련 원장 증적 건수, - * 그리고 최종 대사 상태를 한 번에 조회하여 감사 관점의 추적 정보를 준다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_AUDITTRAIL.pgc - rc 모듈 서비스 RC_AUDITTRAIL (대사 온라인 프로그램, 심화판). + * 업무: 매입별 대사 감사추적(이력/증적/최종상태). + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_AUDITTRAIL.h" +#include "acq_common.h" -/* 62. RC_AUDITTRAIL - 매입별 대사 감사추적(이력/증적/최종상태). */ +/* RC_AUDITTRAIL - 매입별 대사 감사추적(이력/증적/최종상태). */ void RC_AUDITTRAIL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_hist, h_evi; - char h_last[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "AUDITTRAIL_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_AUDITTRAIL"); + snprintf(h_msg, sizeof(h_msg), "[RC_AUDITTRAIL] 매입별 대사 감사추적(이력/증적/최종상태)."); + strcpy(h_key, "-"); + userlog("[RC_AUDITTRAIL] 대사 진입: 매입별 대사 감사추적(이력/증적/최종상태)."); + + /* [1] 입력 검증부 -------------------------------------------------- */ h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_AUDITTRAIL] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + /* 조회 기준일 미입력 서비스: 매입 원장에서 영업일 유도 */ - EXEC SQL SELECT count(*) INTO :h_hist FROM rc_match_log WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_evi FROM ledger - WHERE purchase_id = :h_pid AND entry_type LIKE 'RC%'; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT coalesce( - (SELECT status FROM rc_match_log WHERE purchase_id = :h_pid - ORDER BY updated_at DESC, match_id DESC LIMIT 1), 'NONE') - INTO :h_last; - if (sqlca.sqlcode < 0) FAIL(b); + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_AUDITTRAIL] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_AUDITTRAIL] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_AUDITTRAIL] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_AUDITTRAIL] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { userlog("[RC_AUDITTRAIL] 대사 로그 조회 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_led FROM ledger WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { userlog("[RC_AUDITTRAIL] 원장 조회 오류"); FAIL(b); } - setl(b, T_ARG1, h_hist); - setl(b, T_ARG2, h_evi); - Bchg(b, T_STATUS, 0, h_last, 0L); + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_audittrail CURSOR FOR + SELECT match_id, amount, status FROM rc_match_log + WHERE purchase_id = :h_pid ORDER BY match_id; + EXEC SQL OPEN cur_rc_audittrail; + for (;;) { + EXEC SQL FETCH cur_rc_audittrail INTO :h_id2, :h_val, :h_status; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_audittrail; userlog("[RC_AUDITTRAIL] 이력 커서 오류"); FAIL(b); } + scan_rows++; + if (!strcmp(h_status, "MATCHED") || !strcmp(h_status, "CONFIRMED")) matched_rows++; + else { unmatched_rows++; unmatched_amt += (h_val < 0 ? -h_val : h_val); } + } + EXEC SQL CLOSE cur_rc_audittrail; + userlog("[RC_AUDITTRAIL] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_AUDITTRAIL] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_AUDITTRAIL] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_AUDITTRAIL] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_AUDITTRAIL] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_AUDITTRAIL scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_AUDITTRAIL] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { userlog("[RC_AUDITTRAIL] 정산 조회 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_ARG1, matched_rows); + setl(b, T_ARG2, unmatched_rows); + Bchg(b, T_STATUS, 0, h_status, 0L); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_AUDITTRAIL] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_AUTOFIX.pgc b/app/src/rc/svc/RC_AUTOFIX.pgc index 229ae6d..2f6ac30 100644 --- a/app/src/rc/svc/RC_AUTOFIX.pgc +++ b/app/src/rc/svc/RC_AUTOFIX.pgc @@ -1,20 +1,159 @@ /* - * RC_AUTOFIX.pgc - rc 모듈 서비스 RC_AUTOFIX (one service = one file). - * Moved verbatim from the former inline rc_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by rc_svr at tpsvrinit. + * RC_AUTOFIX.pgc - rc 모듈 서비스 RC_AUTOFIX (대사 온라인 프로그램, 심화판). + * 업무: 자동 보정: 예외 해소 + 보정 원장. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_AUTOFIX.h" +#include "acq_common.h" -/* 14. RC_AUTOFIX - 자동 보정: 예외 해소 + 보정 원장. */ +/* RC_AUTOFIX - 자동 보정: 예외 해소 + 보정 원장. */ void RC_AUTOFIX(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long pid = getl(b, T_PURCHASE_ID), amt = getl(b, T_AMOUNT); + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; + EXEC SQL END DECLARE SECTION; + + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "AUTOFIX_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_AUTOFIX"); + snprintf(h_msg, sizeof(h_msg), "[RC_AUTOFIX] 자동 보정: 예외 해소 + 보정 원장."); + strcpy(h_key, "-"); + userlog("[RC_AUTOFIX] 대사 진입: 자동 보정: 예외 해소 + 보정 원장."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (rcdb_resolve_exception(pid) < 0) FAIL(b); - if (rcdb_insert_ledger(pid, "AUTOFIX", amt, bizdate) < 0) FAIL(b); - if (rcdb_update_match_status(pid, "EXCEPTION", "MATCHED") < 0) FAIL(b); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_AUTOFIX] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_AUTOFIX] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + h_amt = getl(b, T_AMOUNT); + if (h_amt < 0) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "금액은 음수가 될 수 없음", 0L); + userlog("[RC_AUTOFIX] 금액 검증 실패 amt=%ld", h_amt); FAIL(b); + } + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8004); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_AUTOFIX] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_AUTOFIX] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_AUTOFIX] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_AUTOFIX] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_AUTOFIX] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_AUTOFIX] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_AUTOFIX] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_autofix CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_autofix; + for (;;) { + EXEC SQL FETCH cur_rc_autofix INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_autofix; userlog("[RC_AUTOFIX] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_autofix; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_autofix; + userlog("[RC_AUTOFIX] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_AUTOFIX] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_AUTOFIX] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_AUTOFIX] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 대사 로그 적재 (rc_match_log) */ + h_id = rcdb_insert_match(h_pid, "AUTOFIX", h_net, + (unmatched_rows == 0 ? "MATCHED" : "EXCEPTION"), bizdate); + if (h_id < 0) { userlog("[RC_AUTOFIX] 대사 로그 적재 실패 pid=%ld", h_pid); FAIL(b); } + out_cnt = matched_rows > 0 ? matched_rows : 1; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_AUTOFIX] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_AUTOFIX scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_AUTOFIX] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_AUTOFIX] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_AUTOFIX] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_AUTOFIX] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_BACKDATE.pgc b/app/src/rc/svc/RC_BACKDATE.pgc index 9412f08..b66042e 100644 --- a/app/src/rc/svc/RC_BACKDATE.pgc +++ b/app/src/rc/svc/RC_BACKDATE.pgc @@ -1,33 +1,157 @@ /* - * RC_BACKDATE.pgc - rc 모듈 서비스 RC_BACKDATE (one service = one file). - * 소급 대사: 지정 매입을 과거 영업일자로 소급하여 대사 로그를 MATCHED 로 기록. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_BACKDATE.pgc - rc 모듈 서비스 RC_BACKDATE (대사 온라인 프로그램, 심화판). + * 업무: 소급 대사: 지정 매입을 과거 영업일자로 소급하여 대사 로그를 MATCHED 로 기록. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_BACKDATE.h" +#include "acq_common.h" -/* 110. RC_BACKDATE - 소급 대사: 지정 매입을 과거 영업일자로 소급하여 대사 로그를 MATCHED 로 기록. */ +/* RC_BACKDATE - 소급 대사: 지정 매입을 과거 영업일자로 소급하여 대사 로그를 MATCHED 로 기록. */ void RC_BACKDATE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_id, h_pid, h_amt; char h_asof[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "BACKDATE_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_BACKDATE"); + snprintf(h_msg, sizeof(h_msg), "[RC_BACKDATE] 소급 대사: 지정 매입을 과거 영업일자로 소급하여 대사 로그를 MATCHED 로 기록."); + strcpy(h_key, "-"); + userlog("[RC_BACKDATE] 대사 진입: 소급 대사: 지정 매입을 과거 영업일자로 소급하여 대사 로그를 MATCHED 로 기록."); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_BACKDATE] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); h_pid = getl(b, T_PURCHASE_ID); - { char c[16]; gets_(b, T_STR1, c, sizeof(c)); - strncpy(h_asof, c, sizeof(h_asof)-1); h_asof[sizeof(h_asof)-1] = 0; } - EXEC SQL SELECT coalesce(net,0) INTO :h_amt FROM purchase WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0 && sqlca.sqlcode != 100) FAIL(b); - EXEC SQL SELECT nextval('rc_match_seq') INTO :h_id; - EXEC SQL INSERT INTO rc_match_log (match_id, purchase_id, match_type, amount, status, biz_date) - VALUES (:h_id, :h_pid, 'BACKDATE', :h_amt, 'MATCHED', :h_asof); - if (sqlca.sqlcode < 0) FAIL(b); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_BACKDATE] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + gets_(b, T_STR1, tmpin, sizeof(tmpin)); + if (tmpin[0]) { strncpy(h_nbizdate, tmpin, sizeof(h_nbizdate)-1); h_nbizdate[sizeof(h_nbizdate)-1]=0; strncpy(h_lvl, tmpin, sizeof(h_lvl)-1); h_lvl[sizeof(h_lvl)-1]=0; } + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_BACKDATE] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_BACKDATE] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_BACKDATE] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_BACKDATE] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_BACKDATE] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_BACKDATE] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_BACKDATE] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_backdate CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_backdate; + for (;;) { + EXEC SQL FETCH cur_rc_backdate INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_backdate; userlog("[RC_BACKDATE] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_backdate; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_backdate; + userlog("[RC_BACKDATE] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_BACKDATE] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_BACKDATE] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_BACKDATE] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 대사 로그 적재 (rc_match_log) */ + h_id = rcdb_insert_match(h_pid, "BACKDATE", h_net, + (unmatched_rows == 0 ? "MATCHED" : "EXCEPTION"), bizdate); + if (h_id < 0) { userlog("[RC_BACKDATE] 대사 로그 적재 실패 pid=%ld", h_pid); FAIL(b); } + out_cnt = matched_rows > 0 ? matched_rows : 1; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_BACKDATE] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_BACKDATE scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_BACKDATE] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_BACKDATE] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_BACKDATE] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ setl(b, T_ID1, h_id); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_BACKDATE] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_BRANDDIFF.pgc b/app/src/rc/svc/RC_BRANDDIFF.pgc index b48e307..576564a 100644 --- a/app/src/rc/svc/RC_BRANDDIFF.pgc +++ b/app/src/rc/svc/RC_BRANDDIFF.pgc @@ -1,31 +1,134 @@ /* - * RC_BRANDDIFF.pgc - rc 모듈 서비스 RC_BRANDDIFF (one service = one file). - * 브랜드별 차이 분석: 업종(category) 단위 매입 총액과 수수료 합계 비율을 점검. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_BRANDDIFF.pgc - rc 모듈 서비스 RC_BRANDDIFF (대사 온라인 프로그램, 심화판). + * 업무: 브랜드별 차이 분석: 업종(category) 단위 매입 총액과 수수료 합계 비율을 점검. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_BRANDDIFF.h" +#include "acq_common.h" -/* 92. RC_BRANDDIFF - 브랜드별 차이 분석: 업종(category) 단위 매입 총액과 수수료 합계 비율을 점검. */ +/* RC_BRANDDIFF - 브랜드별 차이 분석: 업종(category) 단위 매입 총액과 수수료 합계 비율을 점검. */ void RC_BRANDDIFF(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_gross, h_fee; char h_cat[32]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "BRANDDIFF_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_BRANDDIFF"); + snprintf(h_msg, sizeof(h_msg), "[RC_BRANDDIFF] 브랜드별 차이 분석: 업종(category) 단위 매입 총액과 수수료 합계 비율을 점검."); + strcpy(h_key, "-"); + userlog("[RC_BRANDDIFF] 대사 진입: 브랜드별 차이 분석: 업종(category) 단위 매입 총액과 수수료 합계 비율을 점검."); - { char c[32]; gets_(b, T_CATEGORY, c, sizeof(c)); - strncpy(h_cat, c, sizeof(h_cat)-1); h_cat[sizeof(h_cat)-1] = 0; } - EXEC SQL SELECT coalesce(sum(amount),0), coalesce(sum(fee),0) INTO :h_gross, :h_fee - FROM purchase WHERE biz_date = :h_bizdate AND category = :h_cat; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_GROSS, h_gross); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_BRANDDIFF] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + gets_(b, T_CATEGORY, tmpin, sizeof(tmpin)); if (tmpin[0]) { strncpy(h_category, tmpin, sizeof(h_category)-1); h_category[sizeof(h_category)-1]=0; } + strcpy(h_key, h_category); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_BRANDDIFF] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_BRANDDIFF] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_BRANDDIFF] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_branddiff CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_branddiff; + for (;;) { + EXEC SQL FETCH cur_rc_branddiff INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_branddiff; userlog("[RC_BRANDDIFF] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_branddiff; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_branddiff; + userlog("[RC_BRANDDIFF] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_BRANDDIFF] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_BRANDDIFF] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_BRANDDIFF] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_BRANDDIFF] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_BRANDDIFF scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_BRANDDIFF] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_BRANDDIFF] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_BRANDDIFF] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ setl(b, T_FEE, h_fee); - setl(b, T_RC, (h_gross > 0) ? (h_fee * 10000 / h_gross) : 0); + setl(b, T_GROSS, h_gross); + setl(b, T_RC, rc_code); + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_BRANDDIFF] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_BRANDRECON.pgc b/app/src/rc/svc/RC_BRANDRECON.pgc index 4fd2750..23a3d5f 100644 --- a/app/src/rc/svc/RC_BRANDRECON.pgc +++ b/app/src/rc/svc/RC_BRANDRECON.pgc @@ -1,30 +1,134 @@ /* - * RC_BRANDRECON.pgc - rc 모듈 서비스 RC_BRANDRECON (one service = one file). - * 브랜드별 대사: 업종/브랜드(category) 단위로 당일 매입 총액과 건수를 집계. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_BRANDRECON.pgc - rc 모듈 서비스 RC_BRANDRECON (대사 온라인 프로그램, 심화판). + * 업무: 브랜드별 대사: 업종/브랜드(category) 단위로 당일 매입 총액과 건수를 집계. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_BRANDRECON.h" +#include "acq_common.h" -/* 86. RC_BRANDRECON - 브랜드별 대사: 업종/브랜드(category) 단위로 당일 매입 총액과 건수를 집계. */ +/* RC_BRANDRECON - 브랜드별 대사: 업종/브랜드(category) 단위로 당일 매입 총액과 건수를 집계. */ void RC_BRANDRECON(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_gross, h_cnt; char h_cat[32]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "BRANDRECON_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_BRANDRECON"); + snprintf(h_msg, sizeof(h_msg), "[RC_BRANDRECON] 브랜드별 대사: 업종/브랜드(category) 단위로 당일 매입 총액과 건수를 집계."); + strcpy(h_key, "-"); + userlog("[RC_BRANDRECON] 대사 진입: 브랜드별 대사: 업종/브랜드(category) 단위로 당일 매입 총액과 건수를 집계."); - { char c[32]; gets_(b, T_CATEGORY, c, sizeof(c)); - strncpy(h_cat, c, sizeof(h_cat)-1); h_cat[sizeof(h_cat)-1] = 0; } - EXEC SQL SELECT coalesce(sum(amount),0), count(*) INTO :h_gross, :h_cnt - FROM purchase WHERE biz_date = :h_bizdate AND category = :h_cat; - if (sqlca.sqlcode < 0) FAIL(b); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_BRANDRECON] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + gets_(b, T_CATEGORY, tmpin, sizeof(tmpin)); if (tmpin[0]) { strncpy(h_category, tmpin, sizeof(h_category)-1); h_category[sizeof(h_category)-1]=0; } + strcpy(h_key, h_category); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_BRANDRECON] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_BRANDRECON] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_BRANDRECON] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_brandrecon CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_brandrecon; + for (;;) { + EXEC SQL FETCH cur_rc_brandrecon INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_brandrecon; userlog("[RC_BRANDRECON] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_brandrecon; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_brandrecon; + userlog("[RC_BRANDRECON] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_BRANDRECON] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_BRANDRECON] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_BRANDRECON] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_BRANDRECON] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_BRANDRECON scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_BRANDRECON] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_BRANDRECON] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_BRANDRECON] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); setl(b, T_GROSS, h_gross); - setl(b, T_COUNT, h_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_BRANDRECON] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_BYCATEGORY.pgc b/app/src/rc/svc/RC_BYCATEGORY.pgc index a1aafae..570f8ae 100644 --- a/app/src/rc/svc/RC_BYCATEGORY.pgc +++ b/app/src/rc/svc/RC_BYCATEGORY.pgc @@ -1,43 +1,134 @@ /* - * RC_BYCATEGORY.pgc - rc 모듈 서비스 RC_BYCATEGORY (one service = one file). - * 업종(카테고리)별 대사 세분: 요청 업종의 당일 MATCHED 건수와 EXCEPTION - * 건수를 각각 집계하고 백분율 대사율을 계산하여 반환한다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_BYCATEGORY.pgc - rc 모듈 서비스 RC_BYCATEGORY (대사 온라인 프로그램, 심화판). + * 업무: 업종별 대사 집계 + 대사율. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_BYCATEGORY.h" +#include "acq_common.h" -/* 35. RC_BYCATEGORY - 업종별 대사 집계 + 대사율. */ +/* RC_BYCATEGORY - 업종별 대사 집계 + 대사율. */ void RC_BYCATEGORY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], category[16]; - long total, pct; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_mat, h_exc; - char h_bizdate[16], h_cat[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "BYCATEGORY_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_BYCATEGORY"); + snprintf(h_msg, sizeof(h_msg), "[RC_BYCATEGORY] 업종별 대사 집계 + 대사율."); + strcpy(h_key, "-"); + userlog("[RC_BYCATEGORY] 대사 진입: 업종별 대사 집계 + 대사율."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - gets_(b, T_CATEGORY, category, sizeof(category)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_BYCATEGORY] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - strncpy(h_cat, category, sizeof(h_cat)-1); h_cat[sizeof(h_cat)-1] = 0; + strcpy(h_key, bizdate); + gets_(b, T_CATEGORY, tmpin, sizeof(tmpin)); if (tmpin[0]) { strncpy(h_category, tmpin, sizeof(h_category)-1); h_category[sizeof(h_category)-1]=0; } + strcpy(h_key, h_category); - EXEC SQL SELECT count(*) INTO :h_mat - FROM rc_match_log m, purchase p - WHERE m.purchase_id = p.purchase_id AND m.biz_date = :h_bizdate - AND m.status = 'MATCHED' AND p.category = :h_cat; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_exc - FROM rc_match_log m, purchase p - WHERE m.purchase_id = p.purchase_id AND m.biz_date = :h_bizdate - AND m.status = 'EXCEPTION' AND p.category = :h_cat; - if (sqlca.sqlcode < 0) FAIL(b); + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_BYCATEGORY] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_BYCATEGORY] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_BYCATEGORY] 예외 집계 오류"); FAIL(b); } - total = h_mat + h_exc; - pct = (total > 0) ? (h_mat * 100) / total : 0; - setl(b, T_ARG1, h_mat); - setl(b, T_ARG2, h_exc); - setl(b, T_RC, pct); + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_bycategory CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_bycategory; + for (;;) { + EXEC SQL FETCH cur_rc_bycategory INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_bycategory; userlog("[RC_BYCATEGORY] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_bycategory; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_bycategory; + userlog("[RC_BYCATEGORY] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_BYCATEGORY] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_BYCATEGORY] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_BYCATEGORY] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_BYCATEGORY] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_BYCATEGORY scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_BYCATEGORY] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_BYCATEGORY] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_BYCATEGORY] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_ARG1, matched_rows); + setl(b, T_ARG2, unmatched_rows); + setl(b, T_RC, rc_code); + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_BYCATEGORY] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_BYCHANNEL.pgc b/app/src/rc/svc/RC_BYCHANNEL.pgc index 9f7a739..2309769 100644 --- a/app/src/rc/svc/RC_BYCHANNEL.pgc +++ b/app/src/rc/svc/RC_BYCHANNEL.pgc @@ -1,36 +1,139 @@ /* - * RC_BYCHANNEL.pgc - rc 모듈 서비스 RC_BYCHANNEL (one service = one file). - * 채널별 대사 세분 집계: 요청 채널(POS/EDC/EDI/FOREIGN)의 당일 MATCHED 대사 - * 건수/총액/수수료를 매입 조인으로 집계하여 반환한다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_BYCHANNEL.pgc - rc 모듈 서비스 RC_BYCHANNEL (대사 온라인 프로그램, 심화판). + * 업무: 채널별 대사 집계. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_BYCHANNEL.h" +#include "acq_common.h" -/* 34. RC_BYCHANNEL - 채널별 대사 집계. */ +/* RC_BYCHANNEL - 채널별 대사 집계. */ void RC_BYCHANNEL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], channel[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_amt, h_fee; - char h_bizdate[16], h_chan[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - gets_(b, T_CHANNEL, channel, sizeof(channel)); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - strncpy(h_chan, channel, sizeof(h_chan)-1); h_chan[sizeof(h_chan)-1] = 0; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "BYCHANNEL_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_BYCHANNEL"); + snprintf(h_msg, sizeof(h_msg), "[RC_BYCHANNEL] 채널별 대사 집계."); + strcpy(h_key, "-"); + userlog("[RC_BYCHANNEL] 대사 진입: 채널별 대사 집계."); - EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0) - INTO :h_cnt, :h_amt, :h_fee - FROM purchase p, rc_match_log m - WHERE m.purchase_id = p.purchase_id - AND m.biz_date = :h_bizdate AND m.status = 'MATCHED' - AND p.channel = :h_chan; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_amt); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_BYCHANNEL] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + gets_(b, T_CHANNEL, tmpin, sizeof(tmpin)); if (tmpin[0]) { strncpy(h_channel, tmpin, sizeof(h_channel)-1); h_channel[sizeof(h_channel)-1]=0; } + if (strcmp(h_channel,"POS") && strcmp(h_channel,"EDC") && strcmp(h_channel,"EDI") && strcmp(h_channel,"FOREIGN")) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "채널 코드 오류(POS/EDC/EDI/FOREIGN)", 0L); + userlog("[RC_BYCHANNEL] 채널 코드 오류 ch=[%s]", h_channel); FAIL(b); + } + strcpy(h_key, h_channel); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_BYCHANNEL] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_BYCHANNEL] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_BYCHANNEL] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_bychannel CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_bychannel; + for (;;) { + EXEC SQL FETCH cur_rc_bychannel INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_bychannel; userlog("[RC_BYCHANNEL] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_bychannel; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_bychannel; + userlog("[RC_BYCHANNEL] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_BYCHANNEL] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_BYCHANNEL] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_BYCHANNEL] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_BYCHANNEL] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_BYCHANNEL scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_BYCHANNEL] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_BYCHANNEL] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_BYCHANNEL] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); setl(b, T_FEE, h_fee); + setl(b, T_GROSS, h_gross); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_BYCHANNEL] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_BYDATE.pgc b/app/src/rc/svc/RC_BYDATE.pgc index 17debe6..2349a37 100644 --- a/app/src/rc/svc/RC_BYDATE.pgc +++ b/app/src/rc/svc/RC_BYDATE.pgc @@ -1,26 +1,132 @@ /* - * RC_BYDATE.pgc - rc 모듈 서비스 RC_BYDATE (one service = one file). - * Moved verbatim from the former inline rc_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by rc_svr at tpsvrinit. + * RC_BYDATE.pgc - rc 모듈 서비스 RC_BYDATE (대사 온라인 프로그램, 심화판). + * 업무: 일자별 대사(MATCHED) 집계. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_BYDATE.h" +#include "acq_common.h" -/* 16. RC_BYDATE - 일자별 대사(MATCHED) 집계. */ +/* RC_BYDATE - 일자별 대사(MATCHED) 집계. */ void RC_BYDATE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_sum; - char h_bizdate[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; + + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "BYDATE_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_BYDATE"); + snprintf(h_msg, sizeof(h_msg), "[RC_BYDATE] 일자별 대사(MATCHED) 집계."); + strcpy(h_key, "-"); + userlog("[RC_BYDATE] 대사 진입: 일자별 대사(MATCHED) 집계."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_BYDATE] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum - FROM rc_match_log WHERE biz_date = :h_bizdate AND status = 'MATCHED'; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_sum); + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_BYDATE] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_BYDATE] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_BYDATE] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_bydate CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_bydate; + for (;;) { + EXEC SQL FETCH cur_rc_bydate INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_bydate; userlog("[RC_BYDATE] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_bydate; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_bydate; + userlog("[RC_BYDATE] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_BYDATE] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_BYDATE] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_BYDATE] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_BYDATE] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_BYDATE scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_BYDATE] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_BYDATE] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_BYDATE] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + setl(b, T_GROSS, h_gross); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_BYDATE] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_BYISSUER.pgc b/app/src/rc/svc/RC_BYISSUER.pgc index d895b91..e498f0a 100644 --- a/app/src/rc/svc/RC_BYISSUER.pgc +++ b/app/src/rc/svc/RC_BYISSUER.pgc @@ -1,27 +1,133 @@ /* - * RC_BYISSUER.pgc - rc 모듈 서비스 RC_BYISSUER (one service = one file). - * Moved verbatim from the former inline rc_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by rc_svr at tpsvrinit. + * RC_BYISSUER.pgc - rc 모듈 서비스 RC_BYISSUER (대사 온라인 프로그램, 심화판). + * 업무: 발급사별 매입 건수 집계. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_BYISSUER.h" +#include "acq_common.h" -/* 15. RC_BYISSUER - 발급사별 매입 건수 집계. */ +/* RC_BYISSUER - 발급사별 매입 건수 집계. */ void RC_BYISSUER(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], issuer[32]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt; - char h_bizdate[16], h_issuer[32]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; + + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "BYISSUER_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_BYISSUER"); + snprintf(h_msg, sizeof(h_msg), "[RC_BYISSUER] 발급사별 매입 건수 집계."); + strcpy(h_key, "-"); + userlog("[RC_BYISSUER] 대사 진입: 발급사별 매입 건수 집계."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - gets_(b, T_ISSUER, issuer, sizeof(issuer)); if (issuer[0] == 0) strcpy(issuer, "BC"); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_BYISSUER] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - strncpy(h_issuer, issuer, sizeof(h_issuer)-1); h_issuer[sizeof(h_issuer)-1] = 0; - EXEC SQL SELECT count(*) INTO :h_cnt FROM purchase - WHERE biz_date = :h_bizdate AND issuer = :h_issuer; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); + strcpy(h_key, bizdate); + gets_(b, T_ISSUER, tmpin, sizeof(tmpin)); if (tmpin[0]) { strncpy(h_issuer, tmpin, sizeof(h_issuer)-1); h_issuer[sizeof(h_issuer)-1]=0; } + strcpy(h_key, h_issuer); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_BYISSUER] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_BYISSUER] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_BYISSUER] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_byissuer CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_byissuer; + for (;;) { + EXEC SQL FETCH cur_rc_byissuer INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_byissuer; userlog("[RC_BYISSUER] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_byissuer; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_byissuer; + userlog("[RC_BYISSUER] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_BYISSUER] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_BYISSUER] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_BYISSUER] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_BYISSUER] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_BYISSUER scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_BYISSUER] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_BYISSUER] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_BYISSUER] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_BYISSUER] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_BYMERCH.pgc b/app/src/rc/svc/RC_BYMERCH.pgc index 6860ae4..22229d4 100644 --- a/app/src/rc/svc/RC_BYMERCH.pgc +++ b/app/src/rc/svc/RC_BYMERCH.pgc @@ -1,27 +1,144 @@ /* - * RC_BYMERCH.pgc - rc 모듈 서비스 RC_BYMERCH (one service = one file). - * Moved verbatim from the former inline rc_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by rc_svr at tpsvrinit. + * RC_BYMERCH.pgc - rc 모듈 서비스 RC_BYMERCH (대사 온라인 프로그램, 심화판). + * 업무: 가맹점별 net 합계 조회. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_BYMERCH.h" +#include "acq_common.h" -/* 17. RC_BYMERCH - 가맹점별 net 합계 조회. */ +/* RC_BYMERCH - 가맹점별 net 합계 조회. */ void RC_BYMERCH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], merch[64]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_sum; - char h_bizdate[16], h_merch[64]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; + + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "BYMERCH_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_BYMERCH"); + snprintf(h_msg, sizeof(h_msg), "[RC_BYMERCH] 가맹점별 net 합계 조회."); + strcpy(h_key, "-"); + userlog("[RC_BYMERCH] 대사 진입: 가맹점별 net 합계 조회."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - gets_(b, T_MERCHANT, merch, sizeof(merch)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_BYMERCH] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; - EXEC SQL SELECT coalesce(sum(net),0) INTO :h_sum FROM purchase - WHERE biz_date = :h_bizdate AND merchant_id = :h_merch; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_GROSS, h_sum); + strcpy(h_key, bizdate); + gets_(b, T_MERCHANT, tmpin, sizeof(tmpin)); + if (tmpin[0] == 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "가맹점 ID 필수", 0L); + userlog("[RC_BYMERCH] 가맹점 ID 미입력"); FAIL(b); + } + strncpy(h_merch, tmpin, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strcpy(h_key, h_merch); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_BYMERCH] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_BYMERCH] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_BYMERCH] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_BYMERCH] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_BYMERCH] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_bymerch CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_bymerch; + for (;;) { + EXEC SQL FETCH cur_rc_bymerch INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_bymerch; userlog("[RC_BYMERCH] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_bymerch; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_bymerch; + userlog("[RC_BYMERCH] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_BYMERCH] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_BYMERCH] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_BYMERCH] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_BYMERCH] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_BYMERCH scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_BYMERCH] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_BYMERCH] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_BYMERCH] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_GROSS, h_gross); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_BYMERCH] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_BYMGROUP.pgc b/app/src/rc/svc/RC_BYMGROUP.pgc index 1ac9d53..3b6c0a2 100644 --- a/app/src/rc/svc/RC_BYMGROUP.pgc +++ b/app/src/rc/svc/RC_BYMGROUP.pgc @@ -1,34 +1,138 @@ /* - * RC_BYMGROUP.pgc - rc 모듈 서비스 RC_BYMGROUP (one service = one file). - * 가맹점그룹별 대사 세분: 가맹점 ID prefix (그룹 키) 로 묶어 당일 MATCHED - * 대사 건수와 금액 합계를 IN-서브쿼리로 집계한다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_BYMGROUP.pgc - rc 모듈 서비스 RC_BYMGROUP (대사 온라인 프로그램, 심화판). + * 업무: 가맹점그룹(prefix)별 대사 집계. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_BYMGROUP.h" +#include "acq_common.h" -/* 36. RC_BYMGROUP - 가맹점그룹(prefix)별 대사 집계. */ +/* RC_BYMGROUP - 가맹점그룹(prefix)별 대사 집계. */ void RC_BYMGROUP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], grp[32]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_sum; - char h_bizdate[16], h_like[40]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - gets_(b, T_KEY1, grp, sizeof(grp)); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - snprintf(h_like, sizeof(h_like), "%s%%", grp); + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "BYMGROUP_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_BYMGROUP"); + snprintf(h_msg, sizeof(h_msg), "[RC_BYMGROUP] 가맹점그룹(prefix)별 대사 집계."); + strcpy(h_key, "-"); + userlog("[RC_BYMGROUP] 대사 진입: 가맹점그룹(prefix)별 대사 집계."); - EXEC SQL SELECT count(*), coalesce(sum(m.amount),0) INTO :h_cnt, :h_sum - FROM rc_match_log m - WHERE m.biz_date = :h_bizdate AND m.status = 'MATCHED' - AND m.purchase_id IN - (SELECT purchase_id FROM purchase WHERE merchant_id LIKE :h_like); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_sum); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_BYMGROUP] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + gets_(b, T_KEY1, tmpin, sizeof(tmpin)); + if (tmpin[0] == 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "그룹 키 필수", 0L); + userlog("[RC_BYMGROUP] 그룹 키 미입력"); FAIL(b); + } + strncpy(h_key, tmpin, sizeof(h_key)-1); h_key[sizeof(h_key)-1] = 0; + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_BYMGROUP] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_BYMGROUP] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_BYMGROUP] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_bymgroup CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_bymgroup; + for (;;) { + EXEC SQL FETCH cur_rc_bymgroup INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_bymgroup; userlog("[RC_BYMGROUP] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_bymgroup; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_bymgroup; + userlog("[RC_BYMGROUP] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_BYMGROUP] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_BYMGROUP] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_BYMGROUP] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_BYMGROUP] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_BYMGROUP scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_BYMGROUP] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_BYMGROUP] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_BYMGROUP] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + setl(b, T_GROSS, h_gross); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_BYMGROUP] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_CANCELREC.pgc b/app/src/rc/svc/RC_CANCELREC.pgc index ee5f37a..3e1e7e4 100644 --- a/app/src/rc/svc/RC_CANCELREC.pgc +++ b/app/src/rc/svc/RC_CANCELREC.pgc @@ -1,37 +1,160 @@ /* - * RC_CANCELREC.pgc - rc 모듈 서비스 RC_CANCELREC (one service = one file). - * 취소분 대사: 매입 금액과 취소(ac_cancel) 누적 금액을 비교하여 전액 취소면 - * MATCHED, 부분 취소 잔량이 있으면 HOLD 로 대사 로그를 남긴다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_CANCELREC.pgc - rc 모듈 서비스 RC_CANCELREC (대사 온라인 프로그램, 심화판). + * 업무: 취소분 대사 (전액취소=MATCHED / 잔량=HOLD). + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_CANCELREC.h" +#include "acq_common.h" -/* 51. RC_CANCELREC - 취소분 대사 (전액취소=MATCHED / 잔량=HOLD). */ +/* RC_CANCELREC - 취소분 대사 (전액취소=MATCHED / 잔량=HOLD). */ void RC_CANCELREC(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long mid; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_amt, h_csum, h_ccnt; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "CANCELREC_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_CANCELREC"); + snprintf(h_msg, sizeof(h_msg), "[RC_CANCELREC] 취소분 대사 (전액취소=MATCHED / 잔량=HOLD)."); + strcpy(h_key, "-"); + userlog("[RC_CANCELREC] 대사 진입: 취소분 대사 (전액취소=MATCHED / 잔량=HOLD)."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_CANCELREC] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_CANCELREC] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } - EXEC SQL SELECT amount INTO :h_amt FROM purchase WHERE purchase_id = :h_pid; - if (sqlca.sqlcode != 0) FAIL(b); - EXEC SQL SELECT coalesce(sum(amount),0), count(*) INTO :h_csum, :h_ccnt - FROM ac_cancel WHERE purchase_id = :h_pid; + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_CANCELREC] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_CANCELREC] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_CANCELREC] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_CANCELREC] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_CANCELREC] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_CANCELREC] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_CANCELREC] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_cancelrec CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_cancelrec; + for (;;) { + EXEC SQL FETCH cur_rc_cancelrec INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_cancelrec; userlog("[RC_CANCELREC] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_cancelrec; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_cancelrec; + userlog("[RC_CANCELREC] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_CANCELREC] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_CANCELREC] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_CANCELREC] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 차이 라인 적재 (rc_diff) */ + h_exp = h_net; h_act = h_led; h_diff = h_exp - h_act; + EXEC SQL SELECT nextval('rc_diff_seq') INTO :h_id; if (sqlca.sqlcode < 0) FAIL(b); - if (h_ccnt == 0) FAIL(b); + EXEC SQL INSERT INTO rc_diff (diff_id, purchase_id, expected_amt, actual_amt, diff_amt, diff_kind, biz_date) + VALUES (:h_id, :h_pid, :h_exp, :h_act, :h_diff, :h_dtype, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_CANCELREC] 차이 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + out_cnt = (h_diff != 0 ? 1 : 0); - mid = rcdb_insert_match(h_pid, "CANCEL", h_csum, - (h_csum >= h_amt) ? "MATCHED" : "HOLD", bizdate); - if (mid < 0) FAIL(b); - setl(b, T_AMT1, h_amt); - setl(b, T_AMT2, h_csum); - setl(b, T_ID1, mid); + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_CANCELREC] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_CANCELREC scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_CANCELREC] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_CANCELREC] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_CANCELREC] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_AMT1, h_gross); + setl(b, T_AMT2, h_net); + setl(b, T_ID1, h_id); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_CANCELREC] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_CANCMATCH.pgc b/app/src/rc/svc/RC_CANCMATCH.pgc index 364f677..36bf67c 100644 --- a/app/src/rc/svc/RC_CANCMATCH.pgc +++ b/app/src/rc/svc/RC_CANCMATCH.pgc @@ -1,28 +1,132 @@ /* - * RC_CANCMATCH.pgc - rc 모듈 서비스 RC_CANCMATCH (one service = one file). - * 취소분 대사: 당일 취소(ac_cancel) 금액 합계와 취소 건수를 유형 무관하게 집계. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_CANCMATCH.pgc - rc 모듈 서비스 RC_CANCMATCH (대사 온라인 프로그램, 심화판). + * 업무: 취소분 대사: 당일 취소(ac_cancel) 금액 합계와 취소 건수를 유형 무관하게 집계. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_CANCMATCH.h" +#include "acq_common.h" -/* 122. RC_CANCMATCH - 취소분 대사: 당일 취소(ac_cancel) 금액 합계와 취소 건수를 유형 무관하게 집계. */ +/* RC_CANCMATCH - 취소분 대사: 당일 취소(ac_cancel) 금액 합계와 취소 건수를 유형 무관하게 집계. */ void RC_CANCMATCH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_cnt, h_amt; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "CANCMATCH_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_CANCMATCH"); + snprintf(h_msg, sizeof(h_msg), "[RC_CANCMATCH] 취소분 대사: 당일 취소(ac_cancel) 금액 합계와 취소 건수를 유형 무관하게 집계."); + strcpy(h_key, "-"); + userlog("[RC_CANCMATCH] 대사 진입: 취소분 대사: 당일 취소(ac_cancel) 금액 합계와 취소 건수를 유형 무관하게 집계."); - EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_amt - FROM ac_cancel WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_CANCMATCH] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_CANCMATCH] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_CANCMATCH] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_CANCMATCH] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_cancmatch CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_cancmatch; + for (;;) { + EXEC SQL FETCH cur_rc_cancmatch INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_cancmatch; userlog("[RC_CANCMATCH] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_cancmatch; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_cancmatch; + userlog("[RC_CANCMATCH] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_CANCMATCH] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_CANCMATCH] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_CANCMATCH] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_CANCMATCH] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_CANCMATCH scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_CANCMATCH] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_CANCMATCH] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_CANCMATCH] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ setl(b, T_AMOUNT, h_amt); + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_CANCMATCH] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_CARRYFWD.pgc b/app/src/rc/svc/RC_CARRYFWD.pgc index 1ed327b..6dcfd2b 100644 --- a/app/src/rc/svc/RC_CARRYFWD.pgc +++ b/app/src/rc/svc/RC_CARRYFWD.pgc @@ -1,42 +1,137 @@ /* - * RC_CARRYFWD.pgc - rc 모듈 서비스 RC_CARRYFWD (one service = one file). - * 익일 이월 처리: 당일 미해결 예외를 지정 익일자로 복제 등록하여 이월시킨다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_CARRYFWD.pgc - rc 모듈 서비스 RC_CARRYFWD (대사 온라인 프로그램, 심화판). + * 업무: 익일 이월 처리: 당일 미해결 예외를 지정 익일자로 복제 등록하여 이월시킨다. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_CARRYFWD.h" +#include "acq_common.h" -/* 109. RC_CARRYFWD - 익일 이월 처리: 당일 미해결 예외를 지정 익일자로 복제 등록하여 이월시킨다. */ +/* RC_CARRYFWD - 익일 이월 처리: 당일 미해결 예외를 지정 익일자로 복제 등록하여 이월시킨다. */ void RC_CARRYFWD(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_id, h_pid, h_cnt = 0; char h_next[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "CARRYFWD_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_CARRYFWD"); + snprintf(h_msg, sizeof(h_msg), "[RC_CARRYFWD] 익일 이월 처리: 당일 미해결 예외를 지정 익일자로 복제 등록하여 이월시킨다."); + strcpy(h_key, "-"); + userlog("[RC_CARRYFWD] 대사 진입: 익일 이월 처리: 당일 미해결 예외를 지정 익일자로 복제 등록하여 이월시킨다."); - { char c[16]; gets_(b, T_STR1, c, sizeof(c)); - strncpy(h_next, c, sizeof(h_next)-1); h_next[sizeof(h_next)-1] = 0; } - EXEC SQL DECLARE cur_fwd CURSOR FOR - SELECT purchase_id FROM rc_exception - WHERE biz_date = :h_bizdate AND resolved = false; - EXEC SQL OPEN cur_fwd; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH cur_fwd INTO :h_pid; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_fwd; FAIL(b); } - EXEC SQL SELECT nextval('rc_exc_seq') INTO :h_id; - EXEC SQL INSERT INTO rc_exception (exc_id, purchase_id, reason, biz_date) - VALUES (:h_id, :h_pid, 'CARRY_FWD', :h_next); - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_fwd; FAIL(b); } - h_cnt++; + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_CARRYFWD] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); } - EXEC SQL CLOSE cur_fwd; - setl(b, T_COUNT, h_cnt); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + gets_(b, T_STR1, tmpin, sizeof(tmpin)); + if (tmpin[0]) { strncpy(h_nbizdate, tmpin, sizeof(h_nbizdate)-1); h_nbizdate[sizeof(h_nbizdate)-1]=0; strncpy(h_lvl, tmpin, sizeof(h_lvl)-1); h_lvl[sizeof(h_lvl)-1]=0; } + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_CARRYFWD] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_CARRYFWD] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_CARRYFWD] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_carryfwd CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_carryfwd; + for (;;) { + EXEC SQL FETCH cur_rc_carryfwd INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_carryfwd; userlog("[RC_CARRYFWD] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_carryfwd; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_carryfwd; + userlog("[RC_CARRYFWD] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_CARRYFWD] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_CARRYFWD] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_CARRYFWD] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 예외 적재 (rc_exception) */ + if (h_ecnt >= 0) { + h_id = rcdb_insert_exception(h_pid, h_reason, bizdate); + if (h_id < 0) { userlog("[RC_CARRYFWD] 예외 적재 실패 pid=%ld", h_pid); FAIL(b); } + } + out_cnt = h_ecnt + (h_pid > 0 ? 1 : 0); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_CARRYFWD] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_CARRYFWD scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_CARRYFWD] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_CARRYFWD] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_CARRYFWD] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_CARRYFWD] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_CARRYIN.pgc b/app/src/rc/svc/RC_CARRYIN.pgc index 3c16b80..76d02e9 100644 --- a/app/src/rc/svc/RC_CARRYIN.pgc +++ b/app/src/rc/svc/RC_CARRYIN.pgc @@ -1,29 +1,137 @@ /* - * RC_CARRYIN.pgc - rc 모듈 서비스 RC_CARRYIN (one service = one file). - * 전일 이월 대사: 전 영업일의 미해결 예외 건수를 조회해 당일 이월 대상으로 반환. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_CARRYIN.pgc - rc 모듈 서비스 RC_CARRYIN (대사 온라인 프로그램, 심화판). + * 업무: 전일 이월 대사: 전 영업일의 미해결 예외 건수를 조회해 당일 이월 대상으로 반환. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_CARRYIN.h" +#include "acq_common.h" -/* 108. RC_CARRYIN - 전일 이월 대사: 전 영업일의 미해결 예외 건수를 조회해 당일 이월 대상으로 반환. */ +/* RC_CARRYIN - 전일 이월 대사: 전 영업일의 미해결 예외 건수를 조회해 당일 이월 대상으로 반환. */ void RC_CARRYIN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_cnt; char h_prev[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "CARRYIN_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_CARRYIN"); + snprintf(h_msg, sizeof(h_msg), "[RC_CARRYIN] 전일 이월 대사: 전 영업일의 미해결 예외 건수를 조회해 당일 이월 대상으로 반환."); + strcpy(h_key, "-"); + userlog("[RC_CARRYIN] 대사 진입: 전일 이월 대사: 전 영업일의 미해결 예외 건수를 조회해 당일 이월 대상으로 반환."); - { char c[16]; gets_(b, T_STR1, c, sizeof(c)); - strncpy(h_prev, c, sizeof(h_prev)-1); h_prev[sizeof(h_prev)-1] = 0; } - EXEC SQL SELECT count(*) INTO :h_cnt FROM rc_exception - WHERE biz_date = :h_prev AND resolved = false; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_CARRYIN] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + gets_(b, T_STR1, tmpin, sizeof(tmpin)); + if (tmpin[0]) { strncpy(h_nbizdate, tmpin, sizeof(h_nbizdate)-1); h_nbizdate[sizeof(h_nbizdate)-1]=0; strncpy(h_lvl, tmpin, sizeof(h_lvl)-1); h_lvl[sizeof(h_lvl)-1]=0; } + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_CARRYIN] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_CARRYIN] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_CARRYIN] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_carryin CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_carryin; + for (;;) { + EXEC SQL FETCH cur_rc_carryin INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_carryin; userlog("[RC_CARRYIN] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_carryin; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_carryin; + userlog("[RC_CARRYIN] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_CARRYIN] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_CARRYIN] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_CARRYIN] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 예외 적재 (rc_exception) */ + if (h_ecnt >= 0) { + h_id = rcdb_insert_exception(h_pid, h_reason, bizdate); + if (h_id < 0) { userlog("[RC_CARRYIN] 예외 적재 실패 pid=%ld", h_pid); FAIL(b); } + } + out_cnt = h_ecnt + (h_pid > 0 ? 1 : 0); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_CARRYIN] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_CARRYIN scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_CARRYIN] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_CARRYIN] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_CARRYIN] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_CARRYIN] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_CARRYOVER.pgc b/app/src/rc/svc/RC_CARRYOVER.pgc index 772433f..eac13c2 100644 --- a/app/src/rc/svc/RC_CARRYOVER.pgc +++ b/app/src/rc/svc/RC_CARRYOVER.pgc @@ -1,35 +1,137 @@ /* - * RC_CARRYOVER.pgc - rc 모듈 서비스 RC_CARRYOVER (one service = one file). - * 이월 대사: 이전 영업일의 EXCEPTION 대사 건들을 다음 영업일 HOLD 건으로 - * 일괄 복제(INSERT..SELECT)하여 익일 대사 큐로 이월시킨다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_CARRYOVER.pgc - rc 모듈 서비스 RC_CARRYOVER (대사 온라인 프로그램, 심화판). + * 업무: EXCEPTION 건 익일 이월 (INSERT..SELECT). + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_CARRYOVER.h" +#include "acq_common.h" -/* 44. RC_CARRYOVER - EXCEPTION 건 익일 이월 (INSERT..SELECT). */ +/* RC_CARRYOVER - EXCEPTION 건 익일 이월 (INSERT..SELECT). */ void RC_CARRYOVER(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char fromdate[16], todate[16]; - long rows; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_from[16], h_to[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; - gets_(b, T_STR1, fromdate, sizeof(fromdate)); - gets_(b, T_BIZDATE, todate, sizeof(todate)); - strncpy(h_from, fromdate, sizeof(h_from)-1); h_from[sizeof(h_from)-1] = 0; - strncpy(h_to, todate, sizeof(h_to)-1); h_to[sizeof(h_to)-1] = 0; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "CARRYOVER_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_CARRYOVER"); + snprintf(h_msg, sizeof(h_msg), "[RC_CARRYOVER] EXCEPTION 건 익일 이월 (INSERT..SELECT)."); + strcpy(h_key, "-"); + userlog("[RC_CARRYOVER] 대사 진입: EXCEPTION 건 익일 이월 (INSERT..SELECT)."); - EXEC SQL INSERT INTO rc_match_log - (match_id, purchase_id, match_type, amount, status, biz_date) - SELECT nextval('rc_match_seq'), purchase_id, 'CARRY', amount, 'HOLD', - CAST(:h_to AS date) - FROM rc_match_log - WHERE biz_date = :h_from AND status = 'EXCEPTION'; - if (sqlca.sqlcode < 0) FAIL(b); - rows = sqlca.sqlerrd[2]; - setl(b, T_COUNT, rows); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_CARRYOVER] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + gets_(b, T_STR1, tmpin, sizeof(tmpin)); + if (tmpin[0]) { strncpy(h_nbizdate, tmpin, sizeof(h_nbizdate)-1); h_nbizdate[sizeof(h_nbizdate)-1]=0; strncpy(h_lvl, tmpin, sizeof(h_lvl)-1); h_lvl[sizeof(h_lvl)-1]=0; } + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_CARRYOVER] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_CARRYOVER] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_CARRYOVER] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_carryover CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_carryover; + for (;;) { + EXEC SQL FETCH cur_rc_carryover INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_carryover; userlog("[RC_CARRYOVER] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_carryover; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_carryover; + userlog("[RC_CARRYOVER] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_CARRYOVER] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_CARRYOVER] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_CARRYOVER] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 예외 적재 (rc_exception) */ + if (h_ecnt >= 0) { + h_id = rcdb_insert_exception(h_pid, h_reason, bizdate); + if (h_id < 0) { userlog("[RC_CARRYOVER] 예외 적재 실패 pid=%ld", h_pid); FAIL(b); } + } + out_cnt = h_ecnt + (h_pid > 0 ? 1 : 0); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_CARRYOVER] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_CARRYOVER scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_CARRYOVER] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_CARRYOVER] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_CARRYOVER] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_CARRYOVER] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_CHANDIFF.pgc b/app/src/rc/svc/RC_CHANDIFF.pgc index 2b240be..4c90b49 100644 --- a/app/src/rc/svc/RC_CHANDIFF.pgc +++ b/app/src/rc/svc/RC_CHANDIFF.pgc @@ -1,37 +1,143 @@ /* - * RC_CHANDIFF.pgc - rc 모듈 서비스 RC_CHANDIFF (one service = one file). - * 채널별 차이 분석: 지정 채널의 매입 순액과 원장 SETTLE 합계 차이를 rc_diff 에 기록. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_CHANDIFF.pgc - rc 모듈 서비스 RC_CHANDIFF (대사 온라인 프로그램, 심화판). + * 업무: 채널별 차이 분석: 지정 채널의 매입 순액과 원장 SETTLE 합계 차이를 rc_diff 에 기록. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_CHANDIFF.h" +#include "acq_common.h" -/* 90. RC_CHANDIFF - 채널별 차이 분석: 지정 채널의 매입 순액과 원장 SETTLE 합계 차이를 rc_diff 에 기록. */ +/* RC_CHANDIFF - 채널별 차이 분석: 지정 채널의 매입 순액과 원장 SETTLE 합계 차이를 rc_diff 에 기록. */ void RC_CHANDIFF(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_id, h_exp, h_act; char h_chan[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "CHANDIFF_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_CHANDIFF"); + snprintf(h_msg, sizeof(h_msg), "[RC_CHANDIFF] 채널별 차이 분석: 지정 채널의 매입 순액과 원장 SETTLE 합계 차이를 rc_diff 에 기록."); + strcpy(h_key, "-"); + userlog("[RC_CHANDIFF] 대사 진입: 채널별 차이 분석: 지정 채널의 매입 순액과 원장 SETTLE 합계 차이를 rc_diff 에 기록."); - { char c[16]; gets_(b, T_CHANNEL, c, sizeof(c)); - strncpy(h_chan, c, sizeof(h_chan)-1); h_chan[sizeof(h_chan)-1] = 0; } - EXEC SQL SELECT coalesce(sum(net),0) INTO :h_exp FROM purchase - WHERE biz_date = :h_bizdate AND channel = :h_chan; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT coalesce(sum(l.amount),0) INTO :h_act FROM ledger l - JOIN purchase p ON p.purchase_id = l.purchase_id - WHERE l.biz_date = :h_bizdate AND l.entry_type = 'SETTLE' AND p.channel = :h_chan; - if (sqlca.sqlcode < 0) FAIL(b); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_CHANDIFF] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + gets_(b, T_CHANNEL, tmpin, sizeof(tmpin)); if (tmpin[0]) { strncpy(h_channel, tmpin, sizeof(h_channel)-1); h_channel[sizeof(h_channel)-1]=0; } + if (strcmp(h_channel,"POS") && strcmp(h_channel,"EDC") && strcmp(h_channel,"EDI") && strcmp(h_channel,"FOREIGN")) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "채널 코드 오류(POS/EDC/EDI/FOREIGN)", 0L); + userlog("[RC_CHANDIFF] 채널 코드 오류 ch=[%s]", h_channel); FAIL(b); + } + strcpy(h_key, h_channel); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_CHANDIFF] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_CHANDIFF] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_CHANDIFF] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_chandiff CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_chandiff; + for (;;) { + EXEC SQL FETCH cur_rc_chandiff INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_chandiff; userlog("[RC_CHANDIFF] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_chandiff; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_chandiff; + userlog("[RC_CHANDIFF] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_CHANDIFF] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_CHANDIFF] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_CHANDIFF] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 차이 라인 적재 (rc_diff) */ + h_exp = h_net; h_act = h_led; h_diff = h_exp - h_act; EXEC SQL SELECT nextval('rc_diff_seq') INTO :h_id; - EXEC SQL INSERT INTO rc_diff (diff_id, expected_amt, actual_amt, diff_amt, diff_kind, biz_date) - VALUES (:h_id, :h_exp, :h_act, :h_exp - :h_act, 'AMT', :h_bizdate); if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_DELTA, h_exp - h_act); + EXEC SQL INSERT INTO rc_diff (diff_id, purchase_id, expected_amt, actual_amt, diff_amt, diff_kind, biz_date) + VALUES (:h_id, :h_pid, :h_exp, :h_act, :h_diff, :h_dtype, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_CHANDIFF] 차이 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + out_cnt = (h_diff != 0 ? 1 : 0); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_CHANDIFF] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_CHANDIFF scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_CHANDIFF] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_CHANDIFF] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_CHANDIFF] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_DELTA, h_diff); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_CHANDIFF] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_CHANRECON.pgc b/app/src/rc/svc/RC_CHANRECON.pgc index 9f1309e..2e34c83 100644 --- a/app/src/rc/svc/RC_CHANRECON.pgc +++ b/app/src/rc/svc/RC_CHANRECON.pgc @@ -1,33 +1,138 @@ /* - * RC_CHANRECON.pgc - rc 모듈 서비스 RC_CHANRECON (one service = one file). - * 채널별 대사: 지정 채널의 당일 매입 건수와 대사완료(M/S) 건수를 대조. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_CHANRECON.pgc - rc 모듈 서비스 RC_CHANRECON (대사 온라인 프로그램, 심화판). + * 업무: 채널별 대사: 지정 채널의 당일 매입 건수와 대사완료(M/S) 건수를 대조. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_CHANRECON.h" +#include "acq_common.h" -/* 84. RC_CHANRECON - 채널별 대사: 지정 채널의 당일 매입 건수와 대사완료(M/S) 건수를 대조. */ +/* RC_CHANRECON - 채널별 대사: 지정 채널의 당일 매입 건수와 대사완료(M/S) 건수를 대조. */ void RC_CHANRECON(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_total, h_done; char h_chan[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "CHANRECON_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_CHANRECON"); + snprintf(h_msg, sizeof(h_msg), "[RC_CHANRECON] 채널별 대사: 지정 채널의 당일 매입 건수와 대사완료(M/S) 건수를 대조."); + strcpy(h_key, "-"); + userlog("[RC_CHANRECON] 대사 진입: 채널별 대사: 지정 채널의 당일 매입 건수와 대사완료(M/S) 건수를 대조."); - { char c[16]; gets_(b, T_CHANNEL, c, sizeof(c)); - strncpy(h_chan, c, sizeof(h_chan)-1); h_chan[sizeof(h_chan)-1] = 0; } - EXEC SQL SELECT count(*) INTO :h_total FROM purchase - WHERE biz_date = :h_bizdate AND channel = :h_chan; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_done FROM purchase - WHERE biz_date = :h_bizdate AND channel = :h_chan AND status IN ('M','S'); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_total); - setl(b, T_ARG1, h_done); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_CHANRECON] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + gets_(b, T_CHANNEL, tmpin, sizeof(tmpin)); if (tmpin[0]) { strncpy(h_channel, tmpin, sizeof(h_channel)-1); h_channel[sizeof(h_channel)-1]=0; } + if (strcmp(h_channel,"POS") && strcmp(h_channel,"EDC") && strcmp(h_channel,"EDI") && strcmp(h_channel,"FOREIGN")) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "채널 코드 오류(POS/EDC/EDI/FOREIGN)", 0L); + userlog("[RC_CHANRECON] 채널 코드 오류 ch=[%s]", h_channel); FAIL(b); + } + strcpy(h_key, h_channel); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_CHANRECON] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_CHANRECON] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_CHANRECON] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_chanrecon CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_chanrecon; + for (;;) { + EXEC SQL FETCH cur_rc_chanrecon INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_chanrecon; userlog("[RC_CHANRECON] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_chanrecon; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_chanrecon; + userlog("[RC_CHANRECON] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_CHANRECON] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_CHANRECON] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_CHANRECON] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_CHANRECON] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_CHANRECON scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_CHANRECON] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_CHANRECON] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_CHANRECON] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_ARG1, matched_rows); + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_CHANRECON] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_CLOSEDAY.pgc b/app/src/rc/svc/RC_CLOSEDAY.pgc index 170fedf..d36b4c5 100644 --- a/app/src/rc/svc/RC_CLOSEDAY.pgc +++ b/app/src/rc/svc/RC_CLOSEDAY.pgc @@ -1,53 +1,137 @@ /* - * RC_CLOSEDAY.pgc - rc 모듈 서비스 RC_CLOSEDAY (one service = one file). - * 일마감 대사 확정: 미해결 예외나 보류가 남아 있으면 마감을 거부하고, 모두 - * 정리된 경우 요약을 확정 upsert 한 뒤 당일 MATCHED 건을 CONFIRMED 로 전환. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_CLOSEDAY.pgc - rc 모듈 서비스 RC_CLOSEDAY (대사 온라인 프로그램, 심화판). + * 업무: 일마감 확정 (예외/보류 잔량 시 거부). + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_CLOSEDAY.h" +#include "acq_common.h" -/* 73. RC_CLOSEDAY - 일마감 확정 (예외/보류 잔량 시 거부). */ +/* RC_CLOSEDAY - 일마감 확정 (예외/보류 잔량 시 거부). */ void RC_CLOSEDAY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long matched, unmatched; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_open, h_hold, h_gross; - char h_bizdate[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "CLOSEDAY_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_CLOSEDAY"); + snprintf(h_msg, sizeof(h_msg), "[RC_CLOSEDAY] 일마감 확정 (예외/보류 잔량 시 거부)."); + strcpy(h_key, "-"); + userlog("[RC_CLOSEDAY] 대사 진입: 일마감 확정 (예외/보류 잔량 시 거부)."); + + /* [1] 입력 검증부 -------------------------------------------------- */ 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_open FROM rc_exception - WHERE biz_date = :h_bizdate AND resolved = false; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_hold FROM rc_match_log - WHERE biz_date = :h_bizdate AND status = 'HOLD'; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_open > 0 || h_hold > 0) { - setl(b, T_ARG1, h_open); - setl(b, T_ARG2, h_hold); - setl(b, T_RC, 1); - FAIL(b); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_CLOSEDAY] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); - matched = rcdb_count_match(bizdate, "MATCHED"); - if (matched < 0) FAIL(b); - unmatched = rcdb_count_unmatched(bizdate); - if (unmatched < 0) FAIL(b); - EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_gross FROM rc_match_log + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_CLOSEDAY] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log WHERE biz_date = :h_bizdate AND status = 'MATCHED'; - if (sqlca.sqlcode < 0) FAIL(b); - if (rcdb_upsert_summary(bizdate, matched, unmatched, h_gross) < 0) FAIL(b); + if (sqlca.sqlcode < 0) { userlog("[RC_CLOSEDAY] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_CLOSEDAY] 예외 집계 오류"); FAIL(b); } - EXEC SQL UPDATE rc_match_log SET status = 'CONFIRMED', updated_at = now() - WHERE biz_date = :h_bizdate AND status = 'MATCHED'; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, sqlca.sqlerrd[2]); + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_closeday CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_closeday; + for (;;) { + EXEC SQL FETCH cur_rc_closeday INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_closeday; userlog("[RC_CLOSEDAY] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_closeday; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_closeday; + userlog("[RC_CLOSEDAY] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_CLOSEDAY] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_CLOSEDAY] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_CLOSEDAY] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 요약 upsert (rc_summary) */ + if (rcdb_upsert_summary(bizdate, matched_rows + h_mcnt, unmatched_rows + h_ecnt, h_gross) < 0) { + userlog("[RC_CLOSEDAY] 요약 upsert 실패"); FAIL(b); + } + out_cnt = matched_rows + h_mcnt; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_CLOSEDAY] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_CLOSEDAY scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_CLOSEDAY] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_CLOSEDAY] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_CLOSEDAY] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_ARG1, matched_rows); + setl(b, T_ARG2, unmatched_rows); + setl(b, T_COUNT, out_cnt); setl(b, T_GROSS, h_gross); - setl(b, T_RC, 0); + setl(b, T_RC, rc_code); + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_CLOSEDAY] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_CNTDIFF.pgc b/app/src/rc/svc/RC_CNTDIFF.pgc index 79eb3d4..6fe4099 100644 --- a/app/src/rc/svc/RC_CNTDIFF.pgc +++ b/app/src/rc/svc/RC_CNTDIFF.pgc @@ -1,33 +1,137 @@ /* - * RC_CNTDIFF.pgc - rc 모듈 서비스 RC_CNTDIFF (one service = one file). - * Moved verbatim from the former inline rc_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by rc_svr at tpsvrinit. + * RC_CNTDIFF.pgc - rc 모듈 서비스 RC_CNTDIFF (대사 온라인 프로그램, 심화판). + * 업무: 건수 차이 대사: 매입 건수 vs 대사 로그 건수. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_CNTDIFF.h" +#include "acq_common.h" -/* 6. RC_CNTDIFF - 건수 차이 대사: 매입 건수 vs 대사 로그 건수. */ +/* RC_CNTDIFF - 건수 차이 대사: 매입 건수 vs 대사 로그 건수. */ void RC_CNTDIFF(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_id, h_pcnt, h_mcnt, h_diff; - char h_bizdate[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "CNTDIFF_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_CNTDIFF"); + snprintf(h_msg, sizeof(h_msg), "[RC_CNTDIFF] 건수 차이 대사: 매입 건수 vs 대사 로그 건수."); + strcpy(h_key, "-"); + userlog("[RC_CNTDIFF] 대사 진입: 건수 차이 대사: 매입 건수 vs 대사 로그 건수."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_CNTDIFF] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT count(*) INTO :h_pcnt FROM purchase WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - h_diff = h_pcnt - h_mcnt; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_CNTDIFF] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_CNTDIFF] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_CNTDIFF] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_cntdiff CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_cntdiff; + for (;;) { + EXEC SQL FETCH cur_rc_cntdiff INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_cntdiff; userlog("[RC_CNTDIFF] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_cntdiff; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_cntdiff; + userlog("[RC_CNTDIFF] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_CNTDIFF] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_CNTDIFF] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_CNTDIFF] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 차이 라인 적재 (rc_diff) */ + h_exp = h_net; h_act = h_led; h_diff = h_exp - h_act; EXEC SQL SELECT nextval('rc_diff_seq') INTO :h_id; if (sqlca.sqlcode < 0) FAIL(b); EXEC SQL INSERT INTO rc_diff (diff_id, purchase_id, expected_amt, actual_amt, diff_amt, diff_kind, biz_date) - VALUES (:h_id, 0, :h_pcnt, :h_mcnt, :h_diff, 'CNT', :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_diff); + VALUES (:h_id, :h_pid, :h_exp, :h_act, :h_diff, :h_dtype, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_CNTDIFF] 차이 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + out_cnt = (h_diff != 0 ? 1 : 0); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_CNTDIFF] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_CNTDIFF scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_CNTDIFF] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_CNTDIFF] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_CNTDIFF] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_CNTDIFF] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_CONFIRM.pgc b/app/src/rc/svc/RC_CONFIRM.pgc index ca26d99..fa0debc 100644 --- a/app/src/rc/svc/RC_CONFIRM.pgc +++ b/app/src/rc/svc/RC_CONFIRM.pgc @@ -1,17 +1,137 @@ /* - * RC_CONFIRM.pgc - rc 모듈 서비스 RC_CONFIRM (one service = one file). - * Moved verbatim from the former inline rc_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by rc_svr at tpsvrinit. + * RC_CONFIRM.pgc - rc 모듈 서비스 RC_CONFIRM (대사 온라인 프로그램, 심화판). + * 업무: 대사 확정 (MATCHED -> CONFIRMED). + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_CONFIRM.h" +#include "acq_common.h" -/* 29. RC_CONFIRM - 대사 확정 (MATCHED -> CONFIRMED). */ +/* RC_CONFIRM - 대사 확정 (MATCHED -> CONFIRMED). */ void RC_CONFIRM(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long pid = getl(b, T_PURCHASE_ID); - if (rcdb_update_match_status(pid, "MATCHED", "CONFIRMED") < 0) FAIL(b); - Bchg(b, T_STATUS, 0, "C", 0L); + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; + EXEC SQL END DECLARE SECTION; + + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "CONFIRM_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_CONFIRM"); + snprintf(h_msg, sizeof(h_msg), "[RC_CONFIRM] 대사 확정 (MATCHED -> CONFIRMED)."); + strcpy(h_key, "-"); + userlog("[RC_CONFIRM] 대사 진입: 대사 확정 (MATCHED -> CONFIRMED)."); + + /* [1] 입력 검증부 -------------------------------------------------- */ + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_CONFIRM] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + /* 조회 기준일 미입력 서비스: 매입 원장에서 영업일 유도 */ + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_CONFIRM] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_CONFIRM] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_CONFIRM] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_CONFIRM] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { userlog("[RC_CONFIRM] 대사 로그 조회 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_led FROM ledger WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { userlog("[RC_CONFIRM] 원장 조회 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_confirm CURSOR FOR + SELECT match_id, amount, status FROM rc_match_log + WHERE purchase_id = :h_pid ORDER BY match_id; + EXEC SQL OPEN cur_rc_confirm; + for (;;) { + EXEC SQL FETCH cur_rc_confirm INTO :h_id2, :h_val, :h_status; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_confirm; userlog("[RC_CONFIRM] 이력 커서 오류"); FAIL(b); } + scan_rows++; + if (!strcmp(h_status, "MATCHED") || !strcmp(h_status, "CONFIRMED")) matched_rows++; + else { unmatched_rows++; unmatched_amt += (h_val < 0 ? -h_val : h_val); } + } + EXEC SQL CLOSE cur_rc_confirm; + userlog("[RC_CONFIRM] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_CONFIRM] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_CONFIRM] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_CONFIRM] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 상태 전이: MATCHED -> CONFIRMED */ + if (rcdb_update_match_status(h_pid, "MATCHED", "CONFIRMED") < 0) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "상태 전이 실패", 0L); + userlog("[RC_CONFIRM] 상태 전이 실패 pid=%ld MATCHED->CONFIRMED", h_pid); FAIL(b); + } + strcpy(h_status, "C"); out_cnt = 1; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_CONFIRM] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_CONFIRM scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_CONFIRM] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { userlog("[RC_CONFIRM] 정산 조회 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + Bchg(b, T_STATUS, 0, h_status, 0L); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_CONFIRM] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_DELAY.pgc b/app/src/rc/svc/RC_DELAY.pgc index 1984661..af89810 100644 --- a/app/src/rc/svc/RC_DELAY.pgc +++ b/app/src/rc/svc/RC_DELAY.pgc @@ -1,20 +1,160 @@ /* - * RC_DELAY.pgc - rc 모듈 서비스 RC_DELAY (one service = one file). - * Moved verbatim from the former inline rc_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by rc_svr at tpsvrinit. + * RC_DELAY.pgc - rc 모듈 서비스 RC_DELAY (대사 온라인 프로그램, 심화판). + * 업무: 지연 대사 로그(익일 반영). + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_DELAY.h" +#include "acq_common.h" -/* 24. RC_DELAY - 지연 대사 로그(익일 반영). */ +/* RC_DELAY - 지연 대사 로그(익일 반영). */ void RC_DELAY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long pid = getl(b, T_PURCHASE_ID), net = getl(b, T_NET), mid; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; + EXEC SQL END DECLARE SECTION; + + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "DELAY_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_DELAY"); + snprintf(h_msg, sizeof(h_msg), "[RC_DELAY] 지연 대사 로그(익일 반영)."); + strcpy(h_key, "-"); + userlog("[RC_DELAY] 대사 진입: 지연 대사 로그(익일 반영)."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - mid = rcdb_insert_match(pid, "DELAY", net, "HOLD", bizdate); - if (mid < 0) FAIL(b); - setl(b, T_ID1, mid); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_DELAY] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_DELAY] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + h_net = getl(b, T_NET); + if (h_net < -100000000000L || h_net > 100000000000L) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "순액 범위 초과", 0L); + userlog("[RC_DELAY] 순액 범위 초과 net=%ld", h_net); FAIL(b); + } + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8004); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_DELAY] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_DELAY] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_DELAY] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_DELAY] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_DELAY] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_DELAY] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_DELAY] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_delay CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_delay; + for (;;) { + EXEC SQL FETCH cur_rc_delay INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_delay; userlog("[RC_DELAY] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_delay; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_delay; + userlog("[RC_DELAY] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_DELAY] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_DELAY] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_DELAY] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 대사 로그 적재 (rc_match_log) */ + h_id = rcdb_insert_match(h_pid, "DELAY", h_net, + (unmatched_rows == 0 ? "MATCHED" : "EXCEPTION"), bizdate); + if (h_id < 0) { userlog("[RC_DELAY] 대사 로그 적재 실패 pid=%ld", h_pid); FAIL(b); } + out_cnt = matched_rows > 0 ? matched_rows : 1; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_DELAY] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_DELAY scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_DELAY] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_DELAY] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_DELAY] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_ID1, h_id); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_DELAY] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_DEPGAP.pgc b/app/src/rc/svc/RC_DEPGAP.pgc index 6a151f6..373aa82 100644 --- a/app/src/rc/svc/RC_DEPGAP.pgc +++ b/app/src/rc/svc/RC_DEPGAP.pgc @@ -1,29 +1,135 @@ /* - * RC_DEPGAP.pgc - rc 모듈 서비스 RC_DEPGAP (one service = one file). - * 입금 누락 감지: 매입은 있으나 RECON 원장이 없는 미입금 건수를 집계. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_DEPGAP.pgc - rc 모듈 서비스 RC_DEPGAP (대사 온라인 프로그램, 심화판). + * 업무: 입금 누락 감지: 매입은 있으나 RECON 원장이 없는 미입금 건수를 집계. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_DEPGAP.h" +#include "acq_common.h" -/* 82. RC_DEPGAP - 입금 누락 감지: 매입은 있으나 RECON 원장이 없는 미입금 건수를 집계. */ +/* RC_DEPGAP - 입금 누락 감지: 매입은 있으나 RECON 원장이 없는 미입금 건수를 집계. */ void RC_DEPGAP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_cnt; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "DEPGAP_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_DEPGAP"); + snprintf(h_msg, sizeof(h_msg), "[RC_DEPGAP] 입금 누락 감지: 매입은 있으나 RECON 원장이 없는 미입금 건수를 집계."); + strcpy(h_key, "-"); + userlog("[RC_DEPGAP] 대사 진입: 입금 누락 감지: 매입은 있으나 RECON 원장이 없는 미입금 건수를 집계."); - EXEC SQL SELECT count(*) INTO :h_cnt FROM purchase p + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_DEPGAP] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_DEPGAP] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_DEPGAP] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_DEPGAP] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_depgap CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_depgap; + for (;;) { + EXEC SQL FETCH cur_rc_depgap INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_depgap; userlog("[RC_DEPGAP] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_depgap; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_depgap; + userlog("[RC_DEPGAP] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_DEPGAP] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_DEPGAP] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_DEPGAP] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 예외 적재 (rc_exception) */ + if (h_ecnt >= 0) { + h_id = rcdb_insert_exception(h_pid, h_reason, bizdate); + if (h_id < 0) { userlog("[RC_DEPGAP] 예외 적재 실패 pid=%ld", h_pid); FAIL(b); } + } + out_cnt = h_ecnt + (h_pid > 0 ? 1 : 0); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_DEPGAP] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_DEPGAP scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_DEPGAP] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt 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 l.entry_type = 'RECON'); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_DEPGAP] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_DEPGAP] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_DEPGAP] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_DEPMATCH.pgc b/app/src/rc/svc/RC_DEPMATCH.pgc index f194721..dbe1048 100644 --- a/app/src/rc/svc/RC_DEPMATCH.pgc +++ b/app/src/rc/svc/RC_DEPMATCH.pgc @@ -1,37 +1,160 @@ /* - * RC_DEPMATCH.pgc - rc 모듈 서비스 RC_DEPMATCH (one service = one file). - * 입금(정산) 대사: 매입 net 금액과 settlement 입금 합계를 비교하여 일치 시 - * MATCHED, 불일치/입금없음 시 EXCEPTION 대사 로그를 남긴다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_DEPMATCH.pgc - rc 모듈 서비스 RC_DEPMATCH (대사 온라인 프로그램, 심화판). + * 업무: 입금(정산) 대사. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_DEPMATCH.h" +#include "acq_common.h" -/* 32. RC_DEPMATCH - 입금(정산) 대사. */ +/* RC_DEPMATCH - 입금(정산) 대사. */ void RC_DEPMATCH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long mid; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_pnet, h_scnt, h_snet; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "DEPMATCH_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_DEPMATCH"); + snprintf(h_msg, sizeof(h_msg), "[RC_DEPMATCH] 입금(정산) 대사."); + strcpy(h_key, "-"); + userlog("[RC_DEPMATCH] 대사 진입: 입금(정산) 대사."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_DEPMATCH] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_DEPMATCH] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } - EXEC SQL SELECT net INTO :h_pnet FROM purchase WHERE purchase_id = :h_pid; - if (sqlca.sqlcode != 0) FAIL(b); - EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_scnt, :h_snet - FROM settlement WHERE purchase_id = :h_pid; + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_DEPMATCH] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_DEPMATCH] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_DEPMATCH] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_DEPMATCH] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_DEPMATCH] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_DEPMATCH] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_DEPMATCH] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_depmatch CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_depmatch; + for (;;) { + EXEC SQL FETCH cur_rc_depmatch INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_depmatch; userlog("[RC_DEPMATCH] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_depmatch; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_depmatch; + userlog("[RC_DEPMATCH] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_DEPMATCH] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_DEPMATCH] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_DEPMATCH] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 차이 라인 적재 (rc_diff) */ + h_exp = h_net; h_act = h_led; h_diff = h_exp - h_act; + EXEC SQL SELECT nextval('rc_diff_seq') INTO :h_id; if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL INSERT INTO rc_diff (diff_id, purchase_id, expected_amt, actual_amt, diff_amt, diff_kind, biz_date) + VALUES (:h_id, :h_pid, :h_exp, :h_act, :h_diff, :h_dtype, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_DEPMATCH] 차이 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + out_cnt = (h_diff != 0 ? 1 : 0); - mid = rcdb_insert_match(h_pid, "DEP", h_snet, - (h_scnt > 0 && h_snet == h_pnet) ? "MATCHED" : "EXCEPTION", - bizdate); - if (mid < 0) FAIL(b); - setl(b, T_AMT1, h_pnet); - setl(b, T_AMT2, h_snet); - setl(b, T_ID1, mid); + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_DEPMATCH] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_DEPMATCH scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_DEPMATCH] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_DEPMATCH] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_DEPMATCH] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_AMT1, h_gross); + setl(b, T_AMT2, h_net); + setl(b, T_ID1, h_id); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_DEPMATCH] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_DEPPAY.pgc b/app/src/rc/svc/RC_DEPPAY.pgc index 5638d98..6441664 100644 --- a/app/src/rc/svc/RC_DEPPAY.pgc +++ b/app/src/rc/svc/RC_DEPPAY.pgc @@ -1,32 +1,133 @@ /* - * RC_DEPPAY.pgc - rc 모듈 서비스 RC_DEPPAY (one service = one file). - * 입금-지급 축 대사: 원장 XFER 이체 합계와 정산 순액 합계의 지급 정합성을 대조. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_DEPPAY.pgc - rc 모듈 서비스 RC_DEPPAY (대사 온라인 프로그램, 심화판). + * 업무: 입금-지급 축 대사: 원장 XFER 이체 합계와 정산 순액 합계의 지급 정합성을 대조. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_DEPPAY.h" +#include "acq_common.h" -/* 78. RC_DEPPAY - 입금-지급 축 대사: 원장 XFER 이체 합계와 정산 순액 합계의 지급 정합성을 대조. */ +/* RC_DEPPAY - 입금-지급 축 대사: 원장 XFER 이체 합계와 정산 순액 합계의 지급 정합성을 대조. */ void RC_DEPPAY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_xfer, h_settle; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "DEPPAY_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_DEPPAY"); + snprintf(h_msg, sizeof(h_msg), "[RC_DEPPAY] 입금-지급 축 대사: 원장 XFER 이체 합계와 정산 순액 합계의 지급 정합성을 대조."); + strcpy(h_key, "-"); + userlog("[RC_DEPPAY] 대사 진입: 입금-지급 축 대사: 원장 XFER 이체 합계와 정산 순액 합계의 지급 정합성을 대조."); - EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_xfer FROM ledger - WHERE biz_date = :h_bizdate AND entry_type = 'XFER'; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT coalesce(sum(net),0) INTO :h_settle FROM settlement - WHERE biz_date = :h_bizdate AND status = 'SETTLED'; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_ARG1, h_xfer); - setl(b, T_GROSS, h_settle); - setl(b, T_DELTA, h_xfer - h_settle); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_DEPPAY] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_DEPPAY] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_DEPPAY] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_DEPPAY] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_deppay CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_deppay; + for (;;) { + EXEC SQL FETCH cur_rc_deppay INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_deppay; userlog("[RC_DEPPAY] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_deppay; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_deppay; + userlog("[RC_DEPPAY] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_DEPPAY] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_DEPPAY] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_DEPPAY] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_DEPPAY] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_DEPPAY scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_DEPPAY] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_DEPPAY] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_DEPPAY] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_ARG1, matched_rows); + setl(b, T_DELTA, h_diff); + setl(b, T_GROSS, h_gross); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_DEPPAY] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_DIFFCAUSE.pgc b/app/src/rc/svc/RC_DIFFCAUSE.pgc index e1e2b08..76320d3 100644 --- a/app/src/rc/svc/RC_DIFFCAUSE.pgc +++ b/app/src/rc/svc/RC_DIFFCAUSE.pgc @@ -1,44 +1,134 @@ /* - * RC_DIFFCAUSE.pgc - rc 모듈 서비스 RC_DIFFCAUSE (one service = one file). - * 대사 차이 원인분석: 당일 차이를 (1) 수수료 계산 불일치, (2) 취소 존재, - * (3) 승인 부재 세 가지 원인으로 분해하여 각각의 건수를 반환한다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_DIFFCAUSE.pgc - rc 모듈 서비스 RC_DIFFCAUSE (대사 온라인 프로그램, 심화판). + * 업무: 차이 원인(수수료/취소/승인부재)별 분해. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_DIFFCAUSE.h" +#include "acq_common.h" -/* 42. RC_DIFFCAUSE - 차이 원인(수수료/취소/승인부재)별 분해. */ +/* RC_DIFFCAUSE - 차이 원인(수수료/취소/승인부재)별 분해. */ void RC_DIFFCAUSE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_fee, h_cxl, h_noap; - char h_bizdate[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "DIFFCAUSE_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_DIFFCAUSE"); + snprintf(h_msg, sizeof(h_msg), "[RC_DIFFCAUSE] 차이 원인(수수료/취소/승인부재)별 분해."); + strcpy(h_key, "-"); + userlog("[RC_DIFFCAUSE] 대사 진입: 차이 원인(수수료/취소/승인부재)별 분해."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_DIFFCAUSE] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); - EXEC SQL SELECT count(*) INTO :h_fee - FROM purchase p, merchant m - WHERE p.merchant_id = m.merchant_id AND p.biz_date = :h_bizdate - AND p.fee <> p.amount * m.mdr_bps / 10000; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_cxl - FROM rc_match_log r - WHERE r.biz_date = :h_bizdate AND r.status = 'EXCEPTION' - AND EXISTS (SELECT 1 FROM ac_cancel c WHERE c.purchase_id = r.purchase_id); - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_noap - FROM purchase p + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_DIFFCAUSE] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_DIFFCAUSE] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_DIFFCAUSE] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_diffcause CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_diffcause; + for (;;) { + EXEC SQL FETCH cur_rc_diffcause INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_diffcause; userlog("[RC_DIFFCAUSE] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_diffcause; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_diffcause; + userlog("[RC_DIFFCAUSE] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_DIFFCAUSE] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_DIFFCAUSE] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_DIFFCAUSE] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_DIFFCAUSE] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_DIFFCAUSE scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_DIFFCAUSE] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p WHERE p.biz_date = :h_bizdate - AND NOT EXISTS (SELECT 1 FROM approval a WHERE a.purchase_id = p.purchase_id); - if (sqlca.sqlcode < 0) FAIL(b); + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_DIFFCAUSE] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_DIFFCAUSE] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; - setl(b, T_ARG1, h_fee); - setl(b, T_ARG2, h_cxl); - setl(b, T_ARG3, h_noap); - setl(b, T_COUNT, h_fee + h_cxl + h_noap); + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_ARG1, matched_rows); + setl(b, T_ARG2, unmatched_rows); + setl(b, T_ARG3, scan_rows); + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_DIFFCAUSE] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_DIFFRANK.pgc b/app/src/rc/svc/RC_DIFFRANK.pgc index c1e6562..f505f70 100644 --- a/app/src/rc/svc/RC_DIFFRANK.pgc +++ b/app/src/rc/svc/RC_DIFFRANK.pgc @@ -1,30 +1,132 @@ /* - * RC_DIFFRANK.pgc - rc 모듈 서비스 RC_DIFFRANK (one service = one file). - * 차이 순위 분석: 당일 절대 차이금액이 가장 큰 매입 1건과 그 차액을 반환. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_DIFFRANK.pgc - rc 모듈 서비스 RC_DIFFRANK (대사 온라인 프로그램, 심화판). + * 업무: 차이 순위 분석: 당일 절대 차이금액이 가장 큰 매입 1건과 그 차액을 반환. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_DIFFRANK.h" +#include "acq_common.h" -/* 100. RC_DIFFRANK - 차이 순위 분석: 당일 절대 차이금액이 가장 큰 매입 1건과 그 차액을 반환. */ +/* RC_DIFFRANK - 차이 순위 분석: 당일 절대 차이금액이 가장 큰 매입 1건과 그 차액을 반환. */ void RC_DIFFRANK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_pid = 0, h_max = 0; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "DIFFRANK_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_DIFFRANK"); + snprintf(h_msg, sizeof(h_msg), "[RC_DIFFRANK] 차이 순위 분석: 당일 절대 차이금액이 가장 큰 매입 1건과 그 차액을 반환."); + strcpy(h_key, "-"); + userlog("[RC_DIFFRANK] 대사 진입: 차이 순위 분석: 당일 절대 차이금액이 가장 큰 매입 1건과 그 차액을 반환."); - EXEC SQL SELECT coalesce(purchase_id,0), coalesce(abs(diff_amt),0) - INTO :h_pid, :h_max FROM rc_diff - WHERE biz_date = :h_bizdate - ORDER BY abs(diff_amt) DESC LIMIT 1; - if (sqlca.sqlcode < 0 && sqlca.sqlcode != 100) FAIL(b); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_DIFFRANK] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_DIFFRANK] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_DIFFRANK] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_DIFFRANK] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_diffrank CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_diffrank; + for (;;) { + EXEC SQL FETCH cur_rc_diffrank INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_diffrank; userlog("[RC_DIFFRANK] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_diffrank; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_diffrank; + userlog("[RC_DIFFRANK] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_DIFFRANK] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_DIFFRANK] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_DIFFRANK] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_DIFFRANK] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_DIFFRANK scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_DIFFRANK] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_DIFFRANK] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_DIFFRANK] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_DELTA, h_diff); setl(b, T_PURCHASE_ID, h_pid); - setl(b, T_DELTA, h_max); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_DIFFRANK] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_DIFFRPT.pgc b/app/src/rc/svc/RC_DIFFRPT.pgc index 3cacc3c..281826e 100644 --- a/app/src/rc/svc/RC_DIFFRPT.pgc +++ b/app/src/rc/svc/RC_DIFFRPT.pgc @@ -1,26 +1,132 @@ /* - * RC_DIFFRPT.pgc - rc 모듈 서비스 RC_DIFFRPT (one service = one file). - * Moved verbatim from the former inline rc_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by rc_svr at tpsvrinit. + * RC_DIFFRPT.pgc - rc 모듈 서비스 RC_DIFFRPT (대사 온라인 프로그램, 심화판). + * 업무: 당일 차이 라인 건수 조회. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_DIFFRPT.h" +#include "acq_common.h" -/* 19. RC_DIFFRPT - 당일 차이 라인 건수 조회. */ +/* RC_DIFFRPT - 당일 차이 라인 건수 조회. */ void RC_DIFFRPT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_sum; - char h_bizdate[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; + + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "DIFFRPT_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_DIFFRPT"); + snprintf(h_msg, sizeof(h_msg), "[RC_DIFFRPT] 당일 차이 라인 건수 조회."); + strcpy(h_key, "-"); + userlog("[RC_DIFFRPT] 대사 진입: 당일 차이 라인 건수 조회."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_DIFFRPT] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT count(*), coalesce(sum(diff_amt),0) INTO :h_cnt, :h_sum - FROM rc_diff WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); - setl(b, T_DELTA, h_sum); + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_DIFFRPT] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_DIFFRPT] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_DIFFRPT] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_diffrpt CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_diffrpt; + for (;;) { + EXEC SQL FETCH cur_rc_diffrpt INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_diffrpt; userlog("[RC_DIFFRPT] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_diffrpt; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_diffrpt; + userlog("[RC_DIFFRPT] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_DIFFRPT] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_DIFFRPT] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_DIFFRPT] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_DIFFRPT] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_DIFFRPT scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_DIFFRPT] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_DIFFRPT] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_DIFFRPT] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + setl(b, T_DELTA, h_diff); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_DIFFRPT] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_DIFFTREND.pgc b/app/src/rc/svc/RC_DIFFTREND.pgc index f5488d0..cc47b40 100644 --- a/app/src/rc/svc/RC_DIFFTREND.pgc +++ b/app/src/rc/svc/RC_DIFFTREND.pgc @@ -1,36 +1,132 @@ /* - * RC_DIFFTREND.pgc - rc 모듈 서비스 RC_DIFFTREND (one service = one file). - * 대사 차이 추이 분석: 당일 평균 |차이금액| 과 직전 7영업일 평균을 비교하여 - * 차이 규모가 커지는 추세인지(악화 여부) 판정한다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_DIFFTREND.pgc - rc 모듈 서비스 RC_DIFFTREND (대사 온라인 프로그램, 심화판). + * 업무: 당일 vs 직전 7일 평균 차이 추이. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_DIFFTREND.h" +#include "acq_common.h" -/* 43. RC_DIFFTREND - 당일 vs 직전 7일 평균 차이 추이. */ +/* RC_DIFFTREND - 당일 vs 직전 7일 평균 차이 추이. */ void RC_DIFFTREND(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_today, h_prior; - char h_bizdate[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "DIFFTREND_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_DIFFTREND"); + snprintf(h_msg, sizeof(h_msg), "[RC_DIFFTREND] 당일 vs 직전 7일 평균 차이 추이."); + strcpy(h_key, "-"); + userlog("[RC_DIFFTREND] 대사 진입: 당일 vs 직전 7일 평균 차이 추이."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_DIFFTREND] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); - EXEC SQL SELECT coalesce(round(avg(abs(diff_amt))),0) INTO :h_today - FROM rc_diff WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT coalesce(round(avg(abs(diff_amt))),0) INTO :h_prior - FROM rc_diff - WHERE biz_date < CAST(:h_bizdate AS date) - AND biz_date >= CAST(:h_bizdate AS date) - 7; - if (sqlca.sqlcode < 0) FAIL(b); + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_DIFFTREND] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_DIFFTREND] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_DIFFTREND] 예외 집계 오류"); FAIL(b); } - setl(b, T_AMT1, h_today); - setl(b, T_AMT2, h_prior); - setl(b, T_RC, (h_today > h_prior) ? 1 : 0); + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_difftrend CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_difftrend; + for (;;) { + EXEC SQL FETCH cur_rc_difftrend INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_difftrend; userlog("[RC_DIFFTREND] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_difftrend; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_difftrend; + userlog("[RC_DIFFTREND] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_DIFFTREND] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_DIFFTREND] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_DIFFTREND] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_DIFFTREND] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_DIFFTREND scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_DIFFTREND] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_DIFFTREND] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_DIFFTREND] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_AMT1, h_gross); + setl(b, T_AMT2, h_net); + setl(b, T_RC, rc_code); + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_DIFFTREND] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_DUPCHK.pgc b/app/src/rc/svc/RC_DUPCHK.pgc index fb027b6..9c69f43 100644 --- a/app/src/rc/svc/RC_DUPCHK.pgc +++ b/app/src/rc/svc/RC_DUPCHK.pgc @@ -1,37 +1,140 @@ /* - * RC_DUPCHK.pgc - rc 모듈 서비스 RC_DUPCHK (one service = one file). - * 중복 대사 검출: 당일 대사 로그에서 같은 매입이 두 번 이상 대사된 매입 - * 수를 GROUP BY/HAVING 서브쿼리로 세고, 발견 시 WARN 경보를 남긴다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_DUPCHK.pgc - rc 모듈 서비스 RC_DUPCHK (대사 온라인 프로그램, 심화판). + * 업무: 중복 대사 매입 검출 + WARN 경보. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_DUPCHK.h" +#include "acq_common.h" -/* 67. RC_DUPCHK - 중복 대사 매입 검출 + WARN 경보. */ +/* RC_DUPCHK - 중복 대사 매입 검출 + WARN 경보. */ void RC_DUPCHK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_dup; - char h_bizdate[16], h_msg[80]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "DUPCHK_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_DUPCHK"); + snprintf(h_msg, sizeof(h_msg), "[RC_DUPCHK] 중복 대사 매입 검출 + WARN 경보."); + strcpy(h_key, "-"); + userlog("[RC_DUPCHK] 대사 진입: 중복 대사 매입 검출 + WARN 경보."); + + /* [1] 입력 검증부 -------------------------------------------------- */ 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_dup FROM - (SELECT purchase_id FROM rc_match_log - WHERE biz_date = :h_bizdate - GROUP BY purchase_id HAVING count(*) > 1) dup; - if (sqlca.sqlcode < 0) FAIL(b); - - if (h_dup > 0) { - snprintf(h_msg, sizeof(h_msg), "duplicate recon purchases: %ld", h_dup); - EXEC SQL INSERT INTO rc_alert (alert_id, purchase_id, lvl, message, biz_date) - VALUES (nextval('rc_alert_seq'), 0, 'WARN', :h_msg, :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_DUPCHK] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); } - setl(b, T_COUNT, h_dup); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_DUPCHK] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_DUPCHK] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_DUPCHK] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_dupchk CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_dupchk; + for (;;) { + EXEC SQL FETCH cur_rc_dupchk INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_dupchk; userlog("[RC_DUPCHK] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_dupchk; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_dupchk; + userlog("[RC_DUPCHK] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_DUPCHK] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_DUPCHK] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_DUPCHK] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 경보 등록 (rc_alert) */ + if (unmatched_rows > 0 || h_ecnt > 0) strcpy(h_lvl, "WARN"); + if (unmatched_rows > 10 || h_ecnt > 20) strcpy(h_lvl, "CRIT"); + snprintf(sbuf, sizeof(sbuf), "중복 대사 매입 검출 + WARN 경보. unmatch=%ld exc=%ld", unmatched_rows, h_ecnt); + strncpy(h_msg, sbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL SELECT nextval('rc_alert_seq') INTO :h_id; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL INSERT INTO rc_alert (alert_id, purchase_id, lvl, message, biz_date) + VALUES (:h_id, :h_pid, :h_lvl, :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_DUPCHK] 경보 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + out_cnt = h_ecnt; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_DUPCHK] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_DUPCHK scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_DUPCHK] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_DUPCHK] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_DUPCHK] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_DUPCHK] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_EDCRECON.pgc b/app/src/rc/svc/RC_EDCRECON.pgc index 5537dd6..3e0cf68 100644 --- a/app/src/rc/svc/RC_EDCRECON.pgc +++ b/app/src/rc/svc/RC_EDCRECON.pgc @@ -1,31 +1,132 @@ /* - * RC_EDCRECON.pgc - rc 모듈 서비스 RC_EDCRECON (one service = one file). - * EDC 채널 대사: EDC 단말 매입의 순액 합계와 미대사 건수를 함께 반환. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_EDCRECON.pgc - rc 모듈 서비스 RC_EDCRECON (대사 온라인 프로그램, 심화판). + * 업무: EDC 채널 대사: EDC 단말 매입의 순액 합계와 미대사 건수를 함께 반환. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_EDCRECON.h" +#include "acq_common.h" -/* 89. RC_EDCRECON - EDC 채널 대사: EDC 단말 매입의 순액 합계와 미대사 건수를 함께 반환. */ +/* RC_EDCRECON - EDC 채널 대사: EDC 단말 매입의 순액 합계와 미대사 건수를 함께 반환. */ void RC_EDCRECON(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_net, h_open; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "EDCRECON_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_EDCRECON"); + snprintf(h_msg, sizeof(h_msg), "[RC_EDCRECON] EDC 채널 대사: EDC 단말 매입의 순액 합계와 미대사 건수를 함께 반환."); + strcpy(h_key, "-"); + userlog("[RC_EDCRECON] 대사 진입: EDC 채널 대사: EDC 단말 매입의 순액 합계와 미대사 건수를 함께 반환."); - EXEC SQL SELECT coalesce(sum(net),0) INTO :h_net FROM purchase - WHERE biz_date = :h_bizdate AND channel = 'EDC'; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_open FROM purchase - WHERE biz_date = :h_bizdate AND channel = 'EDC' AND status = 'A'; - if (sqlca.sqlcode < 0) FAIL(b); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_EDCRECON] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_EDCRECON] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_EDCRECON] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_EDCRECON] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_edcrecon CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_edcrecon; + for (;;) { + EXEC SQL FETCH cur_rc_edcrecon INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_edcrecon; userlog("[RC_EDCRECON] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_edcrecon; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_edcrecon; + userlog("[RC_EDCRECON] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_EDCRECON] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_EDCRECON] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_EDCRECON] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_EDCRECON] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_EDCRECON scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_EDCRECON] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_EDCRECON] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_EDCRECON] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_ARG1, matched_rows); setl(b, T_NET, h_net); - setl(b, T_ARG1, h_open); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_EDCRECON] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_EDIRECON.pgc b/app/src/rc/svc/RC_EDIRECON.pgc index 0f18f6b..b671b5d 100644 --- a/app/src/rc/svc/RC_EDIRECON.pgc +++ b/app/src/rc/svc/RC_EDIRECON.pgc @@ -1,45 +1,147 @@ /* - * RC_EDIRECON.pgc - rc 모듈 서비스 RC_EDIRECON (one service = one file). - * EDI 대사: 가맹점의 당일 EDI 전문 금액 합계와 매입(취소 제외) 금액 합계를 - * 비교하고, 차이가 있으면 가맹점 단위 rc_diff 라인을 적재한다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_EDIRECON.pgc - rc 모듈 서비스 RC_EDIRECON (대사 온라인 프로그램, 심화판). + * 업무: 가맹점 EDI 전문 대 매입 합계 대사. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_EDIRECON.h" +#include "acq_common.h" -/* 61. RC_EDIRECON - 가맹점 EDI 전문 대 매입 합계 대사. */ +/* RC_EDIRECON - 가맹점 EDI 전문 대 매입 합계 대사. */ void RC_EDIRECON(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], merchant[64]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_edi, h_en, h_pur; - char h_bizdate[16], h_mid[64]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "EDIRECON_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_EDIRECON"); + snprintf(h_msg, sizeof(h_msg), "[RC_EDIRECON] 가맹점 EDI 전문 대 매입 합계 대사."); + strcpy(h_key, "-"); + userlog("[RC_EDIRECON] 대사 진입: 가맹점 EDI 전문 대 매입 합계 대사."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - gets_(b, T_MERCHANT, merchant, sizeof(merchant)); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - strncpy(h_mid, merchant, sizeof(h_mid)-1); h_mid[sizeof(h_mid)-1] = 0; - - EXEC SQL SELECT coalesce(sum(amount),0), count(*) INTO :h_edi, :h_en - FROM ac_edi WHERE merchant_id = :h_mid AND biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_pur - FROM purchase - WHERE merchant_id = :h_mid AND biz_date = :h_bizdate AND status <> 'C'; - if (sqlca.sqlcode < 0) FAIL(b); - - if (h_edi != h_pur) { - EXEC SQL INSERT INTO rc_diff - (diff_id, purchase_id, expected_amt, actual_amt, diff_amt, diff_kind, biz_date) - VALUES (nextval('rc_diff_seq'), 0, :h_pur, :h_edi, :h_pur - :h_edi, - 'AMT', :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_EDIRECON] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); } - setl(b, T_AMT1, h_edi); - setl(b, T_AMT2, h_pur); - setl(b, T_DELTA, h_pur - h_edi); - setl(b, T_COUNT, h_en); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + gets_(b, T_MERCHANT, tmpin, sizeof(tmpin)); + if (tmpin[0] == 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "가맹점 ID 필수", 0L); + userlog("[RC_EDIRECON] 가맹점 ID 미입력"); FAIL(b); + } + strncpy(h_merch, tmpin, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strcpy(h_key, h_merch); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_EDIRECON] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_EDIRECON] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_EDIRECON] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_EDIRECON] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_EDIRECON] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_edirecon CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_edirecon; + for (;;) { + EXEC SQL FETCH cur_rc_edirecon INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_edirecon; userlog("[RC_EDIRECON] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_edirecon; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_edirecon; + userlog("[RC_EDIRECON] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_EDIRECON] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_EDIRECON] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_EDIRECON] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_EDIRECON] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_EDIRECON scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_EDIRECON] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_EDIRECON] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_EDIRECON] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_AMT1, h_gross); + setl(b, T_AMT2, h_net); + setl(b, T_COUNT, out_cnt); + setl(b, T_DELTA, h_diff); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_EDIRECON] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_ESCALATE.pgc b/app/src/rc/svc/RC_ESCALATE.pgc index c90caed..5d9659a 100644 --- a/app/src/rc/svc/RC_ESCALATE.pgc +++ b/app/src/rc/svc/RC_ESCALATE.pgc @@ -1,29 +1,140 @@ /* - * RC_ESCALATE.pgc - rc 모듈 서비스 RC_ESCALATE (one service = one file). - * 예외 에스컬레이션: 미해결 예외가 걸린 매입의 당일 WARN 경보들을 IN-서브 - * 쿼리 UPDATE 로 일괄 CRIT 승격시키고 승격 건수를 반환한다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_ESCALATE.pgc - rc 모듈 서비스 RC_ESCALATE (대사 온라인 프로그램, 심화판). + * 업무: 미해결 예외 경보 WARN -> CRIT 일괄 승격. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_ESCALATE.h" +#include "acq_common.h" -/* 72. RC_ESCALATE - 미해결 예외 경보 WARN -> CRIT 일괄 승격. */ +/* RC_ESCALATE - 미해결 예외 경보 WARN -> CRIT 일괄 승격. */ void RC_ESCALATE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "ESCALATE_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_ESCALATE"); + snprintf(h_msg, sizeof(h_msg), "[RC_ESCALATE] 미해결 예외 경보 WARN -> CRIT 일괄 승격."); + strcpy(h_key, "-"); + userlog("[RC_ESCALATE] 대사 진입: 미해결 예외 경보 WARN -> CRIT 일괄 승격."); - EXEC SQL UPDATE rc_alert SET lvl = 'CRIT' - WHERE biz_date = :h_bizdate AND lvl = 'WARN' - AND purchase_id IN (SELECT purchase_id FROM rc_exception - WHERE biz_date = :h_bizdate AND resolved = false); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_ESCALATE] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_ESCALATE] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_ESCALATE] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_ESCALATE] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_escalate CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_escalate; + for (;;) { + EXEC SQL FETCH cur_rc_escalate INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_escalate; userlog("[RC_ESCALATE] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_escalate; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_escalate; + userlog("[RC_ESCALATE] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_ESCALATE] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_ESCALATE] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_ESCALATE] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 경보 등록 (rc_alert) */ + if (unmatched_rows > 0 || h_ecnt > 0) strcpy(h_lvl, "WARN"); + if (unmatched_rows > 10 || h_ecnt > 20) strcpy(h_lvl, "CRIT"); + snprintf(sbuf, sizeof(sbuf), "미해결 예외 경보 WARN -> CRIT 일괄 승격. unmatch=%ld exc=%ld", unmatched_rows, h_ecnt); + strncpy(h_msg, sbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL SELECT nextval('rc_alert_seq') INTO :h_id; if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, sqlca.sqlerrd[2]); + EXEC SQL INSERT INTO rc_alert (alert_id, purchase_id, lvl, message, biz_date) + VALUES (:h_id, :h_pid, :h_lvl, :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_ESCALATE] 경보 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + out_cnt = h_ecnt; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_ESCALATE] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_ESCALATE scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_ESCALATE] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_ESCALATE] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_ESCALATE] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_ESCALATE] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_EXCEPTION.pgc b/app/src/rc/svc/RC_EXCEPTION.pgc index 6772d85..c175062 100644 --- a/app/src/rc/svc/RC_EXCEPTION.pgc +++ b/app/src/rc/svc/RC_EXCEPTION.pgc @@ -1,21 +1,157 @@ /* - * RC_EXCEPTION.pgc - rc 모듈 서비스 RC_EXCEPTION (one service = one file). - * Moved verbatim from the former inline rc_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by rc_svr at tpsvrinit. + * RC_EXCEPTION.pgc - rc 모듈 서비스 RC_EXCEPTION (대사 온라인 프로그램, 심화판). + * 업무: 대사 예외 등록. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_EXCEPTION.h" +#include "acq_common.h" -/* 13. RC_EXCEPTION - 대사 예외 등록. */ +/* RC_EXCEPTION - 대사 예외 등록. */ void RC_EXCEPTION(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], reason[128]; - long pid = getl(b, T_PURCHASE_ID), eid; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; + EXEC SQL END DECLARE SECTION; + + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "EXCEPTION_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_EXCEPTION"); + snprintf(h_msg, sizeof(h_msg), "[RC_EXCEPTION] 대사 예외 등록."); + strcpy(h_key, "-"); + userlog("[RC_EXCEPTION] 대사 진입: 대사 예외 등록."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "AMT_MISMATCH"); - eid = rcdb_insert_exception(pid, reason, bizdate); - if (eid < 0) FAIL(b); - setl(b, T_ID1, eid); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_EXCEPTION] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_EXCEPTION] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + gets_(b, T_REASON, tmpin, sizeof(tmpin)); if (tmpin[0]) { strncpy(h_reason, tmpin, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1]=0; } + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_EXCEPTION] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_EXCEPTION] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_EXCEPTION] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_EXCEPTION] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_EXCEPTION] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_EXCEPTION] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_EXCEPTION] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_exception CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_exception; + for (;;) { + EXEC SQL FETCH cur_rc_exception INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_exception; userlog("[RC_EXCEPTION] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_exception; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_exception; + userlog("[RC_EXCEPTION] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_EXCEPTION] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_EXCEPTION] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_EXCEPTION] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 예외 적재 (rc_exception) */ + if (h_ecnt >= 0) { + h_id = rcdb_insert_exception(h_pid, h_reason, bizdate); + if (h_id < 0) { userlog("[RC_EXCEPTION] 예외 적재 실패 pid=%ld", h_pid); FAIL(b); } + } + out_cnt = h_ecnt + (h_pid > 0 ? 1 : 0); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_EXCEPTION] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_EXCEPTION scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_EXCEPTION] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_EXCEPTION] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_EXCEPTION] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_ID1, h_id); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_EXCEPTION] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_EXCNOTIFY.pgc b/app/src/rc/svc/RC_EXCNOTIFY.pgc index 7f69861..f9da1e2 100644 --- a/app/src/rc/svc/RC_EXCNOTIFY.pgc +++ b/app/src/rc/svc/RC_EXCNOTIFY.pgc @@ -1,33 +1,140 @@ /* - * RC_EXCNOTIFY.pgc - rc 모듈 서비스 RC_EXCNOTIFY (one service = one file). - * 대사 예외 알림: 당일 미해결 예외 건수를 집계해 임계 초과 시 rc_alert 경보를 발생. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_EXCNOTIFY.pgc - rc 모듈 서비스 RC_EXCNOTIFY (대사 온라인 프로그램, 심화판). + * 업무: 대사 예외 알림: 당일 미해결 예외 건수를 집계해 임계 초과 시 rc_alert 경보를 발생. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_EXCNOTIFY.h" +#include "acq_common.h" -/* 131. RC_EXCNOTIFY - 대사 예외 알림: 당일 미해결 예외 건수를 집계해 임계 초과 시 rc_alert 경보를 발생. */ +/* RC_EXCNOTIFY - 대사 예외 알림: 당일 미해결 예외 건수를 집계해 임계 초과 시 rc_alert 경보를 발생. */ void RC_EXCNOTIFY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_id, h_cnt; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "EXCNOTIFY_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_EXCNOTIFY"); + snprintf(h_msg, sizeof(h_msg), "[RC_EXCNOTIFY] 대사 예외 알림: 당일 미해결 예외 건수를 집계해 임계 초과 시 rc_alert 경보를 발생."); + strcpy(h_key, "-"); + userlog("[RC_EXCNOTIFY] 대사 진입: 대사 예외 알림: 당일 미해결 예외 건수를 집계해 임계 초과 시 rc_alert 경보를 발생."); - EXEC SQL SELECT count(*) INTO :h_cnt FROM rc_exception - WHERE biz_date = :h_bizdate AND resolved = false; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_cnt > 0) { - EXEC SQL SELECT nextval('rc_alert_seq') INTO :h_id; - EXEC SQL INSERT INTO rc_alert (alert_id, lvl, message, biz_date) - VALUES (:h_id, 'WARN', 'UNRESOLVED_EXC', :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_EXCNOTIFY] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); } - setl(b, T_COUNT, h_cnt); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_EXCNOTIFY] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_EXCNOTIFY] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_EXCNOTIFY] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_excnotify CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_excnotify; + for (;;) { + EXEC SQL FETCH cur_rc_excnotify INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_excnotify; userlog("[RC_EXCNOTIFY] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_excnotify; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_excnotify; + userlog("[RC_EXCNOTIFY] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_EXCNOTIFY] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_EXCNOTIFY] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_EXCNOTIFY] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 경보 등록 (rc_alert) */ + if (unmatched_rows > 0 || h_ecnt > 0) strcpy(h_lvl, "WARN"); + if (unmatched_rows > 10 || h_ecnt > 20) strcpy(h_lvl, "CRIT"); + snprintf(sbuf, sizeof(sbuf), "대사 예외 알림: 당일 미해결 예외 건수를 집계해 임계 초과 시 rc_alert 경보를 발생. unmatch=%ld exc=%ld", unmatched_rows, h_ecnt); + strncpy(h_msg, sbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL SELECT nextval('rc_alert_seq') INTO :h_id; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL INSERT INTO rc_alert (alert_id, purchase_id, lvl, message, biz_date) + VALUES (:h_id, :h_pid, :h_lvl, :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_EXCNOTIFY] 경보 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + out_cnt = h_ecnt; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_EXCNOTIFY] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_EXCNOTIFY scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_EXCNOTIFY] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_EXCNOTIFY] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_EXCNOTIFY] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_EXCNOTIFY] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_FEEBRK.pgc b/app/src/rc/svc/RC_FEEBRK.pgc index 4ff21fb..4db73c9 100644 --- a/app/src/rc/svc/RC_FEEBRK.pgc +++ b/app/src/rc/svc/RC_FEEBRK.pgc @@ -1,30 +1,133 @@ /* - * RC_FEEBRK.pgc - rc 모듈 서비스 RC_FEEBRK (one service = one file). - * 수수료 내역 대사: 당일 매입 수수료 원계와 수수료조정 누계를 비교해 순수수료 산출. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_FEEBRK.pgc - rc 모듈 서비스 RC_FEEBRK (대사 온라인 프로그램, 심화판). + * 업무: 수수료 내역 대사: 당일 매입 수수료 원계와 수수료조정 누계를 비교해 순수수료 산출. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_FEEBRK.h" +#include "acq_common.h" -/* 124. RC_FEEBRK - 수수료 내역 대사: 당일 매입 수수료 원계와 수수료조정 누계를 비교해 순수수료 산출. */ +/* RC_FEEBRK - 수수료 내역 대사: 당일 매입 수수료 원계와 수수료조정 누계를 비교해 순수수료 산출. */ void RC_FEEBRK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_fee, h_adj; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "FEEBRK_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_FEEBRK"); + snprintf(h_msg, sizeof(h_msg), "[RC_FEEBRK] 수수료 내역 대사: 당일 매입 수수료 원계와 수수료조정 누계를 비교해 순수수료 산출."); + strcpy(h_key, "-"); + userlog("[RC_FEEBRK] 대사 진입: 수수료 내역 대사: 당일 매입 수수료 원계와 수수료조정 누계를 비교해 순수수료 산출."); - EXEC SQL SELECT coalesce(sum(fee),0) INTO :h_fee FROM purchase WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT coalesce(sum(delta),0) INTO :h_adj FROM ac_fee_adj WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_FEEBRK] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_FEEBRK] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_FEEBRK] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_FEEBRK] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_feebrk CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_feebrk; + for (;;) { + EXEC SQL FETCH cur_rc_feebrk INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_feebrk; userlog("[RC_FEEBRK] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_feebrk; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_feebrk; + userlog("[RC_FEEBRK] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_FEEBRK] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_FEEBRK] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_FEEBRK] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_FEEBRK] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_FEEBRK scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_FEEBRK] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_FEEBRK] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_FEEBRK] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_ARG1, matched_rows); + setl(b, T_DELTA, h_diff); setl(b, T_FEE, h_fee); - setl(b, T_DELTA, h_adj); - setl(b, T_ARG1, h_fee + h_adj); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_FEEBRK] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_FEEDIFF.pgc b/app/src/rc/svc/RC_FEEDIFF.pgc index 37f221e..f3b6bd1 100644 --- a/app/src/rc/svc/RC_FEEDIFF.pgc +++ b/app/src/rc/svc/RC_FEEDIFF.pgc @@ -1,33 +1,138 @@ /* - * RC_FEEDIFF.pgc - rc 모듈 서비스 RC_FEEDIFF (one service = one file). - * 수수료 차이 일괄 집계: 당일 매입 전체에서 MDR 기대치와 다른 수수료 건수와 - * 차이 합계를 한 번의 조인 집계로 산출한다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_FEEDIFF.pgc - rc 모듈 서비스 RC_FEEDIFF (대사 온라인 프로그램, 심화판). + * 업무: 당일 수수료 불일치 건수/차액 합계. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_FEEDIFF.h" +#include "acq_common.h" -/* 56. RC_FEEDIFF - 당일 수수료 불일치 건수/차액 합계. */ +/* RC_FEEDIFF - 당일 수수료 불일치 건수/차액 합계. */ void RC_FEEDIFF(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_dsum; - char h_bizdate[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "FEEDIFF_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_FEEDIFF"); + snprintf(h_msg, sizeof(h_msg), "[RC_FEEDIFF] 당일 수수료 불일치 건수/차액 합계."); + strcpy(h_key, "-"); + userlog("[RC_FEEDIFF] 대사 진입: 당일 수수료 불일치 건수/차액 합계."); - EXEC SQL SELECT count(*), - coalesce(sum(p.fee - p.amount * m.mdr_bps / 10000),0) - INTO :h_cnt, :h_dsum - FROM purchase p, merchant m - WHERE p.merchant_id = m.merchant_id AND p.biz_date = :h_bizdate - AND p.fee <> p.amount * m.mdr_bps / 10000; + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_FEEDIFF] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_FEEDIFF] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_FEEDIFF] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_FEEDIFF] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_feediff CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_feediff; + for (;;) { + EXEC SQL FETCH cur_rc_feediff INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_feediff; userlog("[RC_FEEDIFF] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_feediff; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_feediff; + userlog("[RC_FEEDIFF] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_FEEDIFF] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_FEEDIFF] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_FEEDIFF] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 차이 라인 적재 (rc_diff) */ + h_exp = h_net; h_act = h_led; h_diff = h_exp - h_act; + EXEC SQL SELECT nextval('rc_diff_seq') INTO :h_id; if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); - setl(b, T_DELTA, h_dsum); + EXEC SQL INSERT INTO rc_diff (diff_id, purchase_id, expected_amt, actual_amt, diff_amt, diff_kind, biz_date) + VALUES (:h_id, :h_pid, :h_exp, :h_act, :h_diff, :h_dtype, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_FEEDIFF] 차이 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + out_cnt = (h_diff != 0 ? 1 : 0); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_FEEDIFF] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_FEEDIFF scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_FEEDIFF] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_FEEDIFF] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_FEEDIFF] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + setl(b, T_DELTA, h_diff); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_FEEDIFF] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_FEERECON.pgc b/app/src/rc/svc/RC_FEERECON.pgc index 022cfd7..8af462e 100644 --- a/app/src/rc/svc/RC_FEERECON.pgc +++ b/app/src/rc/svc/RC_FEERECON.pgc @@ -1,45 +1,160 @@ /* - * RC_FEERECON.pgc - rc 모듈 서비스 RC_FEERECON (one service = one file). - * 수수료 대사: 매입 수수료를 가맹점 MDR(bp) 기준 기대 수수료와 비교한다. - * 오차 1원 이내면 MATCHED, 초과하면 rc_diff 적재 후 EXCEPTION 처리. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_FEERECON.pgc - rc 모듈 서비스 RC_FEERECON (대사 온라인 프로그램, 심화판). + * 업무: MDR 기준 수수료 대사. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_FEERECON.h" +#include "acq_common.h" -/* 55. RC_FEERECON - MDR 기준 수수료 대사. */ +/* RC_FEERECON - MDR 기준 수수료 대사. */ void RC_FEERECON(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long mid, diff, ok; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_fee, h_exp; - char h_bizdate[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "FEERECON_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_FEERECON"); + snprintf(h_msg, sizeof(h_msg), "[RC_FEERECON] MDR 기준 수수료 대사."); + strcpy(h_key, "-"); + userlog("[RC_FEERECON] 대사 진입: MDR 기준 수수료 대사."); + + /* [1] 입력 검증부 -------------------------------------------------- */ 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 p.fee, p.amount * m.mdr_bps / 10000 INTO :h_fee, :h_exp - FROM purchase p, merchant m - WHERE p.merchant_id = m.merchant_id AND p.purchase_id = :h_pid; - if (sqlca.sqlcode != 0) FAIL(b); - - diff = h_fee - h_exp; - ok = (diff >= -1 && diff <= 1); - if (!ok) { - EXEC SQL INSERT INTO rc_diff - (diff_id, purchase_id, expected_amt, actual_amt, diff_amt, diff_kind, biz_date) - VALUES (nextval('rc_diff_seq'), :h_pid, :h_exp, :h_fee, - :h_fee - :h_exp, 'AMT', :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_FEERECON] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); } - mid = rcdb_insert_match(h_pid, "FEE", h_fee, ok ? "MATCHED" : "EXCEPTION", bizdate); - if (mid < 0) FAIL(b); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_FEERECON] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_FEERECON] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_FEERECON] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_FEERECON] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_FEERECON] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_FEERECON] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_FEERECON] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_FEERECON] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_feerecon CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_feerecon; + for (;;) { + EXEC SQL FETCH cur_rc_feerecon INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_feerecon; userlog("[RC_FEERECON] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_feerecon; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_feerecon; + userlog("[RC_FEERECON] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_FEERECON] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_FEERECON] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_FEERECON] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 차이 라인 적재 (rc_diff) */ + h_exp = h_net; h_act = h_led; h_diff = h_exp - h_act; + EXEC SQL SELECT nextval('rc_diff_seq') INTO :h_id; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL INSERT INTO rc_diff (diff_id, purchase_id, expected_amt, actual_amt, diff_amt, diff_kind, biz_date) + VALUES (:h_id, :h_pid, :h_exp, :h_act, :h_diff, :h_dtype, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_FEERECON] 차이 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + out_cnt = (h_diff != 0 ? 1 : 0); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_FEERECON] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_FEERECON scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_FEERECON] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_FEERECON] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_FEERECON] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_DELTA, h_diff); setl(b, T_FEE, h_fee); - setl(b, T_DELTA, diff); - setl(b, T_ID1, mid); + setl(b, T_ID1, h_id); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_FEERECON] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_FIXDUP.pgc b/app/src/rc/svc/RC_FIXDUP.pgc index a0f7f4e..6e3e163 100644 --- a/app/src/rc/svc/RC_FIXDUP.pgc +++ b/app/src/rc/svc/RC_FIXDUP.pgc @@ -1,29 +1,131 @@ /* - * RC_FIXDUP.pgc - rc 모듈 서비스 RC_FIXDUP (one service = one file). - * 중복 자동보정: 동일 매입에 SETTLE 원장이 2건 이상 중복된 매입 건수를 탐지. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_FIXDUP.pgc - rc 모듈 서비스 RC_FIXDUP (대사 온라인 프로그램, 심화판). + * 업무: 중복 자동보정: 동일 매입에 SETTLE 원장이 2건 이상 중복된 매입 건수를 탐지. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_FIXDUP.h" +#include "acq_common.h" -/* 115. RC_FIXDUP - 중복 자동보정: 동일 매입에 SETTLE 원장이 2건 이상 중복된 매입 건수를 탐지. */ +/* RC_FIXDUP - 중복 자동보정: 동일 매입에 SETTLE 원장이 2건 이상 중복된 매입 건수를 탐지. */ void RC_FIXDUP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_cnt; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "FIXDUP_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_FIXDUP"); + snprintf(h_msg, sizeof(h_msg), "[RC_FIXDUP] 중복 자동보정: 동일 매입에 SETTLE 원장이 2건 이상 중복된 매입 건수를 탐지."); + strcpy(h_key, "-"); + userlog("[RC_FIXDUP] 대사 진입: 중복 자동보정: 동일 매입에 SETTLE 원장이 2건 이상 중복된 매입 건수를 탐지."); - EXEC SQL SELECT count(*) INTO :h_cnt FROM ( - SELECT purchase_id FROM ledger - WHERE biz_date = :h_bizdate AND entry_type = 'SETTLE' - GROUP BY purchase_id HAVING count(*) > 1) d; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_FIXDUP] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_FIXDUP] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_FIXDUP] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_FIXDUP] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_fixdup CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_fixdup; + for (;;) { + EXEC SQL FETCH cur_rc_fixdup INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_fixdup; userlog("[RC_FIXDUP] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_fixdup; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_fixdup; + userlog("[RC_FIXDUP] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_FIXDUP] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_FIXDUP] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_FIXDUP] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_FIXDUP] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_FIXDUP scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_FIXDUP] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_FIXDUP] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_FIXDUP] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_FIXDUP] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_FIXFEE.pgc b/app/src/rc/svc/RC_FIXFEE.pgc index ffeefbd..a594f15 100644 --- a/app/src/rc/svc/RC_FIXFEE.pgc +++ b/app/src/rc/svc/RC_FIXFEE.pgc @@ -1,33 +1,154 @@ /* - * RC_FIXFEE.pgc - rc 모듈 서비스 RC_FIXFEE (one service = one file). - * 수수료 자동보정: 매입 수수료가 MDR 이론치와 다르면 차이를 조정 델타로 반환. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_FIXFEE.pgc - rc 모듈 서비스 RC_FIXFEE (대사 온라인 프로그램, 심화판). + * 업무: 수수료 자동보정: 매입 수수료가 MDR 이론치와 다르면 차이를 조정 델타로 반환. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_FIXFEE.h" +#include "acq_common.h" -/* 113. RC_FIXFEE - 수수료 자동보정: 매입 수수료가 MDR 이론치와 다르면 차이를 조정 델타로 반환. */ +/* RC_FIXFEE - 수수료 자동보정: 매입 수수료가 MDR 이론치와 다르면 차이를 조정 델타로 반환. */ void RC_FIXFEE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_pid, h_fee, h_amt, h_bps, h_calc; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "FIXFEE_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_FIXFEE"); + snprintf(h_msg, sizeof(h_msg), "[RC_FIXFEE] 수수료 자동보정: 매입 수수료가 MDR 이론치와 다르면 차이를 조정 델타로 반환."); + strcpy(h_key, "-"); + userlog("[RC_FIXFEE] 대사 진입: 수수료 자동보정: 매입 수수료가 MDR 이론치와 다르면 차이를 조정 델타로 반환."); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_FIXFEE] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); h_pid = getl(b, T_PURCHASE_ID); - EXEC SQL SELECT p.fee, p.amount, coalesce(m.mdr_bps,0) - INTO :h_fee, :h_amt, :h_bps FROM purchase p - JOIN merchant m ON m.merchant_id = p.merchant_id - WHERE p.purchase_id = :h_pid; - if (sqlca.sqlcode < 0 && sqlca.sqlcode != 100) FAIL(b); - h_calc = h_amt * h_bps / 10000; + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_FIXFEE] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_FIXFEE] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_FIXFEE] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_FIXFEE] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_FIXFEE] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_FIXFEE] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_FIXFEE] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_FIXFEE] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_fixfee CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_fixfee; + for (;;) { + EXEC SQL FETCH cur_rc_fixfee INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_fixfee; userlog("[RC_FIXFEE] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_fixfee; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_fixfee; + userlog("[RC_FIXFEE] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_FIXFEE] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_FIXFEE] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_FIXFEE] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_FIXFEE] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_FIXFEE scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_FIXFEE] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_FIXFEE] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_FIXFEE] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_ARG1, matched_rows); + setl(b, T_DELTA, h_diff); setl(b, T_FEE, h_fee); - setl(b, T_ARG1, h_calc); - setl(b, T_DELTA, h_calc - h_fee); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_FIXFEE] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_FIXFX.pgc b/app/src/rc/svc/RC_FIXFX.pgc index 2a5f55a..793f0ef 100644 --- a/app/src/rc/svc/RC_FIXFX.pgc +++ b/app/src/rc/svc/RC_FIXFX.pgc @@ -1,31 +1,154 @@ /* - * RC_FIXFX.pgc - rc 모듈 서비스 RC_FIXFX (one service = one file). - * 외화 자동보정: ac_fx 의 원화환산액과 원통화*환율 이론치의 차이를 산출. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_FIXFX.pgc - rc 모듈 서비스 RC_FIXFX (대사 온라인 프로그램, 심화판). + * 업무: 외화 자동보정: ac_fx 의 원화환산액과 원통화*환율 이론치의 차이를 산출. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_FIXFX.h" +#include "acq_common.h" -/* 114. RC_FIXFX - 외화 자동보정: ac_fx 의 원화환산액과 원통화*환율 이론치의 차이를 산출. */ +/* RC_FIXFX - 외화 자동보정: ac_fx 의 원화환산액과 원통화*환율 이론치의 차이를 산출. */ void RC_FIXFX(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_pid, h_krw, h_fx, h_rate, h_calc; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "FIXFX_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_FIXFX"); + snprintf(h_msg, sizeof(h_msg), "[RC_FIXFX] 외화 자동보정: ac_fx 의 원화환산액과 원통화*환율 이론치의 차이를 산출."); + strcpy(h_key, "-"); + userlog("[RC_FIXFX] 대사 진입: 외화 자동보정: ac_fx 의 원화환산액과 원통화*환율 이론치의 차이를 산출."); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_FIXFX] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); h_pid = getl(b, T_PURCHASE_ID); - EXEC SQL SELECT coalesce(krw_amount,0), coalesce(fx_amount,0), coalesce(rate_bps,0) - INTO :h_krw, :h_fx, :h_rate FROM ac_fx WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0 && sqlca.sqlcode != 100) FAIL(b); - h_calc = h_fx * h_rate / 10000; - setl(b, T_ARG1, h_krw); - setl(b, T_ARG2, h_calc); - setl(b, T_DELTA, h_krw - h_calc); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_FIXFX] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_FIXFX] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_FIXFX] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_FIXFX] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_FIXFX] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_FIXFX] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_FIXFX] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_FIXFX] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_fixfx CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_fixfx; + for (;;) { + EXEC SQL FETCH cur_rc_fixfx INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_fixfx; userlog("[RC_FIXFX] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_fixfx; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_fixfx; + userlog("[RC_FIXFX] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_FIXFX] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_FIXFX] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_FIXFX] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_FIXFX] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_FIXFX scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_FIXFX] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_FIXFX] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_FIXFX] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_ARG1, matched_rows); + setl(b, T_ARG2, unmatched_rows); + setl(b, T_DELTA, h_diff); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_FIXFX] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_FIXROUND.pgc b/app/src/rc/svc/RC_FIXROUND.pgc index a02a577..c992c2b 100644 --- a/app/src/rc/svc/RC_FIXROUND.pgc +++ b/app/src/rc/svc/RC_FIXROUND.pgc @@ -1,40 +1,131 @@ /* - * RC_FIXROUND.pgc - rc 모듈 서비스 RC_FIXROUND (one service = one file). - * 반올림 자동보정: 절대차 1 이하 단수 차이를 수수료조정(ac_fee_adj)으로 상계 기록. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_FIXROUND.pgc - rc 모듈 서비스 RC_FIXROUND (대사 온라인 프로그램, 심화판). + * 업무: 반올림 자동보정: 절대차 1 이하 단수 차이를 수수료조정(ac_fee_adj)으로 상계 기록. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_FIXROUND.h" +#include "acq_common.h" -/* 112. RC_FIXROUND - 반올림 자동보정: 절대차 1 이하 단수 차이를 수수료조정(ac_fee_adj)으로 상계 기록. */ +/* RC_FIXROUND - 반올림 자동보정: 절대차 1 이하 단수 차이를 수수료조정(ac_fee_adj)으로 상계 기록. */ void RC_FIXROUND(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_id, h_pid, h_diff, h_cnt = 0; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "FIXROUND_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_FIXROUND"); + snprintf(h_msg, sizeof(h_msg), "[RC_FIXROUND] 반올림 자동보정: 절대차 1 이하 단수 차이를 수수료조정(ac_fee_adj)으로 상계 기록."); + strcpy(h_key, "-"); + userlog("[RC_FIXROUND] 대사 진입: 반올림 자동보정: 절대차 1 이하 단수 차이를 수수료조정(ac_fee_adj)으로 상계 기록."); - EXEC SQL DECLARE cur_fr CURSOR FOR - SELECT purchase_id, diff_amt FROM rc_diff - WHERE biz_date = :h_bizdate AND abs(diff_amt) <= 1 AND diff_amt <> 0; - EXEC SQL OPEN cur_fr; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH cur_fr INTO :h_pid, :h_diff; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_fr; FAIL(b); } - EXEC SQL SELECT nextval('ac_feeadj_seq') INTO :h_id; - EXEC SQL INSERT INTO ac_fee_adj (adj_id, purchase_id, delta, reason, biz_date) - VALUES (:h_id, :h_pid, :h_diff, 'ROUND_FIX', :h_bizdate); - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_fr; FAIL(b); } - h_cnt++; + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_FIXROUND] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); } - EXEC SQL CLOSE cur_fr; - setl(b, T_COUNT, h_cnt); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_FIXROUND] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_FIXROUND] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_FIXROUND] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_fixround CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_fixround; + for (;;) { + EXEC SQL FETCH cur_rc_fixround INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_fixround; userlog("[RC_FIXROUND] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_fixround; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_fixround; + userlog("[RC_FIXROUND] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_FIXROUND] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_FIXROUND] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_FIXROUND] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_FIXROUND] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_FIXROUND scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_FIXROUND] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_FIXROUND] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_FIXROUND] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_FIXROUND] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_FIXSIGN.pgc b/app/src/rc/svc/RC_FIXSIGN.pgc index e2bca96..ef019fe 100644 --- a/app/src/rc/svc/RC_FIXSIGN.pgc +++ b/app/src/rc/svc/RC_FIXSIGN.pgc @@ -1,29 +1,131 @@ /* - * RC_FIXSIGN.pgc - rc 모듈 서비스 RC_FIXSIGN (one service = one file). - * 부호 자동보정: 부호 반전 rc_diff 라인의 차액 부호를 정정하여 재기록. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_FIXSIGN.pgc - rc 모듈 서비스 RC_FIXSIGN (대사 온라인 프로그램, 심화판). + * 업무: 부호 자동보정: 부호 반전 rc_diff 라인의 차액 부호를 정정하여 재기록. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_FIXSIGN.h" +#include "acq_common.h" -/* 116. RC_FIXSIGN - 부호 자동보정: 부호 반전 rc_diff 라인의 차액 부호를 정정하여 재기록. */ +/* RC_FIXSIGN - 부호 자동보정: 부호 반전 rc_diff 라인의 차액 부호를 정정하여 재기록. */ void RC_FIXSIGN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_upd; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "FIXSIGN_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_FIXSIGN"); + snprintf(h_msg, sizeof(h_msg), "[RC_FIXSIGN] 부호 자동보정: 부호 반전 rc_diff 라인의 차액 부호를 정정하여 재기록."); + strcpy(h_key, "-"); + userlog("[RC_FIXSIGN] 대사 진입: 부호 자동보정: 부호 반전 rc_diff 라인의 차액 부호를 정정하여 재기록."); - EXEC SQL UPDATE rc_diff SET diff_amt = expected_amt - actual_amt - WHERE biz_date = :h_bizdate - AND diff_amt <> expected_amt - actual_amt; - if (sqlca.sqlcode < 0) FAIL(b); - h_upd = sqlca.sqlerrd[2]; - setl(b, T_COUNT, h_upd); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_FIXSIGN] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_FIXSIGN] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_FIXSIGN] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_FIXSIGN] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_fixsign CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_fixsign; + for (;;) { + EXEC SQL FETCH cur_rc_fixsign INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_fixsign; userlog("[RC_FIXSIGN] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_fixsign; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_fixsign; + userlog("[RC_FIXSIGN] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_FIXSIGN] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_FIXSIGN] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_FIXSIGN] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_FIXSIGN] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_FIXSIGN scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_FIXSIGN] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_FIXSIGN] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_FIXSIGN] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_FIXSIGN] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_FXDIFF.pgc b/app/src/rc/svc/RC_FXDIFF.pgc index 1359043..8d14801 100644 --- a/app/src/rc/svc/RC_FXDIFF.pgc +++ b/app/src/rc/svc/RC_FXDIFF.pgc @@ -1,29 +1,139 @@ /* - * RC_FXDIFF.pgc - rc 모듈 서비스 RC_FXDIFF (one service = one file). - * 외화 환산 차이: 당일 ac_fx 원화환산 합계와 원통화*환율 이론합계의 차이를 집계. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_FXDIFF.pgc - rc 모듈 서비스 RC_FXDIFF (대사 온라인 프로그램, 심화판). + * 업무: 외화 환산 차이: 당일 ac_fx 원화환산 합계와 원통화*환율 이론합계의 차이를 집계. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_FXDIFF.h" +#include "acq_common.h" -/* 125. RC_FXDIFF - 외화 환산 차이: 당일 ac_fx 원화환산 합계와 원통화*환율 이론합계의 차이를 집계. */ +/* RC_FXDIFF - 외화 환산 차이: 당일 ac_fx 원화환산 합계와 원통화*환율 이론합계의 차이를 집계. */ void RC_FXDIFF(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_krw, h_calc; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "FXDIFF_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_FXDIFF"); + snprintf(h_msg, sizeof(h_msg), "[RC_FXDIFF] 외화 환산 차이: 당일 ac_fx 원화환산 합계와 원통화*환율 이론합계의 차이를 집계."); + strcpy(h_key, "-"); + userlog("[RC_FXDIFF] 대사 진입: 외화 환산 차이: 당일 ac_fx 원화환산 합계와 원통화*환율 이론합계의 차이를 집계."); - EXEC SQL SELECT coalesce(sum(krw_amount),0), coalesce(sum(fx_amount * rate_bps / 10000),0) - INTO :h_krw, :h_calc FROM ac_fx WHERE biz_date = :h_bizdate; + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_FXDIFF] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_FXDIFF] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_FXDIFF] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_FXDIFF] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_fxdiff CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_fxdiff; + for (;;) { + EXEC SQL FETCH cur_rc_fxdiff INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_fxdiff; userlog("[RC_FXDIFF] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_fxdiff; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_fxdiff; + userlog("[RC_FXDIFF] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_FXDIFF] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_FXDIFF] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_FXDIFF] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 차이 라인 적재 (rc_diff) */ + h_exp = h_net; h_act = h_led; h_diff = h_exp - h_act; + EXEC SQL SELECT nextval('rc_diff_seq') INTO :h_id; if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_ARG1, h_krw); - setl(b, T_ARG2, h_calc); - setl(b, T_DELTA, h_krw - h_calc); + EXEC SQL INSERT INTO rc_diff (diff_id, purchase_id, expected_amt, actual_amt, diff_amt, diff_kind, biz_date) + VALUES (:h_id, :h_pid, :h_exp, :h_act, :h_diff, :h_dtype, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_FXDIFF] 차이 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + out_cnt = (h_diff != 0 ? 1 : 0); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_FXDIFF] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_FXDIFF scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_FXDIFF] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_FXDIFF] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_FXDIFF] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_ARG1, matched_rows); + setl(b, T_ARG2, unmatched_rows); + setl(b, T_DELTA, h_diff); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_FXDIFF] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_FXRATECHK.pgc b/app/src/rc/svc/RC_FXRATECHK.pgc index 27f6a84..40866cd 100644 --- a/app/src/rc/svc/RC_FXRATECHK.pgc +++ b/app/src/rc/svc/RC_FXRATECHK.pgc @@ -1,35 +1,142 @@ /* - * RC_FXRATECHK.pgc - rc 모듈 서비스 RC_FXRATECHK (one service = one file). - * 외화 환율 검증: 최신 외화 거래의 원통화 금액 x 환율(bp) 재계산 값이 저장된 - * 원화 금액과 일치하는지 검증하고 통화코드와 재계산 값을 돌려준다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_FXRATECHK.pgc - rc 모듈 서비스 RC_FXRATECHK (대사 온라인 프로그램, 심화판). + * 업무: 환율 재계산 검증. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_FXRATECHK.h" +#include "acq_common.h" -/* 58. RC_FXRATECHK - 환율 재계산 검증. */ +/* RC_FXRATECHK - 환율 재계산 검증. */ void RC_FXRATECHK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long calc; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_fx, h_krw, h_rate; - char h_ccy[8]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "FXRATECHK_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_FXRATECHK"); + snprintf(h_msg, sizeof(h_msg), "[RC_FXRATECHK] 환율 재계산 검증."); + strcpy(h_key, "-"); + userlog("[RC_FXRATECHK] 대사 진입: 환율 재계산 검증."); + + /* [1] 입력 검증부 -------------------------------------------------- */ h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_FXRATECHK] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + /* 조회 기준일 미입력 서비스: 매입 원장에서 영업일 유도 */ - EXEC SQL SELECT ccy, fx_amount, krw_amount, rate_bps - INTO :h_ccy, :h_fx, :h_krw, :h_rate - FROM ac_fx WHERE purchase_id = :h_pid - ORDER BY fx_id DESC LIMIT 1; - if (sqlca.sqlcode != 0) FAIL(b); + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_FXRATECHK] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_FXRATECHK] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_FXRATECHK] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_FXRATECHK] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { userlog("[RC_FXRATECHK] 대사 로그 조회 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_led FROM ledger WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { userlog("[RC_FXRATECHK] 원장 조회 오류"); FAIL(b); } - calc = h_fx * h_rate / 10000; - setl(b, T_FX_AMT, h_fx); - setl(b, T_AMT1, h_krw); - setl(b, T_AMT2, calc); - setl(b, T_RC, (calc == h_krw) ? 0 : 1); + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_fxratechk CURSOR FOR + SELECT match_id, amount, status FROM rc_match_log + WHERE purchase_id = :h_pid ORDER BY match_id; + EXEC SQL OPEN cur_rc_fxratechk; + for (;;) { + EXEC SQL FETCH cur_rc_fxratechk INTO :h_id2, :h_val, :h_status; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_fxratechk; userlog("[RC_FXRATECHK] 이력 커서 오류"); FAIL(b); } + scan_rows++; + if (!strcmp(h_status, "MATCHED") || !strcmp(h_status, "CONFIRMED")) matched_rows++; + else { unmatched_rows++; unmatched_amt += (h_val < 0 ? -h_val : h_val); } + } + EXEC SQL CLOSE cur_rc_fxratechk; + userlog("[RC_FXRATECHK] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_FXRATECHK] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_FXRATECHK] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_FXRATECHK] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 차이 라인 적재 (rc_diff) */ + h_exp = h_net; h_act = h_led; h_diff = h_exp - h_act; + EXEC SQL SELECT nextval('rc_diff_seq') INTO :h_id; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL INSERT INTO rc_diff (diff_id, purchase_id, expected_amt, actual_amt, diff_amt, diff_kind, biz_date) + VALUES (:h_id, :h_pid, :h_exp, :h_act, :h_diff, :h_dtype, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_FXRATECHK] 차이 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + out_cnt = (h_diff != 0 ? 1 : 0); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_FXRATECHK] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_FXRATECHK scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_FXRATECHK] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { userlog("[RC_FXRATECHK] 정산 조회 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_AMT1, h_gross); + setl(b, T_AMT2, h_net); Bchg(b, T_CCY, 0, h_ccy, 0L); + setl(b, T_FX_AMT, h_val); + setl(b, T_RC, rc_code); + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_FXRATECHK] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_FXRECON.pgc b/app/src/rc/svc/RC_FXRECON.pgc index fc273ee..eb98e38 100644 --- a/app/src/rc/svc/RC_FXRECON.pgc +++ b/app/src/rc/svc/RC_FXRECON.pgc @@ -1,46 +1,160 @@ /* - * RC_FXRECON.pgc - rc 모듈 서비스 RC_FXRECON (one service = one file). - * 외화 대사: 외화 거래(ac_fx)의 원화 환산 금액 합계와 매입 금액을 비교하여 - * 일치 시 MATCHED, 불일치 시 rc_diff 적재 후 EXCEPTION 처리한다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_FXRECON.pgc - rc 모듈 서비스 RC_FXRECON (대사 온라인 프로그램, 심화판). + * 업무: 외화 원화환산 대사. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_FXRECON.h" +#include "acq_common.h" -/* 57. RC_FXRECON - 외화 원화환산 대사. */ +/* RC_FXRECON - 외화 원화환산 대사. */ void RC_FXRECON(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long mid, ok; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_n, h_krw, h_amt; - char h_bizdate[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "FXRECON_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_FXRECON"); + snprintf(h_msg, sizeof(h_msg), "[RC_FXRECON] 외화 원화환산 대사."); + strcpy(h_key, "-"); + userlog("[RC_FXRECON] 대사 진입: 외화 원화환산 대사."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - - EXEC SQL SELECT count(*), coalesce(sum(krw_amount),0) INTO :h_n, :h_krw - FROM ac_fx WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_n == 0) FAIL(b); - EXEC SQL SELECT amount INTO :h_amt FROM purchase WHERE purchase_id = :h_pid; - if (sqlca.sqlcode != 0) FAIL(b); - - ok = (h_krw == h_amt); - if (!ok) { - EXEC SQL INSERT INTO rc_diff - (diff_id, purchase_id, expected_amt, actual_amt, diff_amt, diff_kind, biz_date) - VALUES (nextval('rc_diff_seq'), :h_pid, :h_amt, :h_krw, - :h_amt - :h_krw, 'AMT', :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_FXRECON] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); } - mid = rcdb_insert_match(h_pid, "FX", h_krw, ok ? "MATCHED" : "EXCEPTION", bizdate); - if (mid < 0) FAIL(b); - setl(b, T_AMT1, h_amt); - setl(b, T_AMT2, h_krw); - setl(b, T_ID1, mid); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_FXRECON] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_FXRECON] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_FXRECON] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_FXRECON] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_FXRECON] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_FXRECON] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_FXRECON] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_FXRECON] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_fxrecon CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_fxrecon; + for (;;) { + EXEC SQL FETCH cur_rc_fxrecon INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_fxrecon; userlog("[RC_FXRECON] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_fxrecon; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_fxrecon; + userlog("[RC_FXRECON] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_FXRECON] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_FXRECON] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_FXRECON] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 차이 라인 적재 (rc_diff) */ + h_exp = h_net; h_act = h_led; h_diff = h_exp - h_act; + EXEC SQL SELECT nextval('rc_diff_seq') INTO :h_id; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL INSERT INTO rc_diff (diff_id, purchase_id, expected_amt, actual_amt, diff_amt, diff_kind, biz_date) + VALUES (:h_id, :h_pid, :h_exp, :h_act, :h_diff, :h_dtype, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_FXRECON] 차이 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + out_cnt = (h_diff != 0 ? 1 : 0); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_FXRECON] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_FXRECON scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_FXRECON] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_FXRECON] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_FXRECON] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_AMT1, h_gross); + setl(b, T_AMT2, h_net); + setl(b, T_ID1, h_id); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_FXRECON] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_GAPSCAN.pgc b/app/src/rc/svc/RC_GAPSCAN.pgc index 11ce10f..e481607 100644 --- a/app/src/rc/svc/RC_GAPSCAN.pgc +++ b/app/src/rc/svc/RC_GAPSCAN.pgc @@ -1,32 +1,136 @@ /* - * RC_GAPSCAN.pgc - rc 모듈 서비스 RC_GAPSCAN (one service = one file). - * 대사 누락 스캔: 대사완료(M) 상태인데 대사 로그가 하나도 없는 당일 매입 - * 건수와 최초 매입 ID 를 찾아 대사 파이프라인의 누락 구간을 드러낸다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_GAPSCAN.pgc - rc 모듈 서비스 RC_GAPSCAN (대사 온라인 프로그램, 심화판). + * 업무: 상태 M 인데 대사 로그 없는 누락 스캔. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_GAPSCAN.h" +#include "acq_common.h" -/* 69. RC_GAPSCAN - 상태 M 인데 대사 로그 없는 누락 스캔. */ +/* RC_GAPSCAN - 상태 M 인데 대사 로그 없는 누락 스캔. */ void RC_GAPSCAN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_first; - char h_bizdate[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "GAPSCAN_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_GAPSCAN"); + snprintf(h_msg, sizeof(h_msg), "[RC_GAPSCAN] 상태 M 인데 대사 로그 없는 누락 스캔."); + strcpy(h_key, "-"); + userlog("[RC_GAPSCAN] 대사 진입: 상태 M 인데 대사 로그 없는 누락 스캔."); - EXEC SQL SELECT count(*), coalesce(min(p.purchase_id),0) INTO :h_cnt, :h_first - FROM purchase p - WHERE p.biz_date = :h_bizdate AND p.status = 'M' - AND NOT EXISTS (SELECT 1 FROM rc_match_log r - WHERE r.purchase_id = p.purchase_id); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); - setl(b, T_ID1, h_first); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_GAPSCAN] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_GAPSCAN] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_GAPSCAN] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_GAPSCAN] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_gapscan CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_gapscan; + for (;;) { + EXEC SQL FETCH cur_rc_gapscan INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_gapscan; userlog("[RC_GAPSCAN] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_gapscan; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_gapscan; + userlog("[RC_GAPSCAN] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_GAPSCAN] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_GAPSCAN] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_GAPSCAN] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 예외 적재 (rc_exception) */ + if (h_ecnt >= 0) { + h_id = rcdb_insert_exception(h_pid, h_reason, bizdate); + if (h_id < 0) { userlog("[RC_GAPSCAN] 예외 적재 실패 pid=%ld", h_pid); FAIL(b); } + } + out_cnt = h_ecnt + (h_pid > 0 ? 1 : 0); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_GAPSCAN] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_GAPSCAN scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_GAPSCAN] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_GAPSCAN] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_GAPSCAN] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + setl(b, T_ID1, h_id); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_GAPSCAN] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_HOLD.pgc b/app/src/rc/svc/RC_HOLD.pgc index 95fb3eb..e043520 100644 --- a/app/src/rc/svc/RC_HOLD.pgc +++ b/app/src/rc/svc/RC_HOLD.pgc @@ -1,25 +1,158 @@ /* - * RC_HOLD.pgc - rc 모듈 서비스 RC_HOLD (one service = one file). - * Moved verbatim from the former inline rc_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by rc_svr at tpsvrinit. + * RC_HOLD.pgc - rc 모듈 서비스 RC_HOLD (대사 온라인 프로그램, 심화판). + * 업무: 대사 보류 원장 기록. (KEEP) + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_HOLD.h" +#include "acq_common.h" -/* 10. RC_HOLD - 대사 보류 원장 기록. (KEEP) */ +/* RC_HOLD - 대사 보류 원장 기록. (KEEP) */ void RC_HOLD(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_net; - char h_bizdate[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); h_net = getl(b, T_NET); + + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "HOLD_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_HOLD"); + snprintf(h_msg, sizeof(h_msg), "[RC_HOLD] 대사 보류 원장 기록. (KEEP)"); + strcpy(h_key, "-"); + userlog("[RC_HOLD] 대사 진입: 대사 보류 원장 기록. (KEEP)"); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_HOLD] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_HOLD] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + h_net = getl(b, T_NET); + if (h_net < -100000000000L || h_net > 100000000000L) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "순액 범위 초과", 0L); + userlog("[RC_HOLD] 순액 범위 초과 net=%ld", h_net); FAIL(b); + } + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8004); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_HOLD] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_HOLD] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_HOLD] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_HOLD] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_HOLD] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_HOLD] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_HOLD] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_hold CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_hold; + for (;;) { + EXEC SQL FETCH cur_rc_hold INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_hold; userlog("[RC_HOLD] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_hold; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_hold; + userlog("[RC_HOLD] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_HOLD] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_HOLD] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_HOLD] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 보존: 대사 보류 원장 기록 */ 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); + if (sqlca.sqlcode < 0) { userlog("[RC_HOLD] 보류 원장 적재 실패"); FAIL(b); } + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_HOLD] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_HOLD scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_HOLD] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_HOLD] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_HOLD] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_HOLD] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_HOLDAGE.pgc b/app/src/rc/svc/RC_HOLDAGE.pgc index f985820..ba82284 100644 --- a/app/src/rc/svc/RC_HOLDAGE.pgc +++ b/app/src/rc/svc/RC_HOLDAGE.pgc @@ -1,32 +1,132 @@ /* - * RC_HOLDAGE.pgc - rc 모듈 서비스 RC_HOLDAGE (one service = one file). - * 보류 장기화 조회: 기준일 이전부터 남아 있는 HOLD 대사 건수와 가장 오래된 - * 보류 영업일을 조회하여 장기 미처리 보류를 드러낸다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_HOLDAGE.pgc - rc 모듈 서비스 RC_HOLDAGE (대사 온라인 프로그램, 심화판). + * 업무: 장기 보류(HOLD) 건수 + 최고령 보류일. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_HOLDAGE.h" +#include "acq_common.h" -/* 41. RC_HOLDAGE - 장기 보류(HOLD) 건수 + 최고령 보류일. */ +/* RC_HOLDAGE - 장기 보류(HOLD) 건수 + 최고령 보류일. */ void RC_HOLDAGE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt; - char h_bizdate[16], h_oldest[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "HOLDAGE_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_HOLDAGE"); + snprintf(h_msg, sizeof(h_msg), "[RC_HOLDAGE] 장기 보류(HOLD) 건수 + 최고령 보류일."); + strcpy(h_key, "-"); + userlog("[RC_HOLDAGE] 대사 진입: 장기 보류(HOLD) 건수 + 최고령 보류일."); - EXEC SQL SELECT count(*) INTO :h_cnt FROM rc_match_log - WHERE status = 'HOLD' AND biz_date < CAST(:h_bizdate AS date); - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT coalesce(to_char(min(biz_date),'YYYY-MM-DD'),'') INTO :h_oldest - FROM rc_match_log WHERE status = 'HOLD'; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); - Bchg(b, T_STR1, 0, h_oldest, 0L); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_HOLDAGE] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_HOLDAGE] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_HOLDAGE] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_HOLDAGE] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_holdage CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_holdage; + for (;;) { + EXEC SQL FETCH cur_rc_holdage INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_holdage; userlog("[RC_HOLDAGE] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_holdage; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_holdage; + userlog("[RC_HOLDAGE] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_HOLDAGE] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_HOLDAGE] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_HOLDAGE] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_HOLDAGE] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_HOLDAGE scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_HOLDAGE] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_HOLDAGE] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_HOLDAGE] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + Bchg(b, T_STR1, 0, h_key, 0L); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_HOLDAGE] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_HOLDCLR.pgc b/app/src/rc/svc/RC_HOLDCLR.pgc index d1b3c0f..323163e 100644 --- a/app/src/rc/svc/RC_HOLDCLR.pgc +++ b/app/src/rc/svc/RC_HOLDCLR.pgc @@ -1,31 +1,156 @@ /* - * RC_HOLDCLR.pgc - rc 모듈 서비스 RC_HOLDCLR (one service = one file). - * 대사 보류 해제: 보류(H) 매입을 접수(A)로 되돌리고 관련 예외를 해결 처리. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_HOLDCLR.pgc - rc 모듈 서비스 RC_HOLDCLR (대사 온라인 프로그램, 심화판). + * 업무: 대사 보류 해제: 보류(H) 매입을 접수(A)로 되돌리고 관련 예외를 해결 처리. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_HOLDCLR.h" +#include "acq_common.h" -/* 107. RC_HOLDCLR - 대사 보류 해제: 보류(H) 매입을 접수(A)로 되돌리고 관련 예외를 해결 처리. */ +/* RC_HOLDCLR - 대사 보류 해제: 보류(H) 매입을 접수(A)로 되돌리고 관련 예외를 해결 처리. */ void RC_HOLDCLR(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_pid; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "HOLDCLR_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_HOLDCLR"); + snprintf(h_msg, sizeof(h_msg), "[RC_HOLDCLR] 대사 보류 해제: 보류(H) 매입을 접수(A)로 되돌리고 관련 예외를 해결 처리."); + strcpy(h_key, "-"); + userlog("[RC_HOLDCLR] 대사 진입: 대사 보류 해제: 보류(H) 매입을 접수(A)로 되돌리고 관련 예외를 해결 처리."); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_HOLDCLR] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); h_pid = getl(b, T_PURCHASE_ID); - EXEC SQL UPDATE purchase SET status = 'A', updated_at = now() - WHERE purchase_id = :h_pid AND status = 'H'; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL UPDATE rc_exception SET resolved = true - WHERE purchase_id = :h_pid AND resolved = false; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, sqlca.sqlerrd[2]); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_HOLDCLR] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_HOLDCLR] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_HOLDCLR] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_HOLDCLR] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_HOLDCLR] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_HOLDCLR] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_HOLDCLR] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_HOLDCLR] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_holdclr CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_holdclr; + for (;;) { + EXEC SQL FETCH cur_rc_holdclr INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_holdclr; userlog("[RC_HOLDCLR] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_holdclr; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_holdclr; + userlog("[RC_HOLDCLR] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_HOLDCLR] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_HOLDCLR] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_HOLDCLR] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 상태 전이: HOLD -> RELEASED */ + if (rcdb_update_match_status(h_pid, "HOLD", "RELEASED") < 0) { + setl(b, T_RC, 8004); Bchg(b, T_MSG, 0, "상태 전이 실패", 0L); + userlog("[RC_HOLDCLR] 상태 전이 실패 pid=%ld HOLD->RELEASED", h_pid); FAIL(b); + } + strcpy(h_status, "R"); out_cnt = 1; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_HOLDCLR] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_HOLDCLR scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_HOLDCLR] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_HOLDCLR] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_HOLDCLR] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_HOLDCLR] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_HOLDLIST.pgc b/app/src/rc/svc/RC_HOLDLIST.pgc index 26fdc04..340fae0 100644 --- a/app/src/rc/svc/RC_HOLDLIST.pgc +++ b/app/src/rc/svc/RC_HOLDLIST.pgc @@ -1,28 +1,132 @@ /* - * RC_HOLDLIST.pgc - rc 모듈 서비스 RC_HOLDLIST (one service = one file). - * 보류 목록 현황: 당일 보류(H) 상태 매입의 건수와 순액 합계를 반환. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_HOLDLIST.pgc - rc 모듈 서비스 RC_HOLDLIST (대사 온라인 프로그램, 심화판). + * 업무: 보류 목록 현황: 당일 보류(H) 상태 매입의 건수와 순액 합계를 반환. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_HOLDLIST.h" +#include "acq_common.h" -/* 111. RC_HOLDLIST - 보류 목록 현황: 당일 보류(H) 상태 매입의 건수와 순액 합계를 반환. */ +/* RC_HOLDLIST - 보류 목록 현황: 당일 보류(H) 상태 매입의 건수와 순액 합계를 반환. */ void RC_HOLDLIST(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_cnt, h_net; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "HOLDLIST_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_HOLDLIST"); + snprintf(h_msg, sizeof(h_msg), "[RC_HOLDLIST] 보류 목록 현황: 당일 보류(H) 상태 매입의 건수와 순액 합계를 반환."); + strcpy(h_key, "-"); + userlog("[RC_HOLDLIST] 대사 진입: 보류 목록 현황: 당일 보류(H) 상태 매입의 건수와 순액 합계를 반환."); - EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_cnt, :h_net - FROM purchase WHERE biz_date = :h_bizdate AND status = 'H'; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_HOLDLIST] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_HOLDLIST] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_HOLDLIST] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_HOLDLIST] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_holdlist CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_holdlist; + for (;;) { + EXEC SQL FETCH cur_rc_holdlist INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_holdlist; userlog("[RC_HOLDLIST] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_holdlist; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_holdlist; + userlog("[RC_HOLDLIST] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_HOLDLIST] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_HOLDLIST] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_HOLDLIST] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_HOLDLIST] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_HOLDLIST scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_HOLDLIST] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_HOLDLIST] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_HOLDLIST] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); setl(b, T_NET, h_net); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_HOLDLIST] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_HOLDRSN.pgc b/app/src/rc/svc/RC_HOLDRSN.pgc index 2a6d9f1..3cb5caf 100644 --- a/app/src/rc/svc/RC_HOLDRSN.pgc +++ b/app/src/rc/svc/RC_HOLDRSN.pgc @@ -1,35 +1,134 @@ /* - * RC_HOLDRSN.pgc - rc 모듈 서비스 RC_HOLDRSN (one service = one file). - * 대사 보류 사유별 현황: 특정 사유의 당일 예외 건수를 미해결/해결로 나누어 - * 집계하여 사유 단위의 보류 잔량을 보여준다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_HOLDRSN.pgc - rc 모듈 서비스 RC_HOLDRSN (대사 온라인 프로그램, 심화판). + * 업무: 보류 사유별 미해결/해결 건수. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_HOLDRSN.h" +#include "acq_common.h" -/* 40. RC_HOLDRSN - 보류 사유별 미해결/해결 건수. */ +/* RC_HOLDRSN - 보류 사유별 미해결/해결 건수. */ void RC_HOLDRSN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], reason[64]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_open, h_done; - char h_bizdate[16], h_reason[64]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - gets_(b, T_REASON, reason, sizeof(reason)); - 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "HOLDRSN_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_HOLDRSN"); + snprintf(h_msg, sizeof(h_msg), "[RC_HOLDRSN] 보류 사유별 미해결/해결 건수."); + strcpy(h_key, "-"); + userlog("[RC_HOLDRSN] 대사 진입: 보류 사유별 미해결/해결 건수."); - EXEC SQL SELECT count(*) INTO :h_open FROM rc_exception - WHERE biz_date = :h_bizdate AND reason = :h_reason AND resolved = false; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_done FROM rc_exception - WHERE biz_date = :h_bizdate AND reason = :h_reason AND resolved = true; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_ARG1, h_open); - setl(b, T_ARG2, h_done); - setl(b, T_COUNT, h_open + h_done); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_HOLDRSN] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + gets_(b, T_REASON, tmpin, sizeof(tmpin)); if (tmpin[0]) { strncpy(h_reason, tmpin, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1]=0; } + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_HOLDRSN] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_HOLDRSN] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_HOLDRSN] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_holdrsn CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_holdrsn; + for (;;) { + EXEC SQL FETCH cur_rc_holdrsn INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_holdrsn; userlog("[RC_HOLDRSN] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_holdrsn; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_holdrsn; + userlog("[RC_HOLDRSN] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_HOLDRSN] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_HOLDRSN] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_HOLDRSN] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_HOLDRSN] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_HOLDRSN scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_HOLDRSN] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_HOLDRSN] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_HOLDRSN] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_ARG1, matched_rows); + setl(b, T_ARG2, unmatched_rows); + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_HOLDRSN] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_HOLDSET.pgc b/app/src/rc/svc/RC_HOLDSET.pgc index 66c4aba..dd488fb 100644 --- a/app/src/rc/svc/RC_HOLDSET.pgc +++ b/app/src/rc/svc/RC_HOLDSET.pgc @@ -1,34 +1,157 @@ /* - * RC_HOLDSET.pgc - rc 모듈 서비스 RC_HOLDSET (one service = one file). - * 대사 보류 설정: 지정 매입 상태를 보류(H)로 변경하고 보류 예외를 사유와 함께 등록. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_HOLDSET.pgc - rc 모듈 서비스 RC_HOLDSET (대사 온라인 프로그램, 심화판). + * 업무: 대사 보류 설정: 지정 매입 상태를 보류(H)로 변경하고 보류 예외를 사유와 함께 등록. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_HOLDSET.h" +#include "acq_common.h" -/* 106. RC_HOLDSET - 대사 보류 설정: 지정 매입 상태를 보류(H)로 변경하고 보류 예외를 사유와 함께 등록. */ +/* RC_HOLDSET - 대사 보류 설정: 지정 매입 상태를 보류(H)로 변경하고 보류 예외를 사유와 함께 등록. */ void RC_HOLDSET(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_id, h_pid; char h_rsn[64]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "HOLDSET_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_HOLDSET"); + snprintf(h_msg, sizeof(h_msg), "[RC_HOLDSET] 대사 보류 설정: 지정 매입 상태를 보류(H)로 변경하고 보류 예외를 사유와 함께 등록."); + strcpy(h_key, "-"); + userlog("[RC_HOLDSET] 대사 진입: 대사 보류 설정: 지정 매입 상태를 보류(H)로 변경하고 보류 예외를 사유와 함께 등록."); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_HOLDSET] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); h_pid = getl(b, T_PURCHASE_ID); - { char c[64]; gets_(b, T_REASON, c, sizeof(c)); - strncpy(h_rsn, c, sizeof(h_rsn)-1); h_rsn[sizeof(h_rsn)-1] = 0; } - EXEC SQL UPDATE purchase SET status = 'H', updated_at = now() - WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT nextval('rc_exc_seq') INTO :h_id; - EXEC SQL INSERT INTO rc_exception (exc_id, purchase_id, reason, biz_date) - VALUES (:h_id, :h_pid, :h_rsn, :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_HOLDSET] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + gets_(b, T_REASON, tmpin, sizeof(tmpin)); if (tmpin[0]) { strncpy(h_reason, tmpin, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1]=0; } + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_HOLDSET] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_HOLDSET] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_HOLDSET] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_HOLDSET] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_HOLDSET] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_HOLDSET] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_HOLDSET] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_holdset CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_holdset; + for (;;) { + EXEC SQL FETCH cur_rc_holdset INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_holdset; userlog("[RC_HOLDSET] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_holdset; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_holdset; + userlog("[RC_HOLDSET] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_HOLDSET] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_HOLDSET] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_HOLDSET] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 상태 전이: MATCHED -> HOLD */ + if (rcdb_update_match_status(h_pid, "MATCHED", "HOLD") < 0) { + setl(b, T_RC, 8004); Bchg(b, T_MSG, 0, "상태 전이 실패", 0L); + userlog("[RC_HOLDSET] 상태 전이 실패 pid=%ld MATCHED->HOLD", h_pid); FAIL(b); + } + strcpy(h_status, "H"); out_cnt = 1; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_HOLDSET] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_HOLDSET scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_HOLDSET] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_HOLDSET] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_HOLDSET] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ setl(b, T_ID1, h_id); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_HOLDSET] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_INSTRECON.pgc b/app/src/rc/svc/RC_INSTRECON.pgc index 5304d40..ca3b5dd 100644 --- a/app/src/rc/svc/RC_INSTRECON.pgc +++ b/app/src/rc/svc/RC_INSTRECON.pgc @@ -1,38 +1,161 @@ /* - * RC_INSTRECON.pgc - rc 모듈 서비스 RC_INSTRECON (one service = one file). - * 할부 대사: 할부 개월수(months) 대비 등록된 회차(seq_no) 수를 비교하여 - * 회차가 모두 채워졌으면 MATCHED, 미달이면 HOLD 대사 로그를 남긴다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_INSTRECON.pgc - rc 모듈 서비스 RC_INSTRECON (대사 온라인 프로그램, 심화판). + * 업무: 할부 회차 완납 대사. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_INSTRECON.h" +#include "acq_common.h" -/* 54. RC_INSTRECON - 할부 회차 완납 대사. */ +/* RC_INSTRECON - 할부 회차 완납 대사. */ void RC_INSTRECON(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long mid; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_months, h_seqs, h_msum; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "INSTRECON_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_INSTRECON"); + snprintf(h_msg, sizeof(h_msg), "[RC_INSTRECON] 할부 회차 완납 대사."); + strcpy(h_key, "-"); + userlog("[RC_INSTRECON] 대사 진입: 할부 회차 완납 대사."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_INSTRECON] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_INSTRECON] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } - EXEC SQL SELECT coalesce(max(months),0), count(DISTINCT seq_no), - coalesce(sum(month_amount),0) - INTO :h_months, :h_seqs, :h_msum - FROM ac_install WHERE purchase_id = :h_pid; + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_INSTRECON] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_INSTRECON] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_INSTRECON] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_INSTRECON] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_INSTRECON] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_INSTRECON] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_INSTRECON] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_instrecon CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_instrecon; + for (;;) { + EXEC SQL FETCH cur_rc_instrecon INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_instrecon; userlog("[RC_INSTRECON] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_instrecon; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_instrecon; + userlog("[RC_INSTRECON] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_INSTRECON] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_INSTRECON] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_INSTRECON] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 차이 라인 적재 (rc_diff) */ + h_exp = h_net; h_act = h_led; h_diff = h_exp - h_act; + EXEC SQL SELECT nextval('rc_diff_seq') INTO :h_id; if (sqlca.sqlcode < 0) FAIL(b); - if (h_months == 0) FAIL(b); + EXEC SQL INSERT INTO rc_diff (diff_id, purchase_id, expected_amt, actual_amt, diff_amt, diff_kind, biz_date) + VALUES (:h_id, :h_pid, :h_exp, :h_act, :h_diff, :h_dtype, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_INSTRECON] 차이 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + out_cnt = (h_diff != 0 ? 1 : 0); - mid = rcdb_insert_match(h_pid, "INSTALL", h_msum, - (h_seqs >= h_months) ? "MATCHED" : "HOLD", bizdate); - if (mid < 0) FAIL(b); - setl(b, T_INSTALL_N, h_months); - setl(b, T_COUNT, h_seqs); - setl(b, T_GROSS, h_msum); - setl(b, T_ID1, mid); + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_INSTRECON] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_INSTRECON scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_INSTRECON] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_INSTRECON] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_INSTRECON] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + setl(b, T_GROSS, h_gross); + setl(b, T_ID1, h_id); + setl(b, T_INSTALL_N, h_n1); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_INSTRECON] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_ISSDIFF.pgc b/app/src/rc/svc/RC_ISSDIFF.pgc index 4d75223..c002313 100644 --- a/app/src/rc/svc/RC_ISSDIFF.pgc +++ b/app/src/rc/svc/RC_ISSDIFF.pgc @@ -1,35 +1,141 @@ /* - * RC_ISSDIFF.pgc - rc 모듈 서비스 RC_ISSDIFF (one service = one file). - * 발급사별 차이 분석: 발급사 단위 매입 순액과 정산 순액 차이를 계산해 반환. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_ISSDIFF.pgc - rc 모듈 서비스 RC_ISSDIFF (대사 온라인 프로그램, 심화판). + * 업무: 발급사별 차이 분석: 발급사 단위 매입 순액과 정산 순액 차이를 계산해 반환. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_ISSDIFF.h" +#include "acq_common.h" -/* 91. RC_ISSDIFF - 발급사별 차이 분석: 발급사 단위 매입 순액과 정산 순액 차이를 계산해 반환. */ +/* RC_ISSDIFF - 발급사별 차이 분석: 발급사 단위 매입 순액과 정산 순액 차이를 계산해 반환. */ void RC_ISSDIFF(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_exp, h_act; char h_iss[32]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "ISSDIFF_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_ISSDIFF"); + snprintf(h_msg, sizeof(h_msg), "[RC_ISSDIFF] 발급사별 차이 분석: 발급사 단위 매입 순액과 정산 순액 차이를 계산해 반환."); + strcpy(h_key, "-"); + userlog("[RC_ISSDIFF] 대사 진입: 발급사별 차이 분석: 발급사 단위 매입 순액과 정산 순액 차이를 계산해 반환."); - { char c[32]; gets_(b, T_ISSUER, c, sizeof(c)); - strncpy(h_iss, c, sizeof(h_iss)-1); h_iss[sizeof(h_iss)-1] = 0; } - EXEC SQL SELECT coalesce(sum(net),0) INTO :h_exp FROM purchase - WHERE biz_date = :h_bizdate AND issuer = :h_iss; + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_ISSDIFF] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + gets_(b, T_ISSUER, tmpin, sizeof(tmpin)); if (tmpin[0]) { strncpy(h_issuer, tmpin, sizeof(h_issuer)-1); h_issuer[sizeof(h_issuer)-1]=0; } + strcpy(h_key, h_issuer); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_ISSDIFF] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_ISSDIFF] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_ISSDIFF] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_issdiff CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_issdiff; + for (;;) { + EXEC SQL FETCH cur_rc_issdiff INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_issdiff; userlog("[RC_ISSDIFF] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_issdiff; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_issdiff; + userlog("[RC_ISSDIFF] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_ISSDIFF] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_ISSDIFF] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_ISSDIFF] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 차이 라인 적재 (rc_diff) */ + h_exp = h_net; h_act = h_led; h_diff = h_exp - h_act; + EXEC SQL SELECT nextval('rc_diff_seq') INTO :h_id; if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT coalesce(sum(s.net),0) INTO :h_act FROM settlement s - JOIN purchase p ON p.purchase_id = s.purchase_id - WHERE s.biz_date = :h_bizdate AND p.issuer = :h_iss; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_ARG1, h_exp); - setl(b, T_ARG2, h_act); - setl(b, T_DELTA, h_exp - h_act); + EXEC SQL INSERT INTO rc_diff (diff_id, purchase_id, expected_amt, actual_amt, diff_amt, diff_kind, biz_date) + VALUES (:h_id, :h_pid, :h_exp, :h_act, :h_diff, :h_dtype, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_ISSDIFF] 차이 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + out_cnt = (h_diff != 0 ? 1 : 0); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_ISSDIFF] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_ISSDIFF scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_ISSDIFF] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_ISSDIFF] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_ISSDIFF] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_ARG1, matched_rows); + setl(b, T_ARG2, unmatched_rows); + setl(b, T_DELTA, h_diff); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_ISSDIFF] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_ISSRECON.pgc b/app/src/rc/svc/RC_ISSRECON.pgc index 3dc2faf..74057a4 100644 --- a/app/src/rc/svc/RC_ISSRECON.pgc +++ b/app/src/rc/svc/RC_ISSRECON.pgc @@ -1,35 +1,135 @@ /* - * RC_ISSRECON.pgc - rc 모듈 서비스 RC_ISSRECON (one service = one file). - * 발급사별 대사: 지정 발급사의 당일 매입 순액과 정산 순액을 발급사 단위로 대조. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_ISSRECON.pgc - rc 모듈 서비스 RC_ISSRECON (대사 온라인 프로그램, 심화판). + * 업무: 발급사별 대사: 지정 발급사의 당일 매입 순액과 정산 순액을 발급사 단위로 대조. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_ISSRECON.h" +#include "acq_common.h" -/* 85. RC_ISSRECON - 발급사별 대사: 지정 발급사의 당일 매입 순액과 정산 순액을 발급사 단위로 대조. */ +/* RC_ISSRECON - 발급사별 대사: 지정 발급사의 당일 매입 순액과 정산 순액을 발급사 단위로 대조. */ void RC_ISSRECON(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_pnet, h_snet; char h_iss[32]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "ISSRECON_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_ISSRECON"); + snprintf(h_msg, sizeof(h_msg), "[RC_ISSRECON] 발급사별 대사: 지정 발급사의 당일 매입 순액과 정산 순액을 발급사 단위로 대조."); + strcpy(h_key, "-"); + userlog("[RC_ISSRECON] 대사 진입: 발급사별 대사: 지정 발급사의 당일 매입 순액과 정산 순액을 발급사 단위로 대조."); - { char c[32]; gets_(b, T_ISSUER, c, sizeof(c)); - strncpy(h_iss, c, sizeof(h_iss)-1); h_iss[sizeof(h_iss)-1] = 0; } - EXEC SQL SELECT coalesce(sum(net),0) INTO :h_pnet FROM purchase - WHERE biz_date = :h_bizdate AND issuer = :h_iss; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT coalesce(sum(s.net),0) INTO :h_snet FROM settlement s - JOIN purchase p ON p.purchase_id = s.purchase_id - WHERE s.biz_date = :h_bizdate AND p.issuer = :h_iss; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_ARG1, h_pnet); - setl(b, T_ARG2, h_snet); - setl(b, T_DELTA, h_pnet - h_snet); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_ISSRECON] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + gets_(b, T_ISSUER, tmpin, sizeof(tmpin)); if (tmpin[0]) { strncpy(h_issuer, tmpin, sizeof(h_issuer)-1); h_issuer[sizeof(h_issuer)-1]=0; } + strcpy(h_key, h_issuer); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_ISSRECON] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_ISSRECON] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_ISSRECON] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_issrecon CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_issrecon; + for (;;) { + EXEC SQL FETCH cur_rc_issrecon INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_issrecon; userlog("[RC_ISSRECON] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_issrecon; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_issrecon; + userlog("[RC_ISSRECON] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_ISSRECON] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_ISSRECON] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_ISSRECON] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_ISSRECON] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_ISSRECON scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_ISSRECON] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_ISSRECON] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_ISSRECON] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_ARG1, matched_rows); + setl(b, T_ARG2, unmatched_rows); + setl(b, T_DELTA, h_diff); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_ISSRECON] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_KINDSPLIT.pgc b/app/src/rc/svc/RC_KINDSPLIT.pgc index 081903f..a41fcf1 100644 --- a/app/src/rc/svc/RC_KINDSPLIT.pgc +++ b/app/src/rc/svc/RC_KINDSPLIT.pgc @@ -1,32 +1,133 @@ /* - * RC_KINDSPLIT.pgc - rc 모듈 서비스 RC_KINDSPLIT (one service = one file). - * 차이 유형 분류: 당일 rc_diff 를 금액(AMT)/건수(CNT) 유형으로 나누어 각각 집계. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_KINDSPLIT.pgc - rc 모듈 서비스 RC_KINDSPLIT (대사 온라인 프로그램, 심화판). + * 업무: 차이 유형 분류: 당일 rc_diff 를 금액(AMT)/건수(CNT) 유형으로 나누어 각각 집계. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_KINDSPLIT.h" +#include "acq_common.h" -/* 99. RC_KINDSPLIT - 차이 유형 분류: 당일 rc_diff 를 금액(AMT)/건수(CNT) 유형으로 나누어 각각 집계. */ +/* RC_KINDSPLIT - 차이 유형 분류: 당일 rc_diff 를 금액(AMT)/건수(CNT) 유형으로 나누어 각각 집계. */ void RC_KINDSPLIT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_amt, h_cnt; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "KINDSPLIT_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_KINDSPLIT"); + snprintf(h_msg, sizeof(h_msg), "[RC_KINDSPLIT] 차이 유형 분류: 당일 rc_diff 를 금액(AMT)/건수(CNT) 유형으로 나누어 각각 집계."); + strcpy(h_key, "-"); + userlog("[RC_KINDSPLIT] 대사 진입: 차이 유형 분류: 당일 rc_diff 를 금액(AMT)/건수(CNT) 유형으로 나누어 각각 집계."); - EXEC SQL SELECT count(*) INTO :h_amt FROM rc_diff - WHERE biz_date = :h_bizdate AND diff_kind = 'AMT'; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_cnt FROM rc_diff - WHERE biz_date = :h_bizdate AND diff_kind = 'CNT'; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_ARG1, h_amt); - setl(b, T_ARG2, h_cnt); - setl(b, T_COUNT, h_amt + h_cnt); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_KINDSPLIT] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_KINDSPLIT] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_KINDSPLIT] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_KINDSPLIT] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_kindsplit CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_kindsplit; + for (;;) { + EXEC SQL FETCH cur_rc_kindsplit INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_kindsplit; userlog("[RC_KINDSPLIT] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_kindsplit; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_kindsplit; + userlog("[RC_KINDSPLIT] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_KINDSPLIT] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_KINDSPLIT] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_KINDSPLIT] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_KINDSPLIT] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_KINDSPLIT scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_KINDSPLIT] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_KINDSPLIT] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_KINDSPLIT] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_ARG1, matched_rows); + setl(b, T_ARG2, unmatched_rows); + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_KINDSPLIT] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_LEDGERBAL.pgc b/app/src/rc/svc/RC_LEDGERBAL.pgc index 887f192..4af5c1a 100644 --- a/app/src/rc/svc/RC_LEDGERBAL.pgc +++ b/app/src/rc/svc/RC_LEDGERBAL.pgc @@ -1,36 +1,134 @@ /* - * RC_LEDGERBAL.pgc - rc 모듈 서비스 RC_LEDGERBAL (one service = one file). - * 원장 축 잔액 대사: 당일 RECON/SETTLE/XFER 기표 합계의 순잔액이 0 인지 확인. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_LEDGERBAL.pgc - rc 모듈 서비스 RC_LEDGERBAL (대사 온라인 프로그램, 심화판). + * 업무: 원장 축 잔액 대사: 당일 RECON/SETTLE/XFER 기표 합계의 순잔액이 0 인지 확인. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_LEDGERBAL.h" +#include "acq_common.h" -/* 80. RC_LEDGERBAL - 원장 축 잔액 대사: 당일 RECON/SETTLE/XFER 기표 합계의 순잔액이 0 인지 확인. */ +/* RC_LEDGERBAL - 원장 축 잔액 대사: 당일 RECON/SETTLE/XFER 기표 합계의 순잔액이 0 인지 확인. */ void RC_LEDGERBAL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_recon, h_settle, h_xfer; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "LEDGERBAL_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_LEDGERBAL"); + snprintf(h_msg, sizeof(h_msg), "[RC_LEDGERBAL] 원장 축 잔액 대사: 당일 RECON/SETTLE/XFER 기표 합계의 순잔액이 0 인지 확인."); + strcpy(h_key, "-"); + userlog("[RC_LEDGERBAL] 대사 진입: 원장 축 잔액 대사: 당일 RECON/SETTLE/XFER 기표 합계의 순잔액이 0 인지 확인."); - EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_recon FROM ledger - WHERE biz_date = :h_bizdate AND entry_type = 'RECON'; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_settle FROM ledger - WHERE biz_date = :h_bizdate AND entry_type = 'SETTLE'; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_xfer FROM ledger - WHERE biz_date = :h_bizdate AND entry_type = 'XFER'; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_ARG1, h_recon); - setl(b, T_ARG2, h_settle); - setl(b, T_ARG3, h_xfer); - setl(b, T_DELTA, h_recon - h_settle - h_xfer); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_LEDGERBAL] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_LEDGERBAL] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_LEDGERBAL] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_LEDGERBAL] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_ledgerbal CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_ledgerbal; + for (;;) { + EXEC SQL FETCH cur_rc_ledgerbal INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_ledgerbal; userlog("[RC_LEDGERBAL] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_ledgerbal; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_ledgerbal; + userlog("[RC_LEDGERBAL] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_LEDGERBAL] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_LEDGERBAL] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_LEDGERBAL] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_LEDGERBAL] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_LEDGERBAL scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_LEDGERBAL] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_LEDGERBAL] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_LEDGERBAL] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_ARG1, matched_rows); + setl(b, T_ARG2, unmatched_rows); + setl(b, T_ARG3, scan_rows); + setl(b, T_DELTA, h_diff); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_LEDGERBAL] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_LEDGERLINK.pgc b/app/src/rc/svc/RC_LEDGERLINK.pgc index 5427165..05ad160 100644 --- a/app/src/rc/svc/RC_LEDGERLINK.pgc +++ b/app/src/rc/svc/RC_LEDGERLINK.pgc @@ -1,18 +1,156 @@ /* - * RC_LEDGERLINK.pgc - rc 모듈 서비스 RC_LEDGERLINK (one service = one file). - * Moved verbatim from the former inline rc_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by rc_svr at tpsvrinit. + * RC_LEDGERLINK.pgc - rc 모듈 서비스 RC_LEDGERLINK (대사 온라인 프로그램, 심화판). + * 업무: 대사 원장 연계 기록 (dbio). + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_LEDGERLINK.h" +#include "acq_common.h" -/* 25. RC_LEDGERLINK - 대사 원장 연계 기록 (dbio). */ +/* RC_LEDGERLINK - 대사 원장 연계 기록 (dbio). */ void RC_LEDGERLINK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long pid = getl(b, T_PURCHASE_ID), net = getl(b, T_NET); + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; + EXEC SQL END DECLARE SECTION; + + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "LEDGERLINK_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_LEDGERLINK"); + snprintf(h_msg, sizeof(h_msg), "[RC_LEDGERLINK] 대사 원장 연계 기록 (dbio)."); + strcpy(h_key, "-"); + userlog("[RC_LEDGERLINK] 대사 진입: 대사 원장 연계 기록 (dbio)."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (rcdb_insert_ledger(pid, "RC_LINK", net, bizdate) < 0) FAIL(b); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_LEDGERLINK] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_LEDGERLINK] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + h_net = getl(b, T_NET); + if (h_net < -100000000000L || h_net > 100000000000L) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "순액 범위 초과", 0L); + userlog("[RC_LEDGERLINK] 순액 범위 초과 net=%ld", h_net); FAIL(b); + } + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8004); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_LEDGERLINK] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_LEDGERLINK] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_LEDGERLINK] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_LEDGERLINK] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_LEDGERLINK] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_LEDGERLINK] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_LEDGERLINK] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_ledgerlink CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_ledgerlink; + for (;;) { + EXEC SQL FETCH cur_rc_ledgerlink INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_ledgerlink; userlog("[RC_LEDGERLINK] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_ledgerlink; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_ledgerlink; + userlog("[RC_LEDGERLINK] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_LEDGERLINK] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_LEDGERLINK] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_LEDGERLINK] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_LEDGERLINK] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_LEDGERLINK scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_LEDGERLINK] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_LEDGERLINK] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_LEDGERLINK] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_LEDGERLINK] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_LIMITCHK.pgc b/app/src/rc/svc/RC_LIMITCHK.pgc index b569559..b0ec65c 100644 --- a/app/src/rc/svc/RC_LIMITCHK.pgc +++ b/app/src/rc/svc/RC_LIMITCHK.pgc @@ -1,44 +1,145 @@ /* - * RC_LIMITCHK.pgc - rc 모듈 서비스 RC_LIMITCHK (one service = one file). - * 가맹점 한도 대비 대사: 가맹점 일한도(daily_limit)와 당일 매입(취소 제외) - * 사용액을 비교하여 초과 시 CRIT 경보를 발생시키고 초과 여부를 돌려준다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_LIMITCHK.pgc - rc 모듈 서비스 RC_LIMITCHK (대사 온라인 프로그램, 심화판). + * 업무: 가맹점 일한도 초과 검증. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_LIMITCHK.h" +#include "acq_common.h" -/* 71. RC_LIMITCHK - 가맹점 일한도 초과 검증. */ +/* RC_LIMITCHK - 가맹점 일한도 초과 검증. */ void RC_LIMITCHK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], merchant[64]; - long over; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_lim, h_use; - char h_bizdate[16], h_mid[64], h_msg[96]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "LIMITCHK_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_LIMITCHK"); + snprintf(h_msg, sizeof(h_msg), "[RC_LIMITCHK] 가맹점 일한도 초과 검증."); + strcpy(h_key, "-"); + userlog("[RC_LIMITCHK] 대사 진입: 가맹점 일한도 초과 검증."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - gets_(b, T_MERCHANT, merchant, sizeof(merchant)); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - strncpy(h_mid, merchant, sizeof(h_mid)-1); h_mid[sizeof(h_mid)-1] = 0; - - EXEC SQL SELECT daily_limit INTO :h_lim FROM merchant WHERE merchant_id = :h_mid; - if (sqlca.sqlcode != 0) FAIL(b); - EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_use FROM purchase - WHERE merchant_id = :h_mid AND biz_date = :h_bizdate AND status <> 'C'; - if (sqlca.sqlcode < 0) FAIL(b); - - over = (h_use > h_lim); - if (over) { - snprintf(h_msg, sizeof(h_msg), "daily limit exceeded %.20s use=%ld lim=%ld", - merchant, h_use, h_lim); - EXEC SQL INSERT INTO rc_alert (alert_id, purchase_id, lvl, message, biz_date) - VALUES (nextval('rc_alert_seq'), 0, 'CRIT', :h_msg, :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_LIMITCHK] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); } - setl(b, T_AMT1, h_lim); - setl(b, T_AMT2, h_use); - setl(b, T_RC, over ? 1 : 0); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + gets_(b, T_MERCHANT, tmpin, sizeof(tmpin)); + if (tmpin[0] == 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "가맹점 ID 필수", 0L); + userlog("[RC_LIMITCHK] 가맹점 ID 미입력"); FAIL(b); + } + strncpy(h_merch, tmpin, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strcpy(h_key, h_merch); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_LIMITCHK] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_LIMITCHK] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_LIMITCHK] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_LIMITCHK] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_LIMITCHK] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_limitchk CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_limitchk; + for (;;) { + EXEC SQL FETCH cur_rc_limitchk INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_limitchk; userlog("[RC_LIMITCHK] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_limitchk; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_limitchk; + userlog("[RC_LIMITCHK] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_LIMITCHK] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_LIMITCHK] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_LIMITCHK] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_LIMITCHK] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_LIMITCHK scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_LIMITCHK] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_LIMITCHK] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_LIMITCHK] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_AMT1, h_gross); + setl(b, T_AMT2, h_net); + setl(b, T_RC, rc_code); + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_LIMITCHK] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_MANAPPRV.pgc b/app/src/rc/svc/RC_MANAPPRV.pgc index 2a7be90..5693e01 100644 --- a/app/src/rc/svc/RC_MANAPPRV.pgc +++ b/app/src/rc/svc/RC_MANAPPRV.pgc @@ -1,40 +1,137 @@ /* - * RC_MANAPPRV.pgc - rc 모듈 서비스 RC_MANAPPRV (one service = one file). - * 수작업 대사 승인: HOLD 상태의 MANREG 수작업 대사 1건을 MATCHED 로 확정하고 - * 승인 증적을 원장(ledger) 에 남긴다. 대상이 없으면 실패를 돌려준다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_MANAPPRV.pgc - rc 모듈 서비스 RC_MANAPPRV (대사 온라인 프로그램, 심화판). + * 업무: 수작업 대사 승인 (HOLD -> MATCHED). + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_MANAPPRV.h" +#include "acq_common.h" -/* 38. RC_MANAPPRV - 수작업 대사 승인 (HOLD -> MATCHED). */ +/* RC_MANAPPRV - 수작업 대사 승인 (HOLD -> MATCHED). */ void RC_MANAPPRV(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long rows; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_mid, h_pid, h_amt; - char h_bizdate[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; - h_mid = getl(b, T_ID1); + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "MANAPPRV_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_MANAPPRV"); + snprintf(h_msg, sizeof(h_msg), "[RC_MANAPPRV] 수작업 대사 승인 (HOLD -> MATCHED)."); + strcpy(h_key, "-"); + userlog("[RC_MANAPPRV] 대사 진입: 수작업 대사 승인 (HOLD -> MATCHED)."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_MANAPPRV] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + h_id = getl(b, T_ID1); - EXEC SQL UPDATE rc_match_log SET status = 'MATCHED', updated_at = now() - WHERE match_id = :h_mid AND match_type = 'MANREG' AND status = 'HOLD'; - if (sqlca.sqlcode < 0) FAIL(b); - rows = sqlca.sqlerrd[2]; - if (rows != 1) FAIL(b); + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_MANAPPRV] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_MANAPPRV] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_MANAPPRV] 예외 집계 오류"); FAIL(b); } - EXEC SQL SELECT purchase_id, amount INTO :h_pid, :h_amt - FROM rc_match_log WHERE match_id = :h_mid; - if (sqlca.sqlcode != 0) FAIL(b); - EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) - VALUES (nextval('ledger_seq'), :h_pid, 'RC_MANOK', :h_amt, :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, rows); + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_manapprv CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_manapprv; + for (;;) { + EXEC SQL FETCH cur_rc_manapprv INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_manapprv; userlog("[RC_MANAPPRV] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_manapprv; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_manapprv; + userlog("[RC_MANAPPRV] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_MANAPPRV] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_MANAPPRV] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_MANAPPRV] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 상태 전이: HOLD -> MATCHED */ + if (rcdb_update_match_status(h_pid, "HOLD", "MATCHED") < 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "상태 전이 실패", 0L); + userlog("[RC_MANAPPRV] 상태 전이 실패 pid=%ld HOLD->MATCHED", h_pid); FAIL(b); + } + strcpy(h_status, "M"); out_cnt = 1; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_MANAPPRV] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_MANAPPRV scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_MANAPPRV] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_MANAPPRV] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_MANAPPRV] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); setl(b, T_PURCHASE_ID, h_pid); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_MANAPPRV] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_MANCANCEL.pgc b/app/src/rc/svc/RC_MANCANCEL.pgc index 64ad1b3..159a728 100644 --- a/app/src/rc/svc/RC_MANCANCEL.pgc +++ b/app/src/rc/svc/RC_MANCANCEL.pgc @@ -1,29 +1,156 @@ /* - * RC_MANCANCEL.pgc - rc 모듈 서비스 RC_MANCANCEL (one service = one file). - * 수작업 신청 취소: 아직 확정되지 않은 수작업 HOLD 신청 로그를 삭제. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_MANCANCEL.pgc - rc 모듈 서비스 RC_MANCANCEL (대사 온라인 프로그램, 심화판). + * 업무: 수작업 신청 취소: 아직 확정되지 않은 수작업 HOLD 신청 로그를 삭제. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_MANCANCEL.h" +#include "acq_common.h" -/* 104. RC_MANCANCEL - 수작업 신청 취소: 아직 확정되지 않은 수작업 HOLD 신청 로그를 삭제. */ +/* RC_MANCANCEL - 수작업 신청 취소: 아직 확정되지 않은 수작업 HOLD 신청 로그를 삭제. */ void RC_MANCANCEL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_pid, h_del; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "MANCANCEL_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_MANCANCEL"); + snprintf(h_msg, sizeof(h_msg), "[RC_MANCANCEL] 수작업 신청 취소: 아직 확정되지 않은 수작업 HOLD 신청 로그를 삭제."); + strcpy(h_key, "-"); + userlog("[RC_MANCANCEL] 대사 진입: 수작업 신청 취소: 아직 확정되지 않은 수작업 HOLD 신청 로그를 삭제."); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_MANCANCEL] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); h_pid = getl(b, T_PURCHASE_ID); - EXEC SQL DELETE FROM rc_match_log - WHERE purchase_id = :h_pid AND match_type = 'MANUAL' AND status = 'HOLD'; - if (sqlca.sqlcode < 0) FAIL(b); - h_del = sqlca.sqlerrd[2]; - setl(b, T_COUNT, h_del); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_MANCANCEL] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_MANCANCEL] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_MANCANCEL] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_MANCANCEL] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_MANCANCEL] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_MANCANCEL] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_MANCANCEL] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_MANCANCEL] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_mancancel CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_mancancel; + for (;;) { + EXEC SQL FETCH cur_rc_mancancel INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_mancancel; userlog("[RC_MANCANCEL] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_mancancel; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_mancancel; + userlog("[RC_MANCANCEL] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_MANCANCEL] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_MANCANCEL] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_MANCANCEL] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 상태 전이: HOLD -> ROLLBACK */ + if (rcdb_update_match_status(h_pid, "HOLD", "ROLLBACK") < 0) { + setl(b, T_RC, 8004); Bchg(b, T_MSG, 0, "상태 전이 실패", 0L); + userlog("[RC_MANCANCEL] 상태 전이 실패 pid=%ld HOLD->ROLLBACK", h_pid); FAIL(b); + } + strcpy(h_status, "X"); out_cnt = 1; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_MANCANCEL] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_MANCANCEL scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_MANCANCEL] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_MANCANCEL] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_MANCANCEL] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_MANCANCEL] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_MANLIST.pgc b/app/src/rc/svc/RC_MANLIST.pgc index c1df853..9d583f3 100644 --- a/app/src/rc/svc/RC_MANLIST.pgc +++ b/app/src/rc/svc/RC_MANLIST.pgc @@ -1,31 +1,132 @@ /* - * RC_MANLIST.pgc - rc 모듈 서비스 RC_MANLIST (one service = one file). - * 수작업 신청 현황: 당일 수작업 대사 로그를 상태별(HOLD/확정)로 나누어 건수 집계. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_MANLIST.pgc - rc 모듈 서비스 RC_MANLIST (대사 온라인 프로그램, 심화판). + * 업무: 수작업 신청 현황: 당일 수작업 대사 로그를 상태별(HOLD/확정)로 나누어 건수 집계. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_MANLIST.h" +#include "acq_common.h" -/* 105. RC_MANLIST - 수작업 신청 현황: 당일 수작업 대사 로그를 상태별(HOLD/확정)로 나누어 건수 집계. */ +/* RC_MANLIST - 수작업 신청 현황: 당일 수작업 대사 로그를 상태별(HOLD/확정)로 나누어 건수 집계. */ void RC_MANLIST(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_hold, h_conf; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "MANLIST_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_MANLIST"); + snprintf(h_msg, sizeof(h_msg), "[RC_MANLIST] 수작업 신청 현황: 당일 수작업 대사 로그를 상태별(HOLD/확정)로 나누어 건수 집계."); + strcpy(h_key, "-"); + userlog("[RC_MANLIST] 대사 진입: 수작업 신청 현황: 당일 수작업 대사 로그를 상태별(HOLD/확정)로 나누어 건수 집계."); - EXEC SQL SELECT count(*) INTO :h_hold FROM rc_match_log - WHERE biz_date = :h_bizdate AND match_type = 'MANUAL' AND status = 'HOLD'; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_conf FROM rc_match_log - WHERE biz_date = :h_bizdate AND match_type = 'MANUAL' AND status = 'CONFIRMED'; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_ARG1, h_hold); - setl(b, T_ARG2, h_conf); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_MANLIST] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_MANLIST] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_MANLIST] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_MANLIST] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_manlist CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_manlist; + for (;;) { + EXEC SQL FETCH cur_rc_manlist INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_manlist; userlog("[RC_MANLIST] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_manlist; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_manlist; + userlog("[RC_MANLIST] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_MANLIST] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_MANLIST] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_MANLIST] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_MANLIST] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_MANLIST scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_MANLIST] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_MANLIST] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_MANLIST] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_ARG1, matched_rows); + setl(b, T_ARG2, unmatched_rows); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_MANLIST] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_MANNG.pgc b/app/src/rc/svc/RC_MANNG.pgc index b6b81ef..eaefecb 100644 --- a/app/src/rc/svc/RC_MANNG.pgc +++ b/app/src/rc/svc/RC_MANNG.pgc @@ -1,29 +1,156 @@ /* - * RC_MANNG.pgc - rc 모듈 서비스 RC_MANNG (one service = one file). - * 수작업 대사 반려: 해당 매입의 수작업 HOLD 로그를 ROLLBACK 으로 반려 처리. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_MANNG.pgc - rc 모듈 서비스 RC_MANNG (대사 온라인 프로그램, 심화판). + * 업무: 수작업 대사 반려: 해당 매입의 수작업 HOLD 로그를 ROLLBACK 으로 반려 처리. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_MANNG.h" +#include "acq_common.h" -/* 103. RC_MANNG - 수작업 대사 반려: 해당 매입의 수작업 HOLD 로그를 ROLLBACK 으로 반려 처리. */ +/* RC_MANNG - 수작업 대사 반려: 해당 매입의 수작업 HOLD 로그를 ROLLBACK 으로 반려 처리. */ void RC_MANNG(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_pid, h_upd; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "MANNG_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_MANNG"); + snprintf(h_msg, sizeof(h_msg), "[RC_MANNG] 수작업 대사 반려: 해당 매입의 수작업 HOLD 로그를 ROLLBACK 으로 반려 처리."); + strcpy(h_key, "-"); + userlog("[RC_MANNG] 대사 진입: 수작업 대사 반려: 해당 매입의 수작업 HOLD 로그를 ROLLBACK 으로 반려 처리."); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_MANNG] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); h_pid = getl(b, T_PURCHASE_ID); - EXEC SQL UPDATE rc_match_log SET status = 'ROLLBACK', updated_at = now() - WHERE purchase_id = :h_pid AND match_type = 'MANUAL' AND status = 'HOLD'; - if (sqlca.sqlcode < 0) FAIL(b); - h_upd = sqlca.sqlerrd[2]; - setl(b, T_COUNT, h_upd); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_MANNG] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_MANNG] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_MANNG] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_MANNG] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_MANNG] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_MANNG] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_MANNG] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_MANNG] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_manng CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_manng; + for (;;) { + EXEC SQL FETCH cur_rc_manng INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_manng; userlog("[RC_MANNG] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_manng; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_manng; + userlog("[RC_MANNG] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_MANNG] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_MANNG] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_MANNG] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 상태 전이: HOLD -> ROLLBACK */ + if (rcdb_update_match_status(h_pid, "HOLD", "ROLLBACK") < 0) { + setl(b, T_RC, 8004); Bchg(b, T_MSG, 0, "상태 전이 실패", 0L); + userlog("[RC_MANNG] 상태 전이 실패 pid=%ld HOLD->ROLLBACK", h_pid); FAIL(b); + } + strcpy(h_status, "X"); out_cnt = 1; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_MANNG] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_MANNG scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_MANNG] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_MANNG] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_MANNG] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_MANNG] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_MANOK.pgc b/app/src/rc/svc/RC_MANOK.pgc index 1a20271..4064e80 100644 --- a/app/src/rc/svc/RC_MANOK.pgc +++ b/app/src/rc/svc/RC_MANOK.pgc @@ -1,29 +1,156 @@ /* - * RC_MANOK.pgc - rc 모듈 서비스 RC_MANOK (one service = one file). - * 수작업 대사 확정: 해당 매입의 수작업 HOLD 로그를 CONFIRMED 로 승인 처리. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_MANOK.pgc - rc 모듈 서비스 RC_MANOK (대사 온라인 프로그램, 심화판). + * 업무: 수작업 대사 확정: 해당 매입의 수작업 HOLD 로그를 CONFIRMED 로 승인 처리. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_MANOK.h" +#include "acq_common.h" -/* 102. RC_MANOK - 수작업 대사 확정: 해당 매입의 수작업 HOLD 로그를 CONFIRMED 로 승인 처리. */ +/* RC_MANOK - 수작업 대사 확정: 해당 매입의 수작업 HOLD 로그를 CONFIRMED 로 승인 처리. */ void RC_MANOK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_pid, h_upd; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "MANOK_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_MANOK"); + snprintf(h_msg, sizeof(h_msg), "[RC_MANOK] 수작업 대사 확정: 해당 매입의 수작업 HOLD 로그를 CONFIRMED 로 승인 처리."); + strcpy(h_key, "-"); + userlog("[RC_MANOK] 대사 진입: 수작업 대사 확정: 해당 매입의 수작업 HOLD 로그를 CONFIRMED 로 승인 처리."); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_MANOK] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); h_pid = getl(b, T_PURCHASE_ID); - EXEC SQL UPDATE rc_match_log SET status = 'CONFIRMED', updated_at = now() - WHERE purchase_id = :h_pid AND match_type = 'MANUAL' AND status = 'HOLD'; - if (sqlca.sqlcode < 0) FAIL(b); - h_upd = sqlca.sqlerrd[2]; - setl(b, T_COUNT, h_upd); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_MANOK] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_MANOK] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_MANOK] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_MANOK] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_MANOK] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_MANOK] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_MANOK] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_MANOK] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_manok CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_manok; + for (;;) { + EXEC SQL FETCH cur_rc_manok INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_manok; userlog("[RC_MANOK] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_manok; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_manok; + userlog("[RC_MANOK] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_MANOK] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_MANOK] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_MANOK] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 상태 전이: HOLD -> CONFIRMED */ + if (rcdb_update_match_status(h_pid, "HOLD", "CONFIRMED") < 0) { + setl(b, T_RC, 8004); Bchg(b, T_MSG, 0, "상태 전이 실패", 0L); + userlog("[RC_MANOK] 상태 전이 실패 pid=%ld HOLD->CONFIRMED", h_pid); FAIL(b); + } + strcpy(h_status, "C"); out_cnt = 1; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_MANOK] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_MANOK scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_MANOK] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_MANOK] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_MANOK] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_MANOK] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_MANREG.pgc b/app/src/rc/svc/RC_MANREG.pgc index a6a5986..6607097 100644 --- a/app/src/rc/svc/RC_MANREG.pgc +++ b/app/src/rc/svc/RC_MANREG.pgc @@ -1,35 +1,161 @@ /* - * RC_MANREG.pgc - rc 모듈 서비스 RC_MANREG (one service = one file). - * 수작업 대사 등록: 담당자가 입력한 수작업 대사를 HOLD 상태(승인 대기)로 - * 등록하고, 등록 사실을 INFO 경보로 남긴다. 승인 전까지 확정되지 않는다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_MANREG.pgc - rc 모듈 서비스 RC_MANREG (대사 온라인 프로그램, 심화판). + * 업무: 수작업 대사 등록 (승인 대기 HOLD). + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_MANREG.h" +#include "acq_common.h" -/* 37. RC_MANREG - 수작업 대사 등록 (승인 대기 HOLD). */ +/* RC_MANREG - 수작업 대사 등록 (승인 대기 HOLD). */ void RC_MANREG(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], reason[64]; - long mid; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid; - char h_bizdate[16], h_msg[96]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "MANREG_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_MANREG"); + snprintf(h_msg, sizeof(h_msg), "[RC_MANREG] 수작업 대사 등록 (승인 대기 HOLD)."); + strcpy(h_key, "-"); + userlog("[RC_MANREG] 대사 진입: 수작업 대사 등록 (승인 대기 HOLD)."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - gets_(b, T_REASON, reason, sizeof(reason)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_MANREG] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_MANREG] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + h_net = getl(b, T_NET); + if (h_net < -100000000000L || h_net > 100000000000L) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "순액 범위 초과", 0L); + userlog("[RC_MANREG] 순액 범위 초과 net=%ld", h_net); FAIL(b); + } + gets_(b, T_REASON, tmpin, sizeof(tmpin)); if (tmpin[0]) { strncpy(h_reason, tmpin, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1]=0; } - mid = rcdb_insert_match(h_pid, "MANREG", getl(b, T_NET), "HOLD", bizdate); - if (mid < 0) FAIL(b); + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8004); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_MANREG] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_MANREG] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_MANREG] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_MANREG] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_MANREG] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_MANREG] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_MANREG] 예외 집계 오류"); FAIL(b); } - snprintf(h_msg, sizeof(h_msg), "manual recon registered: %.60s", reason); - EXEC SQL INSERT INTO rc_alert (alert_id, purchase_id, lvl, message, biz_date) - VALUES (nextval('rc_alert_seq'), :h_pid, 'INFO', :h_msg, :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_ID1, mid); + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_manreg CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_manreg; + for (;;) { + EXEC SQL FETCH cur_rc_manreg INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_manreg; userlog("[RC_MANREG] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_manreg; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_manreg; + userlog("[RC_MANREG] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_MANREG] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_MANREG] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_MANREG] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 대사 로그 적재 (rc_match_log) */ + h_id = rcdb_insert_match(h_pid, "MANREG", h_net, + (unmatched_rows == 0 ? "MATCHED" : "EXCEPTION"), bizdate); + if (h_id < 0) { userlog("[RC_MANREG] 대사 로그 적재 실패 pid=%ld", h_pid); FAIL(b); } + out_cnt = matched_rows > 0 ? matched_rows : 1; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_MANREG] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_MANREG scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_MANREG] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_MANREG] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_MANREG] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_ID1, h_id); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_MANREG] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_MANREJECT.pgc b/app/src/rc/svc/RC_MANREJECT.pgc index 12764d3..4cc4645 100644 --- a/app/src/rc/svc/RC_MANREJECT.pgc +++ b/app/src/rc/svc/RC_MANREJECT.pgc @@ -1,36 +1,138 @@ /* - * RC_MANREJECT.pgc - rc 모듈 서비스 RC_MANREJECT (one service = one file). - * 수작업 대사 반려: 승인 대기(HOLD) 중인 수작업 대사를 ROLLBACK 처리하고, - * 반려 사유를 대사 예외(rc_exception) 로 적재하여 재처리 대상으로 만든다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_MANREJECT.pgc - rc 모듈 서비스 RC_MANREJECT (대사 온라인 프로그램, 심화판). + * 업무: 수작업 대사 반려 (HOLD -> ROLLBACK + 예외). + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_MANREJECT.h" +#include "acq_common.h" -/* 39. RC_MANREJECT - 수작업 대사 반려 (HOLD -> ROLLBACK + 예외). */ +/* RC_MANREJECT - 수작업 대사 반려 (HOLD -> ROLLBACK + 예외). */ void RC_MANREJECT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], reason[64]; - long eid; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_mid, h_pid; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; - h_mid = getl(b, T_ID1); + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "MANREJECT_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_MANREJECT"); + snprintf(h_msg, sizeof(h_msg), "[RC_MANREJECT] 수작업 대사 반려 (HOLD -> ROLLBACK + 예외)."); + strcpy(h_key, "-"); + userlog("[RC_MANREJECT] 대사 진입: 수작업 대사 반려 (HOLD -> ROLLBACK + 예외)."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - gets_(b, T_REASON, reason, sizeof(reason)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_MANREJECT] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + gets_(b, T_REASON, tmpin, sizeof(tmpin)); if (tmpin[0]) { strncpy(h_reason, tmpin, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1]=0; } + h_id = getl(b, T_ID1); - EXEC SQL UPDATE rc_match_log SET status = 'ROLLBACK', updated_at = now() - WHERE match_id = :h_mid AND status = 'HOLD'; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlerrd[2] == 0) FAIL(b); + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_MANREJECT] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_MANREJECT] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_MANREJECT] 예외 집계 오류"); FAIL(b); } - EXEC SQL SELECT purchase_id INTO :h_pid FROM rc_match_log WHERE match_id = :h_mid; - if (sqlca.sqlcode != 0) FAIL(b); - eid = rcdb_insert_exception(h_pid, reason[0] ? reason : "MANUAL REJECT", bizdate); - if (eid < 0) FAIL(b); - setl(b, T_ID2, eid); + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_manreject CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_manreject; + for (;;) { + EXEC SQL FETCH cur_rc_manreject INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_manreject; userlog("[RC_MANREJECT] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_manreject; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_manreject; + userlog("[RC_MANREJECT] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_MANREJECT] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_MANREJECT] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_MANREJECT] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 상태 전이: HOLD -> ROLLBACK */ + if (rcdb_update_match_status(h_pid, "HOLD", "ROLLBACK") < 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "상태 전이 실패", 0L); + userlog("[RC_MANREJECT] 상태 전이 실패 pid=%ld HOLD->ROLLBACK", h_pid); FAIL(b); + } + strcpy(h_status, "X"); out_cnt = 1; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_MANREJECT] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_MANREJECT scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_MANREJECT] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_MANREJECT] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_MANREJECT] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_ID2, h_id2); setl(b, T_PURCHASE_ID, h_pid); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_MANREJECT] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_MANREQ.pgc b/app/src/rc/svc/RC_MANREQ.pgc index f163022..d27bd70 100644 --- a/app/src/rc/svc/RC_MANREQ.pgc +++ b/app/src/rc/svc/RC_MANREQ.pgc @@ -1,31 +1,155 @@ /* - * RC_MANREQ.pgc - rc 모듈 서비스 RC_MANREQ (one service = one file). - * 수작업 대사 신청: 지정 매입에 대해 수작업 대사 요청을 HOLD 상태 로그로 등록. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_MANREQ.pgc - rc 모듈 서비스 RC_MANREQ (대사 온라인 프로그램, 심화판). + * 업무: 수작업 대사 신청: 지정 매입에 대해 수작업 대사 요청을 HOLD 상태 로그로 등록. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_MANREQ.h" +#include "acq_common.h" -/* 101. RC_MANREQ - 수작업 대사 신청: 지정 매입에 대해 수작업 대사 요청을 HOLD 상태 로그로 등록. */ +/* RC_MANREQ - 수작업 대사 신청: 지정 매입에 대해 수작업 대사 요청을 HOLD 상태 로그로 등록. */ void RC_MANREQ(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_id, h_pid, h_amt; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "MANREQ_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_MANREQ"); + snprintf(h_msg, sizeof(h_msg), "[RC_MANREQ] 수작업 대사 신청: 지정 매입에 대해 수작업 대사 요청을 HOLD 상태 로그로 등록."); + strcpy(h_key, "-"); + userlog("[RC_MANREQ] 대사 진입: 수작업 대사 신청: 지정 매입에 대해 수작업 대사 요청을 HOLD 상태 로그로 등록."); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_MANREQ] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); h_pid = getl(b, T_PURCHASE_ID); - EXEC SQL SELECT coalesce(net,0) INTO :h_amt FROM purchase WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0 && sqlca.sqlcode != 100) FAIL(b); - EXEC SQL SELECT nextval('rc_match_seq') INTO :h_id; - EXEC SQL INSERT INTO rc_match_log (match_id, purchase_id, match_type, amount, status, biz_date) - VALUES (:h_id, :h_pid, 'MANUAL', :h_amt, 'HOLD', :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_MANREQ] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_MANREQ] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_MANREQ] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_MANREQ] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_MANREQ] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_MANREQ] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_MANREQ] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_MANREQ] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_manreq CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_manreq; + for (;;) { + EXEC SQL FETCH cur_rc_manreq INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_manreq; userlog("[RC_MANREQ] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_manreq; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_manreq; + userlog("[RC_MANREQ] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_MANREQ] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_MANREQ] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_MANREQ] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 대사 로그 적재 (rc_match_log) */ + h_id = rcdb_insert_match(h_pid, "MANREQ", h_net, + (unmatched_rows == 0 ? "MATCHED" : "EXCEPTION"), bizdate); + if (h_id < 0) { userlog("[RC_MANREQ] 대사 로그 적재 실패 pid=%ld", h_pid); FAIL(b); } + out_cnt = matched_rows > 0 ? matched_rows : 1; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_MANREQ] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_MANREQ scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_MANREQ] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_MANREQ] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_MANREQ] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ setl(b, T_ID1, h_id); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_MANREQ] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_MANUAL.pgc b/app/src/rc/svc/RC_MANUAL.pgc index 8ac3ca1..0159ee6 100644 --- a/app/src/rc/svc/RC_MANUAL.pgc +++ b/app/src/rc/svc/RC_MANUAL.pgc @@ -1,20 +1,160 @@ /* - * RC_MANUAL.pgc - rc 모듈 서비스 RC_MANUAL (one service = one file). - * Moved verbatim from the former inline rc_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by rc_svr at tpsvrinit. + * RC_MANUAL.pgc - rc 모듈 서비스 RC_MANUAL (대사 온라인 프로그램, 심화판). + * 업무: 수동 대사 로그. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_MANUAL.h" +#include "acq_common.h" -/* 27. RC_MANUAL - 수동 대사 로그. */ +/* RC_MANUAL - 수동 대사 로그. */ void RC_MANUAL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long pid = getl(b, T_PURCHASE_ID), net = getl(b, T_NET), mid; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; + EXEC SQL END DECLARE SECTION; + + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "MANUAL_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_MANUAL"); + snprintf(h_msg, sizeof(h_msg), "[RC_MANUAL] 수동 대사 로그."); + strcpy(h_key, "-"); + userlog("[RC_MANUAL] 대사 진입: 수동 대사 로그."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - mid = rcdb_insert_match(pid, "MANUAL", net, "MATCHED", bizdate); - if (mid < 0) FAIL(b); - setl(b, T_ID1, mid); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_MANUAL] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_MANUAL] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + h_net = getl(b, T_NET); + if (h_net < -100000000000L || h_net > 100000000000L) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "순액 범위 초과", 0L); + userlog("[RC_MANUAL] 순액 범위 초과 net=%ld", h_net); FAIL(b); + } + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8004); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_MANUAL] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_MANUAL] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_MANUAL] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_MANUAL] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_MANUAL] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_MANUAL] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_MANUAL] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_manual CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_manual; + for (;;) { + EXEC SQL FETCH cur_rc_manual INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_manual; userlog("[RC_MANUAL] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_manual; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_manual; + userlog("[RC_MANUAL] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_MANUAL] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_MANUAL] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_MANUAL] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 대사 로그 적재 (rc_match_log) */ + h_id = rcdb_insert_match(h_pid, "MANUAL", h_net, + (unmatched_rows == 0 ? "MATCHED" : "EXCEPTION"), bizdate); + if (h_id < 0) { userlog("[RC_MANUAL] 대사 로그 적재 실패 pid=%ld", h_pid); FAIL(b); } + out_cnt = matched_rows > 0 ? matched_rows : 1; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_MANUAL] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_MANUAL scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_MANUAL] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_MANUAL] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_MANUAL] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_ID1, h_id); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_MANUAL] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_MGRPDIFF.pgc b/app/src/rc/svc/RC_MGRPDIFF.pgc index 6e3c56b..72d39ee 100644 --- a/app/src/rc/svc/RC_MGRPDIFF.pgc +++ b/app/src/rc/svc/RC_MGRPDIFF.pgc @@ -1,32 +1,143 @@ /* - * RC_MGRPDIFF.pgc - rc 모듈 서비스 RC_MGRPDIFF (one service = one file). - * 가맹점그룹별 차이 분석: 가맹점 접두 그룹의 매입 순액과 정산 순액 차액을 집계. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_MGRPDIFF.pgc - rc 모듈 서비스 RC_MGRPDIFF (대사 온라인 프로그램, 심화판). + * 업무: 가맹점그룹별 차이 분석: 가맹점 접두 그룹의 매입 순액과 정산 순액 차액을 집계. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_MGRPDIFF.h" +#include "acq_common.h" -/* 93. RC_MGRPDIFF - 가맹점그룹별 차이 분석: 가맹점 접두 그룹의 매입 순액과 정산 순액 차액을 집계. */ +/* RC_MGRPDIFF - 가맹점그룹별 차이 분석: 가맹점 접두 그룹의 매입 순액과 정산 순액 차액을 집계. */ void RC_MGRPDIFF(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_exp, h_act; char h_grp[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "MGRPDIFF_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_MGRPDIFF"); + snprintf(h_msg, sizeof(h_msg), "[RC_MGRPDIFF] 가맹점그룹별 차이 분석: 가맹점 접두 그룹의 매입 순액과 정산 순액 차액을 집계."); + strcpy(h_key, "-"); + userlog("[RC_MGRPDIFF] 대사 진입: 가맹점그룹별 차이 분석: 가맹점 접두 그룹의 매입 순액과 정산 순액 차액을 집계."); - { char c[16]; gets_(b, T_KEY1, c, sizeof(c)); - strncpy(h_grp, c, sizeof(h_grp)-1); h_grp[sizeof(h_grp)-1] = 0; } - EXEC SQL SELECT coalesce(sum(net),0) INTO :h_exp FROM purchase - WHERE biz_date = :h_bizdate AND merchant_id LIKE :h_grp || '%'; + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_MGRPDIFF] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + gets_(b, T_KEY1, tmpin, sizeof(tmpin)); + if (tmpin[0] == 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "그룹 키 필수", 0L); + userlog("[RC_MGRPDIFF] 그룹 키 미입력"); FAIL(b); + } + strncpy(h_key, tmpin, sizeof(h_key)-1); h_key[sizeof(h_key)-1] = 0; + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_MGRPDIFF] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_MGRPDIFF] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_MGRPDIFF] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_mgrpdiff CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_mgrpdiff; + for (;;) { + EXEC SQL FETCH cur_rc_mgrpdiff INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_mgrpdiff; userlog("[RC_MGRPDIFF] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_mgrpdiff; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_mgrpdiff; + userlog("[RC_MGRPDIFF] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_MGRPDIFF] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_MGRPDIFF] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_MGRPDIFF] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 차이 라인 적재 (rc_diff) */ + h_exp = h_net; h_act = h_led; h_diff = h_exp - h_act; + EXEC SQL SELECT nextval('rc_diff_seq') INTO :h_id; if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT coalesce(sum(net),0) INTO :h_act FROM settlement - WHERE biz_date = :h_bizdate AND merchant_id LIKE :h_grp || '%'; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_DELTA, h_exp - h_act); + EXEC SQL INSERT INTO rc_diff (diff_id, purchase_id, expected_amt, actual_amt, diff_amt, diff_kind, biz_date) + VALUES (:h_id, :h_pid, :h_exp, :h_act, :h_diff, :h_dtype, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_MGRPDIFF] 차이 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + out_cnt = (h_diff != 0 ? 1 : 0); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_MGRPDIFF] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_MGRPDIFF scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_MGRPDIFF] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_MGRPDIFF] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_MGRPDIFF] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_DELTA, h_diff); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_MGRPDIFF] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_MGRPRECON.pgc b/app/src/rc/svc/RC_MGRPRECON.pgc index 54fc002..5270f71 100644 --- a/app/src/rc/svc/RC_MGRPRECON.pgc +++ b/app/src/rc/svc/RC_MGRPRECON.pgc @@ -1,29 +1,137 @@ /* - * RC_MGRPRECON.pgc - rc 모듈 서비스 RC_MGRPRECON (one service = one file). - * 가맹점그룹별 대사: 가맹점 ID 접두 그룹(prefix) 단위로 매입 순액을 묶어 대사. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_MGRPRECON.pgc - rc 모듈 서비스 RC_MGRPRECON (대사 온라인 프로그램, 심화판). + * 업무: 가맹점그룹별 대사: 가맹점 ID 접두 그룹(prefix) 단위로 매입 순액을 묶어 대사. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_MGRPRECON.h" +#include "acq_common.h" -/* 87. RC_MGRPRECON - 가맹점그룹별 대사: 가맹점 ID 접두 그룹(prefix) 단위로 매입 순액을 묶어 대사. */ +/* RC_MGRPRECON - 가맹점그룹별 대사: 가맹점 ID 접두 그룹(prefix) 단위로 매입 순액을 묶어 대사. */ void RC_MGRPRECON(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_net; char h_grp[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "MGRPRECON_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_MGRPRECON"); + snprintf(h_msg, sizeof(h_msg), "[RC_MGRPRECON] 가맹점그룹별 대사: 가맹점 ID 접두 그룹(prefix) 단위로 매입 순액을 묶어 대사."); + strcpy(h_key, "-"); + userlog("[RC_MGRPRECON] 대사 진입: 가맹점그룹별 대사: 가맹점 ID 접두 그룹(prefix) 단위로 매입 순액을 묶어 대사."); - { char c[16]; gets_(b, T_KEY1, c, sizeof(c)); - strncpy(h_grp, c, sizeof(h_grp)-1); h_grp[sizeof(h_grp)-1] = 0; } - EXEC SQL SELECT coalesce(sum(net),0) INTO :h_net FROM purchase - WHERE biz_date = :h_bizdate AND merchant_id LIKE :h_grp || '%'; - if (sqlca.sqlcode < 0) FAIL(b); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_MGRPRECON] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + gets_(b, T_KEY1, tmpin, sizeof(tmpin)); + if (tmpin[0] == 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "그룹 키 필수", 0L); + userlog("[RC_MGRPRECON] 그룹 키 미입력"); FAIL(b); + } + strncpy(h_key, tmpin, sizeof(h_key)-1); h_key[sizeof(h_key)-1] = 0; + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_MGRPRECON] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_MGRPRECON] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_MGRPRECON] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_mgrprecon CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_mgrprecon; + for (;;) { + EXEC SQL FETCH cur_rc_mgrprecon INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_mgrprecon; userlog("[RC_MGRPRECON] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_mgrprecon; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_mgrprecon; + userlog("[RC_MGRPRECON] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_MGRPRECON] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_MGRPRECON] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_MGRPRECON] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_MGRPRECON] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_MGRPRECON scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_MGRPRECON] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_MGRPRECON] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_MGRPRECON] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ setl(b, T_NET, h_net); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_MGRPRECON] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_NOTIFY.pgc b/app/src/rc/svc/RC_NOTIFY.pgc index 33066da..4733710 100644 --- a/app/src/rc/svc/RC_NOTIFY.pgc +++ b/app/src/rc/svc/RC_NOTIFY.pgc @@ -1,39 +1,142 @@ /* - * RC_NOTIFY.pgc - rc 모듈 서비스 RC_NOTIFY (one service = one file). - * 대사 결과 통보: 당일 대사 요약(rc_summary)을 읽어 통보 메시지를 구성하고, - * 미대사 잔량 유무에 따라 INFO/WARN 레벨 경보로 발송(적재)한다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_NOTIFY.pgc - rc 모듈 서비스 RC_NOTIFY (대사 온라인 프로그램, 심화판). + * 업무: 대사 결과 요약 통보 (경보 발송). + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_NOTIFY.h" +#include "acq_common.h" -/* 46. RC_NOTIFY - 대사 결과 요약 통보 (경보 발송). */ +/* RC_NOTIFY - 대사 결과 요약 통보 (경보 발송). */ void RC_NOTIFY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_mat, h_unm, h_gross; - char h_bizdate[16], h_lvl[8], h_msg[96]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "NOTIFY_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_NOTIFY"); + snprintf(h_msg, sizeof(h_msg), "[RC_NOTIFY] 대사 결과 요약 통보 (경보 발송)."); + strcpy(h_key, "-"); + userlog("[RC_NOTIFY] 대사 진입: 대사 결과 요약 통보 (경보 발송)."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_NOTIFY] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); - h_mat = h_unm = h_gross = 0; - EXEC SQL SELECT matched_cnt, unmatched_cnt, gross_amount - INTO :h_mat, :h_unm, :h_gross - FROM rc_summary WHERE biz_date = :h_bizdate; + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_NOTIFY] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_NOTIFY] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_NOTIFY] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_notify CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_notify; + for (;;) { + EXEC SQL FETCH cur_rc_notify INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_notify; userlog("[RC_NOTIFY] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_notify; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_notify; + userlog("[RC_NOTIFY] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_NOTIFY] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_NOTIFY] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_NOTIFY] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 경보 등록 (rc_alert) */ + if (unmatched_rows > 0 || h_ecnt > 0) strcpy(h_lvl, "WARN"); + if (unmatched_rows > 10 || h_ecnt > 20) strcpy(h_lvl, "CRIT"); + snprintf(sbuf, sizeof(sbuf), "대사 결과 요약 통보 (경보 발송). unmatch=%ld exc=%ld", unmatched_rows, h_ecnt); + strncpy(h_msg, sbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL SELECT nextval('rc_alert_seq') INTO :h_id; if (sqlca.sqlcode < 0) FAIL(b); - - strcpy(h_lvl, (h_unm > 0) ? "WARN" : "INFO"); - snprintf(h_msg, sizeof(h_msg), - "recon result: matched=%ld unmatched=%ld gross=%ld", h_mat, h_unm, h_gross); EXEC SQL INSERT INTO rc_alert (alert_id, purchase_id, lvl, message, biz_date) - VALUES (nextval('rc_alert_seq'), 0, :h_lvl, :h_msg, :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_ARG1, h_mat); - setl(b, T_ARG2, h_unm); + VALUES (:h_id, :h_pid, :h_lvl, :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_NOTIFY] 경보 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + out_cnt = h_ecnt; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_NOTIFY] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_NOTIFY scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_NOTIFY] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_NOTIFY] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_NOTIFY] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_ARG1, matched_rows); + setl(b, T_ARG2, unmatched_rows); setl(b, T_GROSS, h_gross); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_NOTIFY] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_ORPHAN.pgc b/app/src/rc/svc/RC_ORPHAN.pgc index fdd7673..c9f29b1 100644 --- a/app/src/rc/svc/RC_ORPHAN.pgc +++ b/app/src/rc/svc/RC_ORPHAN.pgc @@ -1,30 +1,135 @@ /* - * RC_ORPHAN.pgc - rc 모듈 서비스 RC_ORPHAN (one service = one file). - * 고아 매입 검출: 승인(approval) 이 전혀 없는 당일 매입을 NOT EXISTS 로 - * 찾아 미매입 관리 테이블(ac_unmatch)에 일괄 적재하고 건수를 돌려준다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_ORPHAN.pgc - rc 모듈 서비스 RC_ORPHAN (대사 온라인 프로그램, 심화판). + * 업무: 승인 없는 고아 매입 적재. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_ORPHAN.h" +#include "acq_common.h" -/* 68. RC_ORPHAN - 승인 없는 고아 매입 적재. */ +/* RC_ORPHAN - 승인 없는 고아 매입 적재. */ void RC_ORPHAN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "ORPHAN_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_ORPHAN"); + snprintf(h_msg, sizeof(h_msg), "[RC_ORPHAN] 승인 없는 고아 매입 적재."); + strcpy(h_key, "-"); + userlog("[RC_ORPHAN] 대사 진입: 승인 없는 고아 매입 적재."); - EXEC SQL INSERT INTO ac_unmatch (unmatch_id, purchase_id, reason, biz_date) - SELECT nextval('ac_unmatch_seq'), p.purchase_id, 'NO APPROVAL', p.biz_date - FROM purchase p + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_ORPHAN] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_ORPHAN] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_ORPHAN] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_ORPHAN] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_orphan CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_orphan; + for (;;) { + EXEC SQL FETCH cur_rc_orphan INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_orphan; userlog("[RC_ORPHAN] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_orphan; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_orphan; + userlog("[RC_ORPHAN] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_ORPHAN] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_ORPHAN] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_ORPHAN] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 예외 적재 (rc_exception) */ + if (h_ecnt >= 0) { + h_id = rcdb_insert_exception(h_pid, h_reason, bizdate); + if (h_id < 0) { userlog("[RC_ORPHAN] 예외 적재 실패 pid=%ld", h_pid); FAIL(b); } + } + out_cnt = h_ecnt + (h_pid > 0 ? 1 : 0); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_ORPHAN] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_ORPHAN scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_ORPHAN] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p WHERE p.biz_date = :h_bizdate - AND NOT EXISTS (SELECT 1 FROM approval a WHERE a.purchase_id = p.purchase_id); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, sqlca.sqlerrd[2]); + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_ORPHAN] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_ORPHAN] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_ORPHAN] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_OVERAMT.pgc b/app/src/rc/svc/RC_OVERAMT.pgc index 3e454ea..39487eb 100644 --- a/app/src/rc/svc/RC_OVERAMT.pgc +++ b/app/src/rc/svc/RC_OVERAMT.pgc @@ -1,28 +1,132 @@ /* - * RC_OVERAMT.pgc - rc 모듈 서비스 RC_OVERAMT (one service = one file). - * 과다 차이 분석: 당일 rc_diff 중 실제가 기대를 초과(diff_amt<0)한 과다 건수/합계. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_OVERAMT.pgc - rc 모듈 서비스 RC_OVERAMT (대사 온라인 프로그램, 심화판). + * 업무: 과다 차이 분석: 당일 rc_diff 중 실제가 기대를 초과(diff_amt<0)한 과다 건수/합계. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_OVERAMT.h" +#include "acq_common.h" -/* 94. RC_OVERAMT - 과다 차이 분석: 당일 rc_diff 중 실제가 기대를 초과(diff_amt<0)한 과다 건수/합계. */ +/* RC_OVERAMT - 과다 차이 분석: 당일 rc_diff 중 실제가 기대를 초과(diff_amt<0)한 과다 건수/합계. */ void RC_OVERAMT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_cnt, h_sum; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "OVERAMT_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_OVERAMT"); + snprintf(h_msg, sizeof(h_msg), "[RC_OVERAMT] 과다 차이 분석: 당일 rc_diff 중 실제가 기대를 초과(diff_amt<0)한 과다 건수/합계."); + strcpy(h_key, "-"); + userlog("[RC_OVERAMT] 대사 진입: 과다 차이 분석: 당일 rc_diff 중 실제가 기대를 초과(diff_amt<0)한 과다 건수/합계."); - EXEC SQL SELECT count(*), coalesce(sum(diff_amt),0) INTO :h_cnt, :h_sum - FROM rc_diff WHERE biz_date = :h_bizdate AND diff_amt < 0; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); - setl(b, T_DELTA, h_sum); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_OVERAMT] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_OVERAMT] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_OVERAMT] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_OVERAMT] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_overamt CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_overamt; + for (;;) { + EXEC SQL FETCH cur_rc_overamt INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_overamt; userlog("[RC_OVERAMT] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_overamt; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_overamt; + userlog("[RC_OVERAMT] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_OVERAMT] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_OVERAMT] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_OVERAMT] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_OVERAMT] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_OVERAMT scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_OVERAMT] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_OVERAMT] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_OVERAMT] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + setl(b, T_DELTA, h_diff); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_OVERAMT] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_PARTACQ.pgc b/app/src/rc/svc/RC_PARTACQ.pgc index 6d8463c..1f2d87a 100644 --- a/app/src/rc/svc/RC_PARTACQ.pgc +++ b/app/src/rc/svc/RC_PARTACQ.pgc @@ -1,49 +1,157 @@ /* - * RC_PARTACQ.pgc - rc 모듈 서비스 RC_PARTACQ (one service = one file). - * 부분매입 대사: 분할 매입(ac_install 회차) 합계가 원매입 금액과 일치하는지 - * 검증한다. 회차가 없으면 EXCEPTION, 합계 불일치면 HOLD + rc_diff 적재. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_PARTACQ.pgc - rc 모듈 서비스 RC_PARTACQ (대사 온라인 프로그램, 심화판). + * 업무: 부분매입(회차 합계) 대사. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_PARTACQ.h" +#include "acq_common.h" -/* 53. RC_PARTACQ - 부분매입(회차 합계) 대사. */ +/* RC_PARTACQ - 부분매입(회차 합계) 대사. */ void RC_PARTACQ(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long mid; - const char *st; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_amt, h_n, h_psum; - char h_bizdate[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "PARTACQ_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_PARTACQ"); + snprintf(h_msg, sizeof(h_msg), "[RC_PARTACQ] 부분매입(회차 합계) 대사."); + strcpy(h_key, "-"); + userlog("[RC_PARTACQ] 대사 진입: 부분매입(회차 합계) 대사."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - - EXEC SQL SELECT amount INTO :h_amt FROM purchase WHERE purchase_id = :h_pid; - if (sqlca.sqlcode != 0) FAIL(b); - EXEC SQL SELECT count(*), coalesce(sum(month_amount),0) INTO :h_n, :h_psum - FROM ac_install WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - - if (h_n == 0) st = "EXCEPTION"; - else if (h_psum == h_amt) st = "MATCHED"; - else st = "HOLD"; - - if (h_n > 0 && h_psum != h_amt) { - EXEC SQL INSERT INTO rc_diff - (diff_id, purchase_id, expected_amt, actual_amt, diff_amt, diff_kind, biz_date) - VALUES (nextval('rc_diff_seq'), :h_pid, :h_amt, :h_psum, - :h_amt - :h_psum, 'AMT', :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_PARTACQ] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); } - mid = rcdb_insert_match(h_pid, "PARTACQ", h_psum, st, bizdate); - if (mid < 0) FAIL(b); - setl(b, T_COUNT, h_n); - setl(b, T_AMT2, h_psum); - setl(b, T_ID1, mid); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_PARTACQ] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_PARTACQ] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_PARTACQ] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_PARTACQ] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_PARTACQ] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_PARTACQ] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_PARTACQ] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_PARTACQ] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_partacq CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_partacq; + for (;;) { + EXEC SQL FETCH cur_rc_partacq INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_partacq; userlog("[RC_PARTACQ] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_partacq; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_partacq; + userlog("[RC_PARTACQ] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_PARTACQ] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_PARTACQ] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_PARTACQ] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 대사 로그 적재 (rc_match_log) */ + h_id = rcdb_insert_match(h_pid, "PARTACQ", h_net, + (unmatched_rows == 0 ? "MATCHED" : "EXCEPTION"), bizdate); + if (h_id < 0) { userlog("[RC_PARTACQ] 대사 로그 적재 실패 pid=%ld", h_pid); FAIL(b); } + out_cnt = matched_rows > 0 ? matched_rows : 1; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_PARTACQ] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_PARTACQ scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_PARTACQ] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_PARTACQ] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_PARTACQ] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_AMT2, h_net); + setl(b, T_COUNT, out_cnt); + setl(b, T_ID1, h_id); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_PARTACQ] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_PARTDEP.pgc b/app/src/rc/svc/RC_PARTDEP.pgc index 3d297d3..eaab0c1 100644 --- a/app/src/rc/svc/RC_PARTDEP.pgc +++ b/app/src/rc/svc/RC_PARTDEP.pgc @@ -1,29 +1,131 @@ /* - * RC_PARTDEP.pgc - rc 모듈 서비스 RC_PARTDEP (one service = one file). - * 부분 입금 대사: 매입 순액보다 RECON 입금 누계가 작은 부분입금 건수를 집계. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_PARTDEP.pgc - rc 모듈 서비스 RC_PARTDEP (대사 온라인 프로그램, 심화판). + * 업무: 부분 입금 대사: 매입 순액보다 RECON 입금 누계가 작은 부분입금 건수를 집계. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_PARTDEP.h" +#include "acq_common.h" -/* 121. RC_PARTDEP - 부분 입금 대사: 매입 순액보다 RECON 입금 누계가 작은 부분입금 건수를 집계. */ +/* RC_PARTDEP - 부분 입금 대사: 매입 순액보다 RECON 입금 누계가 작은 부분입금 건수를 집계. */ void RC_PARTDEP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_cnt; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "PARTDEP_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_PARTDEP"); + snprintf(h_msg, sizeof(h_msg), "[RC_PARTDEP] 부분 입금 대사: 매입 순액보다 RECON 입금 누계가 작은 부분입금 건수를 집계."); + strcpy(h_key, "-"); + userlog("[RC_PARTDEP] 대사 진입: 부분 입금 대사: 매입 순액보다 RECON 입금 누계가 작은 부분입금 건수를 집계."); - EXEC SQL SELECT count(*) INTO :h_cnt FROM purchase p + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_PARTDEP] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_PARTDEP] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_PARTDEP] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_PARTDEP] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_partdep CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_partdep; + for (;;) { + EXEC SQL FETCH cur_rc_partdep INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_partdep; userlog("[RC_PARTDEP] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_partdep; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_partdep; + userlog("[RC_PARTDEP] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_PARTDEP] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_PARTDEP] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_PARTDEP] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_PARTDEP] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_PARTDEP scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_PARTDEP] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p WHERE p.biz_date = :h_bizdate - AND coalesce((SELECT sum(amount) FROM ledger l - WHERE l.purchase_id = p.purchase_id AND l.entry_type = 'RECON'),0) < p.net; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_PARTDEP] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_PARTDEP] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_PARTDEP] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_PARTIAL.pgc b/app/src/rc/svc/RC_PARTIAL.pgc index c30d6ea..53275ee 100644 --- a/app/src/rc/svc/RC_PARTIAL.pgc +++ b/app/src/rc/svc/RC_PARTIAL.pgc @@ -1,20 +1,160 @@ /* - * RC_PARTIAL.pgc - rc 모듈 서비스 RC_PARTIAL (one service = one file). - * Moved verbatim from the former inline rc_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by rc_svr at tpsvrinit. + * RC_PARTIAL.pgc - rc 모듈 서비스 RC_PARTIAL (대사 온라인 프로그램, 심화판). + * 업무: 부분 대사 로그. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_PARTIAL.h" +#include "acq_common.h" -/* 18. RC_PARTIAL - 부분 대사 로그. */ +/* RC_PARTIAL - 부분 대사 로그. */ void RC_PARTIAL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long pid = getl(b, T_PURCHASE_ID), part = getl(b, T_AMOUNT), mid; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; + EXEC SQL END DECLARE SECTION; + + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "PARTIAL_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_PARTIAL"); + snprintf(h_msg, sizeof(h_msg), "[RC_PARTIAL] 부분 대사 로그."); + strcpy(h_key, "-"); + userlog("[RC_PARTIAL] 대사 진입: 부분 대사 로그."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - mid = rcdb_insert_match(pid, "PARTIAL", part, "MATCHED", bizdate); - if (mid < 0) FAIL(b); - setl(b, T_ID1, mid); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_PARTIAL] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_PARTIAL] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + h_amt = getl(b, T_AMOUNT); + if (h_amt < 0) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "금액은 음수가 될 수 없음", 0L); + userlog("[RC_PARTIAL] 금액 검증 실패 amt=%ld", h_amt); FAIL(b); + } + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8004); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_PARTIAL] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_PARTIAL] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_PARTIAL] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_PARTIAL] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_PARTIAL] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_PARTIAL] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_PARTIAL] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_partial CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_partial; + for (;;) { + EXEC SQL FETCH cur_rc_partial INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_partial; userlog("[RC_PARTIAL] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_partial; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_partial; + userlog("[RC_PARTIAL] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_PARTIAL] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_PARTIAL] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_PARTIAL] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 대사 로그 적재 (rc_match_log) */ + h_id = rcdb_insert_match(h_pid, "PARTIAL", h_net, + (unmatched_rows == 0 ? "MATCHED" : "EXCEPTION"), bizdate); + if (h_id < 0) { userlog("[RC_PARTIAL] 대사 로그 적재 실패 pid=%ld", h_pid); FAIL(b); } + out_cnt = matched_rows > 0 ? matched_rows : 1; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_PARTIAL] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_PARTIAL scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_PARTIAL] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_PARTIAL] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_PARTIAL] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_ID1, h_id); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_PARTIAL] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_PAYGAP.pgc b/app/src/rc/svc/RC_PAYGAP.pgc index 98ad19f..991b65a 100644 --- a/app/src/rc/svc/RC_PAYGAP.pgc +++ b/app/src/rc/svc/RC_PAYGAP.pgc @@ -1,28 +1,135 @@ /* - * RC_PAYGAP.pgc - rc 모듈 서비스 RC_PAYGAP (one service = one file). - * 지급 누락 감지: 대사완료(M) 매입 중 정산 레코드가 없는 미지급 건수를 반환. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_PAYGAP.pgc - rc 모듈 서비스 RC_PAYGAP (대사 온라인 프로그램, 심화판). + * 업무: 지급 누락 감지: 대사완료(M) 매입 중 정산 레코드가 없는 미지급 건수를 반환. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_PAYGAP.h" +#include "acq_common.h" -/* 83. RC_PAYGAP - 지급 누락 감지: 대사완료(M) 매입 중 정산 레코드가 없는 미지급 건수를 반환. */ +/* RC_PAYGAP - 지급 누락 감지: 대사완료(M) 매입 중 정산 레코드가 없는 미지급 건수를 반환. */ void RC_PAYGAP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_cnt; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "PAYGAP_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_PAYGAP"); + snprintf(h_msg, sizeof(h_msg), "[RC_PAYGAP] 지급 누락 감지: 대사완료(M) 매입 중 정산 레코드가 없는 미지급 건수를 반환."); + strcpy(h_key, "-"); + userlog("[RC_PAYGAP] 대사 진입: 지급 누락 감지: 대사완료(M) 매입 중 정산 레코드가 없는 미지급 건수를 반환."); - EXEC SQL SELECT count(*) INTO :h_cnt FROM purchase p - WHERE p.biz_date = :h_bizdate AND p.status = 'M' - AND NOT EXISTS (SELECT 1 FROM settlement s WHERE s.purchase_id = p.purchase_id); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_PAYGAP] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_PAYGAP] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_PAYGAP] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_PAYGAP] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_paygap CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_paygap; + for (;;) { + EXEC SQL FETCH cur_rc_paygap INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_paygap; userlog("[RC_PAYGAP] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_paygap; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_paygap; + userlog("[RC_PAYGAP] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_PAYGAP] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_PAYGAP] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_PAYGAP] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 예외 적재 (rc_exception) */ + if (h_ecnt >= 0) { + h_id = rcdb_insert_exception(h_pid, h_reason, bizdate); + if (h_id < 0) { userlog("[RC_PAYGAP] 예외 적재 실패 pid=%ld", h_pid); FAIL(b); } + } + out_cnt = h_ecnt + (h_pid > 0 ? 1 : 0); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_PAYGAP] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_PAYGAP scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_PAYGAP] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_PAYGAP] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_PAYGAP] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_PAYGAP] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_PAYLEDGER.pgc b/app/src/rc/svc/RC_PAYLEDGER.pgc index 6bbbefd..dea67e4 100644 --- a/app/src/rc/svc/RC_PAYLEDGER.pgc +++ b/app/src/rc/svc/RC_PAYLEDGER.pgc @@ -1,32 +1,133 @@ /* - * RC_PAYLEDGER.pgc - rc 모듈 서비스 RC_PAYLEDGER (one service = one file). - * 지급-원장 축 대사: 정산 순액과 원장 SETTLE 기표 합계가 일치하는지 검증하고 차액 반환. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_PAYLEDGER.pgc - rc 모듈 서비스 RC_PAYLEDGER (대사 온라인 프로그램, 심화판). + * 업무: 지급-원장 축 대사: 정산 순액과 원장 SETTLE 기표 합계가 일치하는지 검증하고 차액 반환. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_PAYLEDGER.h" +#include "acq_common.h" -/* 79. RC_PAYLEDGER - 지급-원장 축 대사: 정산 순액과 원장 SETTLE 기표 합계가 일치하는지 검증하고 차액 반환. */ +/* RC_PAYLEDGER - 지급-원장 축 대사: 정산 순액과 원장 SETTLE 기표 합계가 일치하는지 검증하고 차액 반환. */ void RC_PAYLEDGER(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_settle, h_ledger; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "PAYLEDGER_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_PAYLEDGER"); + snprintf(h_msg, sizeof(h_msg), "[RC_PAYLEDGER] 지급-원장 축 대사: 정산 순액과 원장 SETTLE 기표 합계가 일치하는지 검증하고 차액 반환."); + strcpy(h_key, "-"); + userlog("[RC_PAYLEDGER] 대사 진입: 지급-원장 축 대사: 정산 순액과 원장 SETTLE 기표 합계가 일치하는지 검증하고 차액 반환."); - EXEC SQL SELECT coalesce(sum(net),0) INTO :h_settle FROM settlement - WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_ledger FROM ledger - WHERE biz_date = :h_bizdate AND entry_type = 'SETTLE'; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_GROSS, h_settle); - setl(b, T_ARG1, h_ledger); - setl(b, T_DELTA, h_settle - h_ledger); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_PAYLEDGER] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_PAYLEDGER] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_PAYLEDGER] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_PAYLEDGER] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_payledger CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_payledger; + for (;;) { + EXEC SQL FETCH cur_rc_payledger INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_payledger; userlog("[RC_PAYLEDGER] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_payledger; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_payledger; + userlog("[RC_PAYLEDGER] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_PAYLEDGER] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_PAYLEDGER] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_PAYLEDGER] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_PAYLEDGER] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_PAYLEDGER scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_PAYLEDGER] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_PAYLEDGER] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_PAYLEDGER] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_ARG1, matched_rows); + setl(b, T_DELTA, h_diff); + setl(b, T_GROSS, h_gross); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_PAYLEDGER] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_PAYMATCH.pgc b/app/src/rc/svc/RC_PAYMATCH.pgc index f4f151a..55b612d 100644 --- a/app/src/rc/svc/RC_PAYMATCH.pgc +++ b/app/src/rc/svc/RC_PAYMATCH.pgc @@ -1,44 +1,159 @@ /* - * RC_PAYMATCH.pgc - rc 모듈 서비스 RC_PAYMATCH (one service = one file). - * 지급 대사: 매입 net 대비 원장(ledger) SETTLE/XFER 지급 합계를 비교하고, - * 차이가 있으면 rc_diff 에 금액 차이 라인을 적재한 뒤 대사 로그를 남긴다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_PAYMATCH.pgc - rc 모듈 서비스 RC_PAYMATCH (대사 온라인 프로그램, 심화판). + * 업무: 지급(원장) 대사 + 차이 적재. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_PAYMATCH.h" +#include "acq_common.h" -/* 33. RC_PAYMATCH - 지급(원장) 대사 + 차이 적재. */ +/* RC_PAYMATCH - 지급(원장) 대사 + 차이 적재. */ void RC_PAYMATCH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long mid; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_pnet, h_paysum; - char h_bizdate[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "PAYMATCH_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_PAYMATCH"); + snprintf(h_msg, sizeof(h_msg), "[RC_PAYMATCH] 지급(원장) 대사 + 차이 적재."); + strcpy(h_key, "-"); + userlog("[RC_PAYMATCH] 대사 진입: 지급(원장) 대사 + 차이 적재."); + + /* [1] 입력 검증부 -------------------------------------------------- */ 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 net INTO :h_pnet FROM purchase WHERE purchase_id = :h_pid; - if (sqlca.sqlcode != 0) FAIL(b); - EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_paysum FROM ledger - WHERE purchase_id = :h_pid AND entry_type IN ('SETTLE','XFER'); - if (sqlca.sqlcode < 0) FAIL(b); - - if (h_paysum != h_pnet) { - EXEC SQL INSERT INTO rc_diff - (diff_id, purchase_id, expected_amt, actual_amt, diff_amt, diff_kind, biz_date) - VALUES (nextval('rc_diff_seq'), :h_pid, :h_pnet, :h_paysum, - :h_pnet - :h_paysum, 'AMT', :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_PAYMATCH] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); } - mid = rcdb_insert_match(h_pid, "PAY", h_paysum, - (h_paysum == h_pnet) ? "MATCHED" : "EXCEPTION", bizdate); - if (mid < 0) FAIL(b); - setl(b, T_DELTA, h_pnet - h_paysum); - setl(b, T_ID1, mid); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_PAYMATCH] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_PAYMATCH] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_PAYMATCH] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_PAYMATCH] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_PAYMATCH] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_PAYMATCH] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_PAYMATCH] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_PAYMATCH] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_paymatch CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_paymatch; + for (;;) { + EXEC SQL FETCH cur_rc_paymatch INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_paymatch; userlog("[RC_PAYMATCH] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_paymatch; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_paymatch; + userlog("[RC_PAYMATCH] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_PAYMATCH] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_PAYMATCH] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_PAYMATCH] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 차이 라인 적재 (rc_diff) */ + h_exp = h_net; h_act = h_led; h_diff = h_exp - h_act; + EXEC SQL SELECT nextval('rc_diff_seq') INTO :h_id; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL INSERT INTO rc_diff (diff_id, purchase_id, expected_amt, actual_amt, diff_amt, diff_kind, biz_date) + VALUES (:h_id, :h_pid, :h_exp, :h_act, :h_diff, :h_dtype, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_PAYMATCH] 차이 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + out_cnt = (h_diff != 0 ? 1 : 0); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_PAYMATCH] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_PAYMATCH scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_PAYMATCH] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_PAYMATCH] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_PAYMATCH] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_DELTA, h_diff); + setl(b, T_ID1, h_id); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_PAYMATCH] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_POSRECON.pgc b/app/src/rc/svc/RC_POSRECON.pgc index 4855eee..5c1bb90 100644 --- a/app/src/rc/svc/RC_POSRECON.pgc +++ b/app/src/rc/svc/RC_POSRECON.pgc @@ -1,32 +1,131 @@ /* - * RC_POSRECON.pgc - rc 모듈 서비스 RC_POSRECON (one service = one file). - * POS 채널 대사: POS 채널 매입의 대사완료율을 건수 기준으로 산출(백분율). - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_POSRECON.pgc - rc 모듈 서비스 RC_POSRECON (대사 온라인 프로그램, 심화판). + * 업무: POS 채널 대사: POS 채널 매입의 대사완료율을 건수 기준으로 산출(백분율). + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_POSRECON.h" +#include "acq_common.h" -/* 88. RC_POSRECON - POS 채널 대사: POS 채널 매입의 대사완료율을 건수 기준으로 산출(백분율). */ +/* RC_POSRECON - POS 채널 대사: POS 채널 매입의 대사완료율을 건수 기준으로 산출(백분율). */ void RC_POSRECON(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_total, h_done, h_rate; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "POSRECON_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_POSRECON"); + snprintf(h_msg, sizeof(h_msg), "[RC_POSRECON] POS 채널 대사: POS 채널 매입의 대사완료율을 건수 기준으로 산출(백분율)."); + strcpy(h_key, "-"); + userlog("[RC_POSRECON] 대사 진입: POS 채널 대사: POS 채널 매입의 대사완료율을 건수 기준으로 산출(백분율)."); - EXEC SQL SELECT count(*) INTO :h_total FROM purchase - WHERE biz_date = :h_bizdate AND channel = 'POS'; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_done FROM purchase - WHERE biz_date = :h_bizdate AND channel = 'POS' AND status IN ('M','S'); - if (sqlca.sqlcode < 0) FAIL(b); - h_rate = (h_total > 0) ? (h_done * 100 / h_total) : 0; - setl(b, T_COUNT, h_total); - setl(b, T_RC, h_rate); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_POSRECON] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_POSRECON] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_POSRECON] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_POSRECON] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_posrecon CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_posrecon; + for (;;) { + EXEC SQL FETCH cur_rc_posrecon INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_posrecon; userlog("[RC_POSRECON] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_posrecon; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_posrecon; + userlog("[RC_POSRECON] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_POSRECON] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_POSRECON] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_POSRECON] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_POSRECON] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_POSRECON scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_POSRECON] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_POSRECON] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_POSRECON] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + setl(b, T_RC, rc_code); + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_POSRECON] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_PROGRESS.pgc b/app/src/rc/svc/RC_PROGRESS.pgc index f9801e8..f895ebe 100644 --- a/app/src/rc/svc/RC_PROGRESS.pgc +++ b/app/src/rc/svc/RC_PROGRESS.pgc @@ -1,27 +1,130 @@ /* - * RC_PROGRESS.pgc - rc 모듈 서비스 RC_PROGRESS (one service = one file). - * Moved verbatim from the former inline rc_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by rc_svr at tpsvrinit. + * RC_PROGRESS.pgc - rc 모듈 서비스 RC_PROGRESS (대사 온라인 프로그램, 심화판). + * 업무: 진행률(%) = MATCHED / 전체매입 * 100. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_PROGRESS.h" +#include "acq_common.h" -/* 21. RC_PROGRESS - 진행률(%) = MATCHED / 전체매입 * 100. */ +/* RC_PROGRESS - 진행률(%) = MATCHED / 전체매입 * 100. */ void RC_PROGRESS(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_total, h_matched; - char h_bizdate[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; + + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "PROGRESS_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_PROGRESS"); + snprintf(h_msg, sizeof(h_msg), "[RC_PROGRESS] 진행률(%) = MATCHED / 전체매입 * 100."); + strcpy(h_key, "-"); + userlog("[RC_PROGRESS] 대사 진입: 진행률(%) = MATCHED / 전체매입 * 100."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_PROGRESS] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT count(*) INTO :h_total FROM purchase WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_matched FROM rc_match_log + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_PROGRESS] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log WHERE biz_date = :h_bizdate AND status = 'MATCHED'; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_RC, h_total > 0 ? (h_matched * 100 / h_total) : 0); + if (sqlca.sqlcode < 0) { userlog("[RC_PROGRESS] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_PROGRESS] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_progress CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_progress; + for (;;) { + EXEC SQL FETCH cur_rc_progress INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_progress; userlog("[RC_PROGRESS] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_progress; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_progress; + userlog("[RC_PROGRESS] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_PROGRESS] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_PROGRESS] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_PROGRESS] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_PROGRESS] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_PROGRESS scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_PROGRESS] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_PROGRESS] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_PROGRESS] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_RC, rc_code); + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_PROGRESS] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_RATEBYISS.pgc b/app/src/rc/svc/RC_RATEBYISS.pgc index 970d029..0dc7f8a 100644 --- a/app/src/rc/svc/RC_RATEBYISS.pgc +++ b/app/src/rc/svc/RC_RATEBYISS.pgc @@ -1,40 +1,135 @@ /* - * RC_RATEBYISS.pgc - rc 모듈 서비스 RC_RATEBYISS (one service = one file). - * 발급사별 대사율: 요청 발급사의 당일 대사 로그를 매입과 조인하여 총건수와 - * MATCHED 건수를 한 번에 집계하고 발급사 대사율(bp)을 계산한다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_RATEBYISS.pgc - rc 모듈 서비스 RC_RATEBYISS (대사 온라인 프로그램, 심화판). + * 업무: 발급사별 대사율(bp). + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_RATEBYISS.h" +#include "acq_common.h" -/* 48. RC_RATEBYISS - 발급사별 대사율(bp). */ +/* RC_RATEBYISS - 발급사별 대사율(bp). */ void RC_RATEBYISS(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], issuer[32]; - long rate; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_tot, h_mat; - char h_bizdate[16], h_issuer[32]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "RATEBYISS_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_RATEBYISS"); + snprintf(h_msg, sizeof(h_msg), "[RC_RATEBYISS] 발급사별 대사율(bp)."); + strcpy(h_key, "-"); + userlog("[RC_RATEBYISS] 대사 진입: 발급사별 대사율(bp)."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - gets_(b, T_ISSUER, issuer, sizeof(issuer)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_RATEBYISS] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - strncpy(h_issuer, issuer, sizeof(h_issuer)-1); h_issuer[sizeof(h_issuer)-1] = 0; + strcpy(h_key, bizdate); + gets_(b, T_ISSUER, tmpin, sizeof(tmpin)); if (tmpin[0]) { strncpy(h_issuer, tmpin, sizeof(h_issuer)-1); h_issuer[sizeof(h_issuer)-1]=0; } + strcpy(h_key, h_issuer); - EXEC SQL SELECT count(*), - coalesce(sum(CASE WHEN m.status = 'MATCHED' THEN 1 ELSE 0 END),0) - INTO :h_tot, :h_mat - FROM rc_match_log m, purchase p - WHERE m.purchase_id = p.purchase_id - AND m.biz_date = :h_bizdate AND p.issuer = :h_issuer; - if (sqlca.sqlcode < 0) FAIL(b); + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_RATEBYISS] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_RATEBYISS] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_RATEBYISS] 예외 집계 오류"); FAIL(b); } - rate = (h_tot > 0) ? (h_mat * 10000) / h_tot : 0; - setl(b, T_COUNT, h_tot); - setl(b, T_ARG1, h_mat); - setl(b, T_RC, rate); + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_ratebyiss CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_ratebyiss; + for (;;) { + EXEC SQL FETCH cur_rc_ratebyiss INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_ratebyiss; userlog("[RC_RATEBYISS] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_ratebyiss; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_ratebyiss; + userlog("[RC_RATEBYISS] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_RATEBYISS] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_RATEBYISS] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_RATEBYISS] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_RATEBYISS] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_RATEBYISS scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_RATEBYISS] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_RATEBYISS] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_RATEBYISS] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_ARG1, matched_rows); + setl(b, T_COUNT, out_cnt); Bchg(b, T_ISSUER, 0, h_issuer, 0L); + setl(b, T_RC, rc_code); + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_RATEBYISS] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_RATECHAN.pgc b/app/src/rc/svc/RC_RATECHAN.pgc index a02a80e..02b973e 100644 --- a/app/src/rc/svc/RC_RATECHAN.pgc +++ b/app/src/rc/svc/RC_RATECHAN.pgc @@ -1,33 +1,137 @@ /* - * RC_RATECHAN.pgc - rc 모듈 서비스 RC_RATECHAN (one service = one file). - * 채널별 대사율: 지정 채널의 당일 매입 대비 대사완료 비율을 백분율로 산출. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_RATECHAN.pgc - rc 모듈 서비스 RC_RATECHAN (대사 온라인 프로그램, 심화판). + * 업무: 채널별 대사율: 지정 채널의 당일 매입 대비 대사완료 비율을 백분율로 산출. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_RATECHAN.h" +#include "acq_common.h" -/* 129. RC_RATECHAN - 채널별 대사율: 지정 채널의 당일 매입 대비 대사완료 비율을 백분율로 산출. */ +/* RC_RATECHAN - 채널별 대사율: 지정 채널의 당일 매입 대비 대사완료 비율을 백분율로 산출. */ void RC_RATECHAN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_total, h_done; char h_chan[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "RATECHAN_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_RATECHAN"); + snprintf(h_msg, sizeof(h_msg), "[RC_RATECHAN] 채널별 대사율: 지정 채널의 당일 매입 대비 대사완료 비율을 백분율로 산출."); + strcpy(h_key, "-"); + userlog("[RC_RATECHAN] 대사 진입: 채널별 대사율: 지정 채널의 당일 매입 대비 대사완료 비율을 백분율로 산출."); - { char c[16]; gets_(b, T_CHANNEL, c, sizeof(c)); - strncpy(h_chan, c, sizeof(h_chan)-1); h_chan[sizeof(h_chan)-1] = 0; } - EXEC SQL SELECT count(*) INTO :h_total FROM purchase - WHERE biz_date = :h_bizdate AND channel = :h_chan; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_done FROM purchase - WHERE biz_date = :h_bizdate AND channel = :h_chan AND status IN ('M','S'); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_total); - setl(b, T_RC, (h_total > 0) ? (h_done * 100 / h_total) : 0); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_RATECHAN] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + gets_(b, T_CHANNEL, tmpin, sizeof(tmpin)); if (tmpin[0]) { strncpy(h_channel, tmpin, sizeof(h_channel)-1); h_channel[sizeof(h_channel)-1]=0; } + if (strcmp(h_channel,"POS") && strcmp(h_channel,"EDC") && strcmp(h_channel,"EDI") && strcmp(h_channel,"FOREIGN")) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "채널 코드 오류(POS/EDC/EDI/FOREIGN)", 0L); + userlog("[RC_RATECHAN] 채널 코드 오류 ch=[%s]", h_channel); FAIL(b); + } + strcpy(h_key, h_channel); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_RATECHAN] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_RATECHAN] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_RATECHAN] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_ratechan CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_ratechan; + for (;;) { + EXEC SQL FETCH cur_rc_ratechan INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_ratechan; userlog("[RC_RATECHAN] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_ratechan; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_ratechan; + userlog("[RC_RATECHAN] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_RATECHAN] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_RATECHAN] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_RATECHAN] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_RATECHAN] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_RATECHAN scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_RATECHAN] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_RATECHAN] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_RATECHAN] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + setl(b, T_RC, rc_code); + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_RATECHAN] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_RATEDAY.pgc b/app/src/rc/svc/RC_RATEDAY.pgc index 566b5c1..ee430bf 100644 --- a/app/src/rc/svc/RC_RATEDAY.pgc +++ b/app/src/rc/svc/RC_RATEDAY.pgc @@ -1,29 +1,130 @@ /* - * RC_RATEDAY.pgc - rc 모듈 서비스 RC_RATEDAY (one service = one file). - * 일별 대사율: 당일 매입 대비 대사완료 건수 비율을 백분율로 산출. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_RATEDAY.pgc - rc 모듈 서비스 RC_RATEDAY (대사 온라인 프로그램, 심화판). + * 업무: 일별 대사율: 당일 매입 대비 대사완료 건수 비율을 백분율로 산출. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_RATEDAY.h" +#include "acq_common.h" -/* 126. RC_RATEDAY - 일별 대사율: 당일 매입 대비 대사완료 건수 비율을 백분율로 산출. */ +/* RC_RATEDAY - 일별 대사율: 당일 매입 대비 대사완료 건수 비율을 백분율로 산출. */ void RC_RATEDAY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_total, h_done; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "RATEDAY_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_RATEDAY"); + snprintf(h_msg, sizeof(h_msg), "[RC_RATEDAY] 일별 대사율: 당일 매입 대비 대사완료 건수 비율을 백분율로 산출."); + strcpy(h_key, "-"); + userlog("[RC_RATEDAY] 대사 진입: 일별 대사율: 당일 매입 대비 대사완료 건수 비율을 백분율로 산출."); - EXEC SQL SELECT count(*) INTO :h_total FROM purchase WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_done FROM purchase - WHERE biz_date = :h_bizdate AND status IN ('M','S'); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_RC, (h_total > 0) ? (h_done * 100 / h_total) : 0); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_RATEDAY] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_RATEDAY] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_RATEDAY] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_RATEDAY] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_rateday CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_rateday; + for (;;) { + EXEC SQL FETCH cur_rc_rateday INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_rateday; userlog("[RC_RATEDAY] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_rateday; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_rateday; + userlog("[RC_RATEDAY] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_RATEDAY] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_RATEDAY] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_RATEDAY] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_RATEDAY] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_RATEDAY scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_RATEDAY] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_RATEDAY] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_RATEDAY] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_RC, rc_code); + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_RATEDAY] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_RATEMERCH.pgc b/app/src/rc/svc/RC_RATEMERCH.pgc index 27c40c4..5784215 100644 --- a/app/src/rc/svc/RC_RATEMERCH.pgc +++ b/app/src/rc/svc/RC_RATEMERCH.pgc @@ -1,33 +1,144 @@ /* - * RC_RATEMERCH.pgc - rc 모듈 서비스 RC_RATEMERCH (one service = one file). - * 가맹점별 대사율: 지정 가맹점의 당일 매입 대비 대사완료 비율을 백분율로 산출. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_RATEMERCH.pgc - rc 모듈 서비스 RC_RATEMERCH (대사 온라인 프로그램, 심화판). + * 업무: 가맹점별 대사율: 지정 가맹점의 당일 매입 대비 대사완료 비율을 백분율로 산출. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_RATEMERCH.h" +#include "acq_common.h" -/* 130. RC_RATEMERCH - 가맹점별 대사율: 지정 가맹점의 당일 매입 대비 대사완료 비율을 백분율로 산출. */ +/* RC_RATEMERCH - 가맹점별 대사율: 지정 가맹점의 당일 매입 대비 대사완료 비율을 백분율로 산출. */ void RC_RATEMERCH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_total, h_done; char h_mid[64]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "RATEMERCH_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_RATEMERCH"); + snprintf(h_msg, sizeof(h_msg), "[RC_RATEMERCH] 가맹점별 대사율: 지정 가맹점의 당일 매입 대비 대사완료 비율을 백분율로 산출."); + strcpy(h_key, "-"); + userlog("[RC_RATEMERCH] 대사 진입: 가맹점별 대사율: 지정 가맹점의 당일 매입 대비 대사완료 비율을 백분율로 산출."); - { char c[64]; gets_(b, T_MERCHANT, c, sizeof(c)); - strncpy(h_mid, c, sizeof(h_mid)-1); h_mid[sizeof(h_mid)-1] = 0; } - EXEC SQL SELECT count(*) INTO :h_total FROM purchase - WHERE biz_date = :h_bizdate AND merchant_id = :h_mid; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_done FROM purchase - WHERE biz_date = :h_bizdate AND merchant_id = :h_mid AND status IN ('M','S'); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_total); - setl(b, T_RC, (h_total > 0) ? (h_done * 100 / h_total) : 0); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_RATEMERCH] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + gets_(b, T_MERCHANT, tmpin, sizeof(tmpin)); + if (tmpin[0] == 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "가맹점 ID 필수", 0L); + userlog("[RC_RATEMERCH] 가맹점 ID 미입력"); FAIL(b); + } + strncpy(h_merch, tmpin, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strcpy(h_key, h_merch); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_RATEMERCH] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_RATEMERCH] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_RATEMERCH] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_RATEMERCH] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_RATEMERCH] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_ratemerch CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_ratemerch; + for (;;) { + EXEC SQL FETCH cur_rc_ratemerch INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_ratemerch; userlog("[RC_RATEMERCH] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_ratemerch; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_ratemerch; + userlog("[RC_RATEMERCH] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_RATEMERCH] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_RATEMERCH] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_RATEMERCH] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_RATEMERCH] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_RATEMERCH scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_RATEMERCH] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_RATEMERCH] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_RATEMERCH] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + setl(b, T_RC, rc_code); + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_RATEMERCH] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_RATEMON.pgc b/app/src/rc/svc/RC_RATEMON.pgc index 4e630ff..8c928e0 100644 --- a/app/src/rc/svc/RC_RATEMON.pgc +++ b/app/src/rc/svc/RC_RATEMON.pgc @@ -1,32 +1,131 @@ /* - * RC_RATEMON.pgc - rc 모듈 서비스 RC_RATEMON (one service = one file). - * 월별 대사율: 기준일이 속한 달 전체 매입 대비 대사완료 비율을 백분율로 산출. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_RATEMON.pgc - rc 모듈 서비스 RC_RATEMON (대사 온라인 프로그램, 심화판). + * 업무: 월별 대사율: 기준일이 속한 달 전체 매입 대비 대사완료 비율을 백분율로 산출. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_RATEMON.h" +#include "acq_common.h" -/* 128. RC_RATEMON - 월별 대사율: 기준일이 속한 달 전체 매입 대비 대사완료 비율을 백분율로 산출. */ +/* RC_RATEMON - 월별 대사율: 기준일이 속한 달 전체 매입 대비 대사완료 비율을 백분율로 산출. */ void RC_RATEMON(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_total, h_done; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "RATEMON_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_RATEMON"); + snprintf(h_msg, sizeof(h_msg), "[RC_RATEMON] 월별 대사율: 기준일이 속한 달 전체 매입 대비 대사완료 비율을 백분율로 산출."); + strcpy(h_key, "-"); + userlog("[RC_RATEMON] 대사 진입: 월별 대사율: 기준일이 속한 달 전체 매입 대비 대사완료 비율을 백분율로 산출."); - EXEC SQL SELECT count(*) INTO :h_total FROM purchase - WHERE date_trunc('month', biz_date) = date_trunc('month', :h_bizdate::date); - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_done FROM purchase - WHERE date_trunc('month', biz_date) = date_trunc('month', :h_bizdate::date) - AND status IN ('M','S'); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_total); - setl(b, T_RC, (h_total > 0) ? (h_done * 100 / h_total) : 0); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_RATEMON] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_RATEMON] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_RATEMON] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_RATEMON] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_ratemon CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_ratemon; + for (;;) { + EXEC SQL FETCH cur_rc_ratemon INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_ratemon; userlog("[RC_RATEMON] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_ratemon; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_ratemon; + userlog("[RC_RATEMON] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_RATEMON] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_RATEMON] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_RATEMON] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_RATEMON] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_RATEMON scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_RATEMON] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_RATEMON] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_RATEMON] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + setl(b, T_RC, rc_code); + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_RATEMON] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_RATESTAT.pgc b/app/src/rc/svc/RC_RATESTAT.pgc index 0c3d4bf..1b0229f 100644 --- a/app/src/rc/svc/RC_RATESTAT.pgc +++ b/app/src/rc/svc/RC_RATESTAT.pgc @@ -1,35 +1,132 @@ /* - * RC_RATESTAT.pgc - rc 모듈 서비스 RC_RATESTAT (one service = one file). - * 대사율 통계: 당일 전체 대사 로그 대비 MATCHED 비율을 bp(1/10000) 단위로 - * 계산하여 총건수/성공건수/대사율을 반환한다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_RATESTAT.pgc - rc 모듈 서비스 RC_RATESTAT (대사 온라인 프로그램, 심화판). + * 업무: 당일 대사율(bp) 통계. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_RATESTAT.h" +#include "acq_common.h" -/* 47. RC_RATESTAT - 당일 대사율(bp) 통계. */ +/* RC_RATESTAT - 당일 대사율(bp) 통계. */ void RC_RATESTAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long rate; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_tot, h_mat; - char h_bizdate[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "RATESTAT_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_RATESTAT"); + snprintf(h_msg, sizeof(h_msg), "[RC_RATESTAT] 당일 대사율(bp) 통계."); + strcpy(h_key, "-"); + userlog("[RC_RATESTAT] 대사 진입: 당일 대사율(bp) 통계."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_RATESTAT] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); - EXEC SQL SELECT count(*), - coalesce(sum(CASE WHEN status = 'MATCHED' THEN 1 ELSE 0 END),0) - INTO :h_tot, :h_mat - FROM rc_match_log WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_RATESTAT] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_RATESTAT] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_RATESTAT] 예외 집계 오류"); FAIL(b); } - rate = (h_tot > 0) ? (h_mat * 10000) / h_tot : 0; - setl(b, T_COUNT, h_tot); - setl(b, T_ARG1, h_mat); - setl(b, T_RC, rate); + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_ratestat CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_ratestat; + for (;;) { + EXEC SQL FETCH cur_rc_ratestat INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_ratestat; userlog("[RC_RATESTAT] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_ratestat; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_ratestat; + userlog("[RC_RATESTAT] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_RATESTAT] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_RATESTAT] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_RATESTAT] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_RATESTAT] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_RATESTAT scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_RATESTAT] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_RATESTAT] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_RATESTAT] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_ARG1, matched_rows); + setl(b, T_COUNT, out_cnt); + setl(b, T_RC, rc_code); + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_RATESTAT] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_RATEWEEK.pgc b/app/src/rc/svc/RC_RATEWEEK.pgc index 81ba660..e49d2a7 100644 --- a/app/src/rc/svc/RC_RATEWEEK.pgc +++ b/app/src/rc/svc/RC_RATEWEEK.pgc @@ -1,32 +1,131 @@ /* - * RC_RATEWEEK.pgc - rc 모듈 서비스 RC_RATEWEEK (one service = one file). - * 주별 대사율: 기준일 포함 최근 7영업일 매입 대비 대사완료 비율을 백분율로 산출. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_RATEWEEK.pgc - rc 모듈 서비스 RC_RATEWEEK (대사 온라인 프로그램, 심화판). + * 업무: 주별 대사율: 기준일 포함 최근 7영업일 매입 대비 대사완료 비율을 백분율로 산출. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_RATEWEEK.h" +#include "acq_common.h" -/* 127. RC_RATEWEEK - 주별 대사율: 기준일 포함 최근 7영업일 매입 대비 대사완료 비율을 백분율로 산출. */ +/* RC_RATEWEEK - 주별 대사율: 기준일 포함 최근 7영업일 매입 대비 대사완료 비율을 백분율로 산출. */ void RC_RATEWEEK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_total, h_done; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "RATEWEEK_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_RATEWEEK"); + snprintf(h_msg, sizeof(h_msg), "[RC_RATEWEEK] 주별 대사율: 기준일 포함 최근 7영업일 매입 대비 대사완료 비율을 백분율로 산출."); + strcpy(h_key, "-"); + userlog("[RC_RATEWEEK] 대사 진입: 주별 대사율: 기준일 포함 최근 7영업일 매입 대비 대사완료 비율을 백분율로 산출."); - EXEC SQL SELECT count(*) INTO :h_total FROM purchase - WHERE biz_date > :h_bizdate::date - 7 AND biz_date <= :h_bizdate::date; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_done FROM purchase - WHERE biz_date > :h_bizdate::date - 7 AND biz_date <= :h_bizdate::date - AND status IN ('M','S'); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_total); - setl(b, T_RC, (h_total > 0) ? (h_done * 100 / h_total) : 0); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_RATEWEEK] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_RATEWEEK] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_RATEWEEK] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_RATEWEEK] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_rateweek CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_rateweek; + for (;;) { + EXEC SQL FETCH cur_rc_rateweek INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_rateweek; userlog("[RC_RATEWEEK] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_rateweek; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_rateweek; + userlog("[RC_RATEWEEK] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_RATEWEEK] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_RATEWEEK] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_RATEWEEK] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_RATEWEEK] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_RATEWEEK scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_RATEWEEK] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_RATEWEEK] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_RATEWEEK] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + setl(b, T_RC, rc_code); + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_RATEWEEK] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_RECORR.pgc b/app/src/rc/svc/RC_RECORR.pgc index 5321172..000cbcf 100644 --- a/app/src/rc/svc/RC_RECORR.pgc +++ b/app/src/rc/svc/RC_RECORR.pgc @@ -1,33 +1,155 @@ /* - * RC_RECORR.pgc - rc 모듈 서비스 RC_RECORR (one service = one file). - * 정정후 재대사: 수수료조정이 반영된 매입을 대상으로 순액 재검증 후 로그를 남긴다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_RECORR.pgc - rc 모듈 서비스 RC_RECORR (대사 온라인 프로그램, 심화판). + * 업무: 정정후 재대사: 수수료조정이 반영된 매입을 대상으로 순액 재검증 후 로그를 남긴다. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_RECORR.h" +#include "acq_common.h" -/* 119. RC_RECORR - 정정후 재대사: 수수료조정이 반영된 매입을 대상으로 순액 재검증 후 로그를 남긴다. */ +/* RC_RECORR - 정정후 재대사: 수수료조정이 반영된 매입을 대상으로 순액 재검증 후 로그를 남긴다. */ void RC_RECORR(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_id, h_pid, h_net, h_adj; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "RECORR_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_RECORR"); + snprintf(h_msg, sizeof(h_msg), "[RC_RECORR] 정정후 재대사: 수수료조정이 반영된 매입을 대상으로 순액 재검증 후 로그를 남긴다."); + strcpy(h_key, "-"); + userlog("[RC_RECORR] 대사 진입: 정정후 재대사: 수수료조정이 반영된 매입을 대상으로 순액 재검증 후 로그를 남긴다."); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_RECORR] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); h_pid = getl(b, T_PURCHASE_ID); - EXEC SQL SELECT coalesce(net,0) INTO :h_net FROM purchase WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0 && sqlca.sqlcode != 100) FAIL(b); - EXEC SQL SELECT coalesce(sum(delta),0) INTO :h_adj FROM ac_fee_adj WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT nextval('rc_match_seq') INTO :h_id; - EXEC SQL INSERT INTO rc_match_log (match_id, purchase_id, match_type, amount, status, biz_date) - VALUES (:h_id, :h_pid, 'RECORR', :h_net + :h_adj, 'MATCHED', :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_NET, h_net + h_adj); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_RECORR] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_RECORR] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_RECORR] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_RECORR] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_RECORR] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_RECORR] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_RECORR] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_RECORR] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_recorr CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_recorr; + for (;;) { + EXEC SQL FETCH cur_rc_recorr INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_recorr; userlog("[RC_RECORR] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_recorr; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_recorr; + userlog("[RC_RECORR] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_RECORR] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_RECORR] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_RECORR] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 대사 로그 적재 (rc_match_log) */ + h_id = rcdb_insert_match(h_pid, "RECORR", h_net, + (unmatched_rows == 0 ? "MATCHED" : "EXCEPTION"), bizdate); + if (h_id < 0) { userlog("[RC_RECORR] 대사 로그 적재 실패 pid=%ld", h_pid); FAIL(b); } + out_cnt = matched_rows > 0 ? matched_rows : 1; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_RECORR] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_RECORR scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_RECORR] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_RECORR] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_RECORR] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_NET, h_net); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_RECORR] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_REDIFF.pgc b/app/src/rc/svc/RC_REDIFF.pgc index 215bf80..db75cb6 100644 --- a/app/src/rc/svc/RC_REDIFF.pgc +++ b/app/src/rc/svc/RC_REDIFF.pgc @@ -1,34 +1,158 @@ /* - * RC_REDIFF.pgc - rc 모듈 서비스 RC_REDIFF (one service = one file). - * 차이발생 재대사: 지정 매입의 기대 순액과 SETTLE 실적 차이를 재산출해 rc_diff 갱신. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_REDIFF.pgc - rc 모듈 서비스 RC_REDIFF (대사 온라인 프로그램, 심화판). + * 업무: 차이발생 재대사: 지정 매입의 기대 순액과 SETTLE 실적 차이를 재산출해 rc_diff 갱신. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_REDIFF.h" +#include "acq_common.h" -/* 117. RC_REDIFF - 차이발생 재대사: 지정 매입의 기대 순액과 SETTLE 실적 차이를 재산출해 rc_diff 갱신. */ +/* RC_REDIFF - 차이발생 재대사: 지정 매입의 기대 순액과 SETTLE 실적 차이를 재산출해 rc_diff 갱신. */ void RC_REDIFF(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_id, h_pid, h_exp, h_act; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "REDIFF_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_REDIFF"); + snprintf(h_msg, sizeof(h_msg), "[RC_REDIFF] 차이발생 재대사: 지정 매입의 기대 순액과 SETTLE 실적 차이를 재산출해 rc_diff 갱신."); + strcpy(h_key, "-"); + userlog("[RC_REDIFF] 대사 진입: 차이발생 재대사: 지정 매입의 기대 순액과 SETTLE 실적 차이를 재산출해 rc_diff 갱신."); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_REDIFF] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); h_pid = getl(b, T_PURCHASE_ID); - EXEC SQL SELECT coalesce(net,0) INTO :h_exp FROM purchase WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0 && sqlca.sqlcode != 100) FAIL(b); - EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger - WHERE purchase_id = :h_pid AND entry_type = 'SETTLE'; - if (sqlca.sqlcode < 0) FAIL(b); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_REDIFF] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_REDIFF] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_REDIFF] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_REDIFF] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_REDIFF] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_REDIFF] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_REDIFF] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_REDIFF] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_rediff CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_rediff; + for (;;) { + EXEC SQL FETCH cur_rc_rediff INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_rediff; userlog("[RC_REDIFF] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_rediff; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_rediff; + userlog("[RC_REDIFF] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_REDIFF] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_REDIFF] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_REDIFF] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 차이 라인 적재 (rc_diff) */ + h_exp = h_net; h_act = h_led; h_diff = h_exp - h_act; EXEC SQL SELECT nextval('rc_diff_seq') INTO :h_id; - EXEC SQL INSERT INTO rc_diff (diff_id, purchase_id, expected_amt, actual_amt, diff_amt, diff_kind, biz_date) - VALUES (:h_id, :h_pid, :h_exp, :h_act, :h_exp - :h_act, 'AMT', :h_bizdate); if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_DELTA, h_exp - h_act); + EXEC SQL INSERT INTO rc_diff (diff_id, purchase_id, expected_amt, actual_amt, diff_amt, diff_kind, biz_date) + VALUES (:h_id, :h_pid, :h_exp, :h_act, :h_diff, :h_dtype, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_REDIFF] 차이 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + out_cnt = (h_diff != 0 ? 1 : 0); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_REDIFF] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_REDIFF scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_REDIFF] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_REDIFF] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_REDIFF] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_DELTA, h_diff); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_REDIFF] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_REFULL.pgc b/app/src/rc/svc/RC_REFULL.pgc index 9aaef36..aa7ef19 100644 --- a/app/src/rc/svc/RC_REFULL.pgc +++ b/app/src/rc/svc/RC_REFULL.pgc @@ -1,31 +1,132 @@ /* - * RC_REFULL.pgc - rc 모듈 서비스 RC_REFULL (one service = one file). - * 전체 재대사: 당일 전체 매입 대비 대사완료(M/S) 비율을 재계산해 진척률로 반환. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_REFULL.pgc - rc 모듈 서비스 RC_REFULL (대사 온라인 프로그램, 심화판). + * 업무: 전체 재대사: 당일 전체 매입 대비 대사완료(M/S) 비율을 재계산해 진척률로 반환. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_REFULL.h" +#include "acq_common.h" -/* 120. RC_REFULL - 전체 재대사: 당일 전체 매입 대비 대사완료(M/S) 비율을 재계산해 진척률로 반환. */ +/* RC_REFULL - 전체 재대사: 당일 전체 매입 대비 대사완료(M/S) 비율을 재계산해 진척률로 반환. */ void RC_REFULL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_total, h_done; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "REFULL_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_REFULL"); + snprintf(h_msg, sizeof(h_msg), "[RC_REFULL] 전체 재대사: 당일 전체 매입 대비 대사완료(M/S) 비율을 재계산해 진척률로 반환."); + strcpy(h_key, "-"); + userlog("[RC_REFULL] 대사 진입: 전체 재대사: 당일 전체 매입 대비 대사완료(M/S) 비율을 재계산해 진척률로 반환."); - EXEC SQL SELECT count(*) INTO :h_total FROM purchase WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_done FROM purchase - WHERE biz_date = :h_bizdate AND status IN ('M','S'); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_total); - setl(b, T_ARG1, h_done); - setl(b, T_RC, (h_total > 0) ? (h_done * 100 / h_total) : 0); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_REFULL] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_REFULL] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_REFULL] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_REFULL] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_refull CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_refull; + for (;;) { + EXEC SQL FETCH cur_rc_refull INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_refull; userlog("[RC_REFULL] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_refull; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_refull; + userlog("[RC_REFULL] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_REFULL] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_REFULL] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_REFULL] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_REFULL] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_REFULL scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_REFULL] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_REFULL] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_REFULL] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_ARG1, matched_rows); + setl(b, T_COUNT, out_cnt); + setl(b, T_RC, rc_code); + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_REFULL] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_REFUNDREC.pgc b/app/src/rc/svc/RC_REFUNDREC.pgc index 6042020..3c577b0 100644 --- a/app/src/rc/svc/RC_REFUNDREC.pgc +++ b/app/src/rc/svc/RC_REFUNDREC.pgc @@ -1,28 +1,132 @@ /* - * RC_REFUNDREC.pgc - rc 모듈 서비스 RC_REFUNDREC (one service = one file). - * 환불분 대사: 취소 유형이 REFUND 인 환불 금액과 건수를 별도로 대사 집계. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_REFUNDREC.pgc - rc 모듈 서비스 RC_REFUNDREC (대사 온라인 프로그램, 심화판). + * 업무: 환불분 대사: 취소 유형이 REFUND 인 환불 금액과 건수를 별도로 대사 집계. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_REFUNDREC.h" +#include "acq_common.h" -/* 123. RC_REFUNDREC - 환불분 대사: 취소 유형이 REFUND 인 환불 금액과 건수를 별도로 대사 집계. */ +/* RC_REFUNDREC - 환불분 대사: 취소 유형이 REFUND 인 환불 금액과 건수를 별도로 대사 집계. */ void RC_REFUNDREC(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_cnt, h_amt; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "REFUNDREC_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_REFUNDREC"); + snprintf(h_msg, sizeof(h_msg), "[RC_REFUNDREC] 환불분 대사: 취소 유형이 REFUND 인 환불 금액과 건수를 별도로 대사 집계."); + strcpy(h_key, "-"); + userlog("[RC_REFUNDREC] 대사 진입: 환불분 대사: 취소 유형이 REFUND 인 환불 금액과 건수를 별도로 대사 집계."); - EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_amt - FROM ac_cancel WHERE biz_date = :h_bizdate AND cancel_type = 'REFUND'; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_REFUNDREC] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_REFUNDREC] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_REFUNDREC] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_REFUNDREC] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_refundrec CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_refundrec; + for (;;) { + EXEC SQL FETCH cur_rc_refundrec INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_refundrec; userlog("[RC_REFUNDREC] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_refundrec; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_refundrec; + userlog("[RC_REFUNDREC] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_REFUNDREC] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_REFUNDREC] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_REFUNDREC] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_REFUNDREC] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_REFUNDREC scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_REFUNDREC] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_REFUNDREC] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_REFUNDREC] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ setl(b, T_AMOUNT, h_amt); + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_REFUNDREC] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_RELATE.pgc b/app/src/rc/svc/RC_RELATE.pgc index 958d1f0..63249ea 100644 --- a/app/src/rc/svc/RC_RELATE.pgc +++ b/app/src/rc/svc/RC_RELATE.pgc @@ -1,28 +1,134 @@ /* - * RC_RELATE.pgc - rc 모듈 서비스 RC_RELATE (one service = one file). - * 지연도착 재대사: 원장 기표가 매입일보다 늦게 도착한 지연 건을 재대사 대상으로 집계. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_RELATE.pgc - rc 모듈 서비스 RC_RELATE (대사 온라인 프로그램, 심화판). + * 업무: 지연도착 재대사: 원장 기표가 매입일보다 늦게 도착한 지연 건을 재대사 대상으로 집계. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_RELATE.h" +#include "acq_common.h" -/* 118. RC_RELATE - 지연도착 재대사: 원장 기표가 매입일보다 늦게 도착한 지연 건을 재대사 대상으로 집계. */ +/* RC_RELATE - 지연도착 재대사: 원장 기표가 매입일보다 늦게 도착한 지연 건을 재대사 대상으로 집계. */ void RC_RELATE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_cnt; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "RELATE_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_RELATE"); + snprintf(h_msg, sizeof(h_msg), "[RC_RELATE] 지연도착 재대사: 원장 기표가 매입일보다 늦게 도착한 지연 건을 재대사 대상으로 집계."); + strcpy(h_key, "-"); + userlog("[RC_RELATE] 대사 진입: 지연도착 재대사: 원장 기표가 매입일보다 늦게 도착한 지연 건을 재대사 대상으로 집계."); - EXEC SQL SELECT count(DISTINCT p.purchase_id) INTO :h_cnt FROM purchase p - JOIN ledger l ON l.purchase_id = p.purchase_id - WHERE p.biz_date = :h_bizdate AND l.biz_date > p.biz_date; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_RELATE] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_RELATE] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_RELATE] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_RELATE] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_relate CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_relate; + for (;;) { + EXEC SQL FETCH cur_rc_relate INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_relate; userlog("[RC_RELATE] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_relate; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_relate; + userlog("[RC_RELATE] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_RELATE] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_RELATE] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_RELATE] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 대사 로그 적재 (rc_match_log) */ + h_id = rcdb_insert_match(h_pid, "RELATE", h_net, + (unmatched_rows == 0 ? "MATCHED" : "EXCEPTION"), bizdate); + if (h_id < 0) { userlog("[RC_RELATE] 대사 로그 적재 실패 pid=%ld", h_pid); FAIL(b); } + out_cnt = matched_rows > 0 ? matched_rows : 1; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_RELATE] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_RELATE scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_RELATE] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_RELATE] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_RELATE] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_RELATE] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_RELEASE.pgc b/app/src/rc/svc/RC_RELEASE.pgc index 2e6041e..e947555 100644 --- a/app/src/rc/svc/RC_RELEASE.pgc +++ b/app/src/rc/svc/RC_RELEASE.pgc @@ -1,17 +1,137 @@ /* - * RC_RELEASE.pgc - rc 모듈 서비스 RC_RELEASE (one service = one file). - * Moved verbatim from the former inline rc_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by rc_svr at tpsvrinit. + * RC_RELEASE.pgc - rc 모듈 서비스 RC_RELEASE (대사 온라인 프로그램, 심화판). + * 업무: 대사 보류 해제 (HOLD -> RELEASED). + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_RELEASE.h" +#include "acq_common.h" -/* 12. RC_RELEASE - 대사 보류 해제 (HOLD -> RELEASED). */ +/* RC_RELEASE - 대사 보류 해제 (HOLD -> RELEASED). */ void RC_RELEASE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long pid = getl(b, T_PURCHASE_ID); - if (rcdb_update_match_status(pid, "HOLD", "RELEASED") < 0) FAIL(b); - Bchg(b, T_STATUS, 0, "R", 0L); + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; + EXEC SQL END DECLARE SECTION; + + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "RELEASE_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_RELEASE"); + snprintf(h_msg, sizeof(h_msg), "[RC_RELEASE] 대사 보류 해제 (HOLD -> RELEASED)."); + strcpy(h_key, "-"); + userlog("[RC_RELEASE] 대사 진입: 대사 보류 해제 (HOLD -> RELEASED)."); + + /* [1] 입력 검증부 -------------------------------------------------- */ + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_RELEASE] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + /* 조회 기준일 미입력 서비스: 매입 원장에서 영업일 유도 */ + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_RELEASE] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_RELEASE] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_RELEASE] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_RELEASE] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { userlog("[RC_RELEASE] 대사 로그 조회 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_led FROM ledger WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { userlog("[RC_RELEASE] 원장 조회 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_release CURSOR FOR + SELECT match_id, amount, status FROM rc_match_log + WHERE purchase_id = :h_pid ORDER BY match_id; + EXEC SQL OPEN cur_rc_release; + for (;;) { + EXEC SQL FETCH cur_rc_release INTO :h_id2, :h_val, :h_status; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_release; userlog("[RC_RELEASE] 이력 커서 오류"); FAIL(b); } + scan_rows++; + if (!strcmp(h_status, "MATCHED") || !strcmp(h_status, "CONFIRMED")) matched_rows++; + else { unmatched_rows++; unmatched_amt += (h_val < 0 ? -h_val : h_val); } + } + EXEC SQL CLOSE cur_rc_release; + userlog("[RC_RELEASE] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_RELEASE] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_RELEASE] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_RELEASE] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 상태 전이: HOLD -> RELEASED */ + if (rcdb_update_match_status(h_pid, "HOLD", "RELEASED") < 0) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "상태 전이 실패", 0L); + userlog("[RC_RELEASE] 상태 전이 실패 pid=%ld HOLD->RELEASED", h_pid); FAIL(b); + } + strcpy(h_status, "R"); out_cnt = 1; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_RELEASE] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_RELEASE scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_RELEASE] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { userlog("[RC_RELEASE] 정산 조회 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + Bchg(b, T_STATUS, 0, h_status, 0L); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_RELEASE] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_REMATCH.pgc b/app/src/rc/svc/RC_REMATCH.pgc index f0a5da1..693b6d8 100644 --- a/app/src/rc/svc/RC_REMATCH.pgc +++ b/app/src/rc/svc/RC_REMATCH.pgc @@ -1,25 +1,158 @@ /* - * RC_REMATCH.pgc - rc 모듈 서비스 RC_REMATCH (one service = one file). - * Moved verbatim from the former inline rc_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by rc_svr at tpsvrinit. + * RC_REMATCH.pgc - rc 모듈 서비스 RC_REMATCH (대사 온라인 프로그램, 심화판). + * 업무: 재대사 원장 기록. (KEEP) + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_REMATCH.h" +#include "acq_common.h" -/* 9. RC_REMATCH - 재대사 원장 기록. (KEEP) */ +/* RC_REMATCH - 재대사 원장 기록. (KEEP) */ void RC_REMATCH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_net; - char h_bizdate[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); h_net = getl(b, T_NET); + + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "REMATCH_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_REMATCH"); + snprintf(h_msg, sizeof(h_msg), "[RC_REMATCH] 재대사 원장 기록. (KEEP)"); + strcpy(h_key, "-"); + userlog("[RC_REMATCH] 대사 진입: 재대사 원장 기록. (KEEP)"); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_REMATCH] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_REMATCH] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + h_net = getl(b, T_NET); + if (h_net < -100000000000L || h_net > 100000000000L) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "순액 범위 초과", 0L); + userlog("[RC_REMATCH] 순액 범위 초과 net=%ld", h_net); FAIL(b); + } + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8004); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_REMATCH] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_REMATCH] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_REMATCH] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_REMATCH] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_REMATCH] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_REMATCH] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_REMATCH] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_rematch CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_rematch; + for (;;) { + EXEC SQL FETCH cur_rc_rematch INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_rematch; userlog("[RC_REMATCH] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_rematch; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_rematch; + userlog("[RC_REMATCH] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_REMATCH] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_REMATCH] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_REMATCH] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 보존: 재대사 원장 기록 */ 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); + if (sqlca.sqlcode < 0) { userlog("[RC_REMATCH] 재대사 원장 적재 실패"); FAIL(b); } + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_REMATCH] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_REMATCH scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_REMATCH] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_REMATCH] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_REMATCH] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_REMATCH] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_REOPEN.pgc b/app/src/rc/svc/RC_REOPEN.pgc index 6242c5d..7cf94d5 100644 --- a/app/src/rc/svc/RC_REOPEN.pgc +++ b/app/src/rc/svc/RC_REOPEN.pgc @@ -1,35 +1,135 @@ /* - * RC_REOPEN.pgc - rc 모듈 서비스 RC_REOPEN (one service = one file). - * 마감 재개: 확정(CONFIRMED)된 당일 대사 건을 다시 MATCHED 로 되돌려 마감을 - * 해제하고, 재개 사실을 WARN 경보로 남긴다. 되돌린 건수를 반환한다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_REOPEN.pgc - rc 모듈 서비스 RC_REOPEN (대사 온라인 프로그램, 심화판). + * 업무: 일마감 해제 (CONFIRMED -> MATCHED). + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_REOPEN.h" +#include "acq_common.h" -/* 74. RC_REOPEN - 일마감 해제 (CONFIRMED -> MATCHED). */ +/* RC_REOPEN - 일마감 해제 (CONFIRMED -> MATCHED). */ void RC_REOPEN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long rows; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16], h_msg[64]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "REOPEN_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_REOPEN"); + snprintf(h_msg, sizeof(h_msg), "[RC_REOPEN] 일마감 해제 (CONFIRMED -> MATCHED)."); + strcpy(h_key, "-"); + userlog("[RC_REOPEN] 대사 진입: 일마감 해제 (CONFIRMED -> MATCHED)."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_REOPEN] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); - EXEC SQL UPDATE rc_match_log SET status = 'MATCHED', updated_at = now() - WHERE biz_date = :h_bizdate AND status = 'CONFIRMED'; - if (sqlca.sqlcode < 0) FAIL(b); - rows = sqlca.sqlerrd[2]; - if (rows == 0) FAIL(b); + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_REOPEN] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_REOPEN] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_REOPEN] 예외 집계 오류"); FAIL(b); } - snprintf(h_msg, sizeof(h_msg), "recon day reopened: rows=%ld", rows); - EXEC SQL INSERT INTO rc_alert (alert_id, purchase_id, lvl, message, biz_date) - VALUES (nextval('rc_alert_seq'), 0, 'WARN', :h_msg, :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, rows); + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_reopen CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_reopen; + for (;;) { + EXEC SQL FETCH cur_rc_reopen INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_reopen; userlog("[RC_REOPEN] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_reopen; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_reopen; + userlog("[RC_REOPEN] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_REOPEN] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_REOPEN] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_REOPEN] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 상태 전이: CONFIRMED -> MATCHED */ + if (rcdb_update_match_status(h_pid, "CONFIRMED", "MATCHED") < 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "상태 전이 실패", 0L); + userlog("[RC_REOPEN] 상태 전이 실패 pid=%ld CONFIRMED->MATCHED", h_pid); FAIL(b); + } + strcpy(h_status, "M"); out_cnt = 1; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_REOPEN] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_REOPEN scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_REOPEN] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_REOPEN] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_REOPEN] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_REOPEN] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_REPROC.pgc b/app/src/rc/svc/RC_REPROC.pgc index c016ce4..989fb92 100644 --- a/app/src/rc/svc/RC_REPROC.pgc +++ b/app/src/rc/svc/RC_REPROC.pgc @@ -1,17 +1,137 @@ /* - * RC_REPROC.pgc - rc 모듈 서비스 RC_REPROC (one service = one file). - * Moved verbatim from the former inline rc_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by rc_svr at tpsvrinit. + * RC_REPROC.pgc - rc 모듈 서비스 RC_REPROC (대사 온라인 프로그램, 심화판). + * 업무: 재처리: EXCEPTION 대사 로그를 MATCHED 로 되돌림. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_REPROC.h" +#include "acq_common.h" -/* 23. RC_REPROC - 재처리: EXCEPTION 대사 로그를 MATCHED 로 되돌림. */ +/* RC_REPROC - 재처리: EXCEPTION 대사 로그를 MATCHED 로 되돌림. */ void RC_REPROC(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long pid = getl(b, T_PURCHASE_ID); - if (rcdb_update_match_status(pid, "EXCEPTION", "MATCHED") < 0) FAIL(b); - Bchg(b, T_STATUS, 0, "M", 0L); + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; + EXEC SQL END DECLARE SECTION; + + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "REPROC_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_REPROC"); + snprintf(h_msg, sizeof(h_msg), "[RC_REPROC] 재처리: EXCEPTION 대사 로그를 MATCHED 로 되돌림."); + strcpy(h_key, "-"); + userlog("[RC_REPROC] 대사 진입: 재처리: EXCEPTION 대사 로그를 MATCHED 로 되돌림."); + + /* [1] 입력 검증부 -------------------------------------------------- */ + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_REPROC] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + /* 조회 기준일 미입력 서비스: 매입 원장에서 영업일 유도 */ + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_REPROC] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_REPROC] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_REPROC] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_REPROC] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { userlog("[RC_REPROC] 대사 로그 조회 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_led FROM ledger WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { userlog("[RC_REPROC] 원장 조회 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_reproc CURSOR FOR + SELECT match_id, amount, status FROM rc_match_log + WHERE purchase_id = :h_pid ORDER BY match_id; + EXEC SQL OPEN cur_rc_reproc; + for (;;) { + EXEC SQL FETCH cur_rc_reproc INTO :h_id2, :h_val, :h_status; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_reproc; userlog("[RC_REPROC] 이력 커서 오류"); FAIL(b); } + scan_rows++; + if (!strcmp(h_status, "MATCHED") || !strcmp(h_status, "CONFIRMED")) matched_rows++; + else { unmatched_rows++; unmatched_amt += (h_val < 0 ? -h_val : h_val); } + } + EXEC SQL CLOSE cur_rc_reproc; + userlog("[RC_REPROC] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_REPROC] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_REPROC] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_REPROC] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 상태 전이: EXCEPTION -> MATCHED */ + if (rcdb_update_match_status(h_pid, "EXCEPTION", "MATCHED") < 0) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "상태 전이 실패", 0L); + userlog("[RC_REPROC] 상태 전이 실패 pid=%ld EXCEPTION->MATCHED", h_pid); FAIL(b); + } + strcpy(h_status, "M"); out_cnt = 1; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_REPROC] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_REPROC scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_REPROC] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { userlog("[RC_REPROC] 정산 조회 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + Bchg(b, T_STATUS, 0, h_status, 0L); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_REPROC] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_RERUN.pgc b/app/src/rc/svc/RC_RERUN.pgc index f4690f4..55d6ca3 100644 --- a/app/src/rc/svc/RC_RERUN.pgc +++ b/app/src/rc/svc/RC_RERUN.pgc @@ -1,43 +1,132 @@ /* - * RC_RERUN.pgc - rc 모듈 서비스 RC_RERUN (one service = one file). - * 임계초과 재대사 실행: 임계값을 넘는 당일 차이 건들을 커서로 순회하며 - * 각 매입에 대해 RERUN 유형의 HOLD 대사 로그를 새로 등록한다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_RERUN.pgc - rc 모듈 서비스 RC_RERUN (대사 온라인 프로그램, 심화판). + * 업무: 임계초과 건 재대사 등록 (커서 순회). + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_RERUN.h" +#include "acq_common.h" -/* 50. RC_RERUN - 임계초과 건 재대사 등록 (커서 순회). */ +/* RC_RERUN - 임계초과 건 재대사 등록 (커서 순회). */ void RC_RERUN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long done = 0; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_limit, h_pid, h_diff; - char h_bizdate[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; - h_limit = getl(b, T_AMT1); - gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "RERUN_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_RERUN"); + snprintf(h_msg, sizeof(h_msg), "[RC_RERUN] 임계초과 건 재대사 등록 (커서 순회)."); + strcpy(h_key, "-"); + userlog("[RC_RERUN] 대사 진입: 임계초과 건 재대사 등록 (커서 순회)."); - EXEC SQL DECLARE rrn CURSOR FOR - SELECT purchase_id, diff_amt FROM rc_diff - WHERE biz_date = :h_bizdate AND abs(diff_amt) > :h_limit - ORDER BY diff_id; - EXEC SQL OPEN rrn; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH rrn INTO :h_pid, :h_diff; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE rrn; FAIL(b); } - if (rcdb_insert_match(h_pid, "RERUN", h_diff, "HOLD", bizdate) < 0) { - EXEC SQL CLOSE rrn; FAIL(b); - } - done++; + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_RERUN] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); } - EXEC SQL CLOSE rrn; - setl(b, T_COUNT, done); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + h_n1 = getl(b, T_AMT1); if (h_n1 <= 0) h_n1 = 1000; /* 임계 기준금액 */ + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_RERUN] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_RERUN] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_RERUN] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_rerun CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_rerun; + for (;;) { + EXEC SQL FETCH cur_rc_rerun INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_rerun; userlog("[RC_RERUN] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_rerun; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_rerun; + userlog("[RC_RERUN] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_RERUN] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_RERUN] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_RERUN] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_RERUN] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_RERUN scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_RERUN] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_RERUN] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_RERUN] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_RERUN] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_RETRO.pgc b/app/src/rc/svc/RC_RETRO.pgc index 3a7ac6e..b5f88da 100644 --- a/app/src/rc/svc/RC_RETRO.pgc +++ b/app/src/rc/svc/RC_RETRO.pgc @@ -1,41 +1,162 @@ /* - * RC_RETRO.pgc - rc 모듈 서비스 RC_RETRO (one service = one file). - * 소급 대사: 과거 영업일에 보류/예외로 남은 특정 매입의 대사를 새 영업일로 - * 소급 확정(MATCHED)하고 소급 증적을 원장에 기록한다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_RETRO.pgc - rc 모듈 서비스 RC_RETRO (대사 온라인 프로그램, 심화판). + * 업무: 과거 보류/예외 건 소급 확정. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_RETRO.h" +#include "acq_common.h" -/* 45. RC_RETRO - 과거 보류/예외 건 소급 확정. */ +/* RC_RETRO - 과거 보류/예외 건 소급 확정. */ void RC_RETRO(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char olddate[16], newdate[16]; - long rows; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_net; - char h_old[16], h_new[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); - h_net = getl(b, T_NET); - gets_(b, T_STR1, olddate, sizeof(olddate)); - gets_(b, T_BIZDATE, newdate, sizeof(newdate)); - strncpy(h_old, olddate, sizeof(h_old)-1); h_old[sizeof(h_old)-1] = 0; - strncpy(h_new, newdate, sizeof(h_new)-1); h_new[sizeof(h_new)-1] = 0; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "RETRO_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_RETRO"); + snprintf(h_msg, sizeof(h_msg), "[RC_RETRO] 과거 보류/예외 건 소급 확정."); + strcpy(h_key, "-"); + userlog("[RC_RETRO] 대사 진입: 과거 보류/예외 건 소급 확정."); - EXEC SQL UPDATE rc_match_log - SET biz_date = CAST(:h_new AS date), status = 'MATCHED', updated_at = now() - WHERE purchase_id = :h_pid AND biz_date = :h_old - AND status IN ('HOLD','EXCEPTION'); - if (sqlca.sqlcode < 0) FAIL(b); - rows = sqlca.sqlerrd[2]; - if (rows > 0) { - EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) - VALUES (nextval('ledger_seq'), :h_pid, 'RC_RETRO', :h_net, :h_new); - if (sqlca.sqlcode < 0) FAIL(b); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_RETRO] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); } - setl(b, T_COUNT, rows); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_RETRO] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + h_net = getl(b, T_NET); + if (h_net < -100000000000L || h_net > 100000000000L) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "순액 범위 초과", 0L); + userlog("[RC_RETRO] 순액 범위 초과 net=%ld", h_net); FAIL(b); + } + gets_(b, T_STR1, tmpin, sizeof(tmpin)); + if (tmpin[0]) { strncpy(h_nbizdate, tmpin, sizeof(h_nbizdate)-1); h_nbizdate[sizeof(h_nbizdate)-1]=0; strncpy(h_lvl, tmpin, sizeof(h_lvl)-1); h_lvl[sizeof(h_lvl)-1]=0; } + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8004); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_RETRO] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_RETRO] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_RETRO] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_RETRO] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_RETRO] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_RETRO] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_RETRO] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_retro CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_retro; + for (;;) { + EXEC SQL FETCH cur_rc_retro INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_retro; userlog("[RC_RETRO] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_retro; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_retro; + userlog("[RC_RETRO] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_RETRO] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_RETRO] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_RETRO] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 대사 로그 적재 (rc_match_log) */ + h_id = rcdb_insert_match(h_pid, "RETRO", h_net, + (unmatched_rows == 0 ? "MATCHED" : "EXCEPTION"), bizdate); + if (h_id < 0) { userlog("[RC_RETRO] 대사 로그 적재 실패 pid=%ld", h_pid); FAIL(b); } + out_cnt = matched_rows > 0 ? matched_rows : 1; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_RETRO] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_RETRO scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_RETRO] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_RETRO] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_RETRO] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_RETRO] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_RETURN.pgc b/app/src/rc/svc/RC_RETURN.pgc index 5b2a44e..19c5232 100644 --- a/app/src/rc/svc/RC_RETURN.pgc +++ b/app/src/rc/svc/RC_RETURN.pgc @@ -1,21 +1,160 @@ /* - * RC_RETURN.pgc - rc 모듈 서비스 RC_RETURN (one service = one file). - * Moved verbatim from the former inline rc_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by rc_svr at tpsvrinit. + * RC_RETURN.pgc - rc 모듈 서비스 RC_RETURN (대사 온라인 프로그램, 심화판). + * 업무: 반품 대사: 반품 원장 + 음수 대사 로그. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_RETURN.h" +#include "acq_common.h" -/* 4. RC_RETURN - 반품 대사: 반품 원장 + 음수 대사 로그. */ +/* RC_RETURN - 반품 대사: 반품 원장 + 음수 대사 로그. */ void RC_RETURN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long pid = getl(b, T_PURCHASE_ID), amt = getl(b, T_AMOUNT), mid; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; + EXEC SQL END DECLARE SECTION; + + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "RETURN_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_RETURN"); + snprintf(h_msg, sizeof(h_msg), "[RC_RETURN] 반품 대사: 반품 원장 + 음수 대사 로그."); + strcpy(h_key, "-"); + userlog("[RC_RETURN] 대사 진입: 반품 대사: 반품 원장 + 음수 대사 로그."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (rcdb_insert_ledger(pid, "RETURN", -amt, bizdate) < 0) FAIL(b); - mid = rcdb_insert_match(pid, "RETURN", -amt, "MATCHED", bizdate); - if (mid < 0) FAIL(b); - setl(b, T_ID1, mid); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_RETURN] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_RETURN] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + h_amt = getl(b, T_AMOUNT); + if (h_amt < 0) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "금액은 음수가 될 수 없음", 0L); + userlog("[RC_RETURN] 금액 검증 실패 amt=%ld", h_amt); FAIL(b); + } + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8004); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_RETURN] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_RETURN] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_RETURN] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_RETURN] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_RETURN] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_RETURN] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_RETURN] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_return CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_return; + for (;;) { + EXEC SQL FETCH cur_rc_return INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_return; userlog("[RC_RETURN] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_return; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_return; + userlog("[RC_RETURN] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_RETURN] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_RETURN] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_RETURN] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 대사 로그 적재 (rc_match_log) */ + h_id = rcdb_insert_match(h_pid, "RETURN", h_net, + (unmatched_rows == 0 ? "MATCHED" : "EXCEPTION"), bizdate); + if (h_id < 0) { userlog("[RC_RETURN] 대사 로그 적재 실패 pid=%ld", h_pid); FAIL(b); } + out_cnt = matched_rows > 0 ? matched_rows : 1; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_RETURN] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_RETURN scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_RETURN] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_RETURN] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_RETURN] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_ID1, h_id); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_RETURN] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_ROLLBACK.pgc b/app/src/rc/svc/RC_ROLLBACK.pgc index f5ce309..080af7c 100644 --- a/app/src/rc/svc/RC_ROLLBACK.pgc +++ b/app/src/rc/svc/RC_ROLLBACK.pgc @@ -1,17 +1,137 @@ /* - * RC_ROLLBACK.pgc - rc 모듈 서비스 RC_ROLLBACK (one service = one file). - * Moved verbatim from the former inline rc_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by rc_svr at tpsvrinit. + * RC_ROLLBACK.pgc - rc 모듈 서비스 RC_ROLLBACK (대사 온라인 프로그램, 심화판). + * 업무: 대사 롤백 (-> ROLLBACK 상태). + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_ROLLBACK.h" +#include "acq_common.h" -/* 28. RC_ROLLBACK - 대사 롤백 (-> ROLLBACK 상태). */ +/* RC_ROLLBACK - 대사 롤백 (-> ROLLBACK 상태). */ void RC_ROLLBACK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long pid = getl(b, T_PURCHASE_ID); - if (rcdb_update_match_status(pid, "MATCHED", "ROLLBACK") < 0) FAIL(b); - Bchg(b, T_STATUS, 0, "X", 0L); + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; + EXEC SQL END DECLARE SECTION; + + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "ROLLBACK_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_ROLLBACK"); + snprintf(h_msg, sizeof(h_msg), "[RC_ROLLBACK] 대사 롤백 (-> ROLLBACK 상태)."); + strcpy(h_key, "-"); + userlog("[RC_ROLLBACK] 대사 진입: 대사 롤백 (-> ROLLBACK 상태)."); + + /* [1] 입력 검증부 -------------------------------------------------- */ + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_ROLLBACK] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + /* 조회 기준일 미입력 서비스: 매입 원장에서 영업일 유도 */ + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_ROLLBACK] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_ROLLBACK] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_ROLLBACK] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_ROLLBACK] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { userlog("[RC_ROLLBACK] 대사 로그 조회 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_led FROM ledger WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { userlog("[RC_ROLLBACK] 원장 조회 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_rollback CURSOR FOR + SELECT match_id, amount, status FROM rc_match_log + WHERE purchase_id = :h_pid ORDER BY match_id; + EXEC SQL OPEN cur_rc_rollback; + for (;;) { + EXEC SQL FETCH cur_rc_rollback INTO :h_id2, :h_val, :h_status; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_rollback; userlog("[RC_ROLLBACK] 이력 커서 오류"); FAIL(b); } + scan_rows++; + if (!strcmp(h_status, "MATCHED") || !strcmp(h_status, "CONFIRMED")) matched_rows++; + else { unmatched_rows++; unmatched_amt += (h_val < 0 ? -h_val : h_val); } + } + EXEC SQL CLOSE cur_rc_rollback; + userlog("[RC_ROLLBACK] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_ROLLBACK] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_ROLLBACK] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_ROLLBACK] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 상태 전이: MATCHED -> ROLLBACK */ + if (rcdb_update_match_status(h_pid, "MATCHED", "ROLLBACK") < 0) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "상태 전이 실패", 0L); + userlog("[RC_ROLLBACK] 상태 전이 실패 pid=%ld MATCHED->ROLLBACK", h_pid); FAIL(b); + } + strcpy(h_status, "X"); out_cnt = 1; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_ROLLBACK] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_ROLLBACK scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_ROLLBACK] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { userlog("[RC_ROLLBACK] 정산 조회 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + Bchg(b, T_STATUS, 0, h_status, 0L); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_ROLLBACK] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_ROUNDDIFF.pgc b/app/src/rc/svc/RC_ROUNDDIFF.pgc index 3704033..81efebb 100644 --- a/app/src/rc/svc/RC_ROUNDDIFF.pgc +++ b/app/src/rc/svc/RC_ROUNDDIFF.pgc @@ -1,27 +1,131 @@ /* - * RC_ROUNDDIFF.pgc - rc 모듈 서비스 RC_ROUNDDIFF (one service = one file). - * 반올림 차이 분석: 절대차가 1 이하인 단수 차이 건을 반올림 오차로 분류해 집계. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_ROUNDDIFF.pgc - rc 모듈 서비스 RC_ROUNDDIFF (대사 온라인 프로그램, 심화판). + * 업무: 반올림 차이 분석: 절대차가 1 이하인 단수 차이 건을 반올림 오차로 분류해 집계. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_ROUNDDIFF.h" +#include "acq_common.h" -/* 96. RC_ROUNDDIFF - 반올림 차이 분석: 절대차가 1 이하인 단수 차이 건을 반올림 오차로 분류해 집계. */ +/* RC_ROUNDDIFF - 반올림 차이 분석: 절대차가 1 이하인 단수 차이 건을 반올림 오차로 분류해 집계. */ void RC_ROUNDDIFF(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_cnt; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "ROUNDDIFF_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_ROUNDDIFF"); + snprintf(h_msg, sizeof(h_msg), "[RC_ROUNDDIFF] 반올림 차이 분석: 절대차가 1 이하인 단수 차이 건을 반올림 오차로 분류해 집계."); + strcpy(h_key, "-"); + userlog("[RC_ROUNDDIFF] 대사 진입: 반올림 차이 분석: 절대차가 1 이하인 단수 차이 건을 반올림 오차로 분류해 집계."); - EXEC SQL SELECT count(*) INTO :h_cnt FROM rc_diff - WHERE biz_date = :h_bizdate AND abs(diff_amt) <= 1 AND diff_amt <> 0; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_ROUNDDIFF] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_ROUNDDIFF] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_ROUNDDIFF] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_ROUNDDIFF] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_rounddiff CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_rounddiff; + for (;;) { + EXEC SQL FETCH cur_rc_rounddiff INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_rounddiff; userlog("[RC_ROUNDDIFF] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_rounddiff; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_rounddiff; + userlog("[RC_ROUNDDIFF] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_ROUNDDIFF] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_ROUNDDIFF] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_ROUNDDIFF] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_ROUNDDIFF] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_ROUNDDIFF scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_ROUNDDIFF] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_ROUNDDIFF] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_ROUNDDIFF] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_ROUNDDIFF] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_SIGNDIFF.pgc b/app/src/rc/svc/RC_SIGNDIFF.pgc index c9686eb..ca552cf 100644 --- a/app/src/rc/svc/RC_SIGNDIFF.pgc +++ b/app/src/rc/svc/RC_SIGNDIFF.pgc @@ -1,28 +1,131 @@ /* - * RC_SIGNDIFF.pgc - rc 모듈 서비스 RC_SIGNDIFF (one service = one file). - * 부호 오류 차이 분석: 기대와 실제의 부호가 서로 반대인 rc_diff 라인을 검출. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_SIGNDIFF.pgc - rc 모듈 서비스 RC_SIGNDIFF (대사 온라인 프로그램, 심화판). + * 업무: 부호 오류 차이 분석: 기대와 실제의 부호가 서로 반대인 rc_diff 라인을 검출. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_SIGNDIFF.h" +#include "acq_common.h" -/* 98. RC_SIGNDIFF - 부호 오류 차이 분석: 기대와 실제의 부호가 서로 반대인 rc_diff 라인을 검출. */ +/* RC_SIGNDIFF - 부호 오류 차이 분석: 기대와 실제의 부호가 서로 반대인 rc_diff 라인을 검출. */ void RC_SIGNDIFF(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_cnt; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "SIGNDIFF_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_SIGNDIFF"); + snprintf(h_msg, sizeof(h_msg), "[RC_SIGNDIFF] 부호 오류 차이 분석: 기대와 실제의 부호가 서로 반대인 rc_diff 라인을 검출."); + strcpy(h_key, "-"); + userlog("[RC_SIGNDIFF] 대사 진입: 부호 오류 차이 분석: 기대와 실제의 부호가 서로 반대인 rc_diff 라인을 검출."); - EXEC SQL SELECT count(*) INTO :h_cnt FROM rc_diff - WHERE biz_date = :h_bizdate - AND ((expected_amt > 0 AND actual_amt < 0) OR (expected_amt < 0 AND actual_amt > 0)); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_SIGNDIFF] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_SIGNDIFF] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_SIGNDIFF] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_SIGNDIFF] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_signdiff CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_signdiff; + for (;;) { + EXEC SQL FETCH cur_rc_signdiff INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_signdiff; userlog("[RC_SIGNDIFF] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_signdiff; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_signdiff; + userlog("[RC_SIGNDIFF] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_SIGNDIFF] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_SIGNDIFF] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_SIGNDIFF] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_SIGNDIFF] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_SIGNDIFF scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_SIGNDIFF] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_SIGNDIFF] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_SIGNDIFF] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_SIGNDIFF] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_SNAPSHOT.pgc b/app/src/rc/svc/RC_SNAPSHOT.pgc index bbd55a1..a9ebab3 100644 --- a/app/src/rc/svc/RC_SNAPSHOT.pgc +++ b/app/src/rc/svc/RC_SNAPSHOT.pgc @@ -1,40 +1,137 @@ /* - * RC_SNAPSHOT.pgc - rc 모듈 서비스 RC_SNAPSHOT (one service = one file). - * 대사 스냅샷: 당일 대사 로그의 총건수/MATCHED 건수/MATCHED 금액을 CASE - * 집계 한 방으로 산출하고 미대사 건수와 함께 요약에 스냅샷 upsert 한다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_SNAPSHOT.pgc - rc 모듈 서비스 RC_SNAPSHOT (대사 온라인 프로그램, 심화판). + * 업무: 현재 시점 대사 현황 스냅샷 upsert. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_SNAPSHOT.h" +#include "acq_common.h" -/* 75. RC_SNAPSHOT - 현재 시점 대사 현황 스냅샷 upsert. */ +/* RC_SNAPSHOT - 현재 시점 대사 현황 스냅샷 upsert. */ void RC_SNAPSHOT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long unmatched; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_tot, h_mat, h_gross; - char h_bizdate[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "SNAPSHOT_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_SNAPSHOT"); + snprintf(h_msg, sizeof(h_msg), "[RC_SNAPSHOT] 현재 시점 대사 현황 스냅샷 upsert."); + strcpy(h_key, "-"); + userlog("[RC_SNAPSHOT] 대사 진입: 현재 시점 대사 현황 스냅샷 upsert."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_SNAPSHOT] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); - EXEC SQL SELECT count(*), - coalesce(sum(CASE WHEN status = 'MATCHED' THEN 1 ELSE 0 END),0), - coalesce(sum(CASE WHEN status = 'MATCHED' THEN amount ELSE 0 END),0) - INTO :h_tot, :h_mat, :h_gross - FROM rc_match_log WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_SNAPSHOT] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_SNAPSHOT] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_SNAPSHOT] 예외 집계 오류"); FAIL(b); } - unmatched = rcdb_count_unmatched(bizdate); - if (unmatched < 0) FAIL(b); - if (rcdb_upsert_summary(bizdate, h_mat, unmatched, h_gross) < 0) FAIL(b); + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_snapshot CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_snapshot; + for (;;) { + EXEC SQL FETCH cur_rc_snapshot INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_snapshot; userlog("[RC_SNAPSHOT] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_snapshot; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_snapshot; + userlog("[RC_SNAPSHOT] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); - setl(b, T_COUNT, h_tot); - setl(b, T_ARG1, h_mat); - setl(b, T_ARG2, unmatched); + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_SNAPSHOT] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_SNAPSHOT] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_SNAPSHOT] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 요약 upsert (rc_summary) */ + if (rcdb_upsert_summary(bizdate, matched_rows + h_mcnt, unmatched_rows + h_ecnt, h_gross) < 0) { + userlog("[RC_SNAPSHOT] 요약 upsert 실패"); FAIL(b); + } + out_cnt = matched_rows + h_mcnt; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_SNAPSHOT] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_SNAPSHOT scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_SNAPSHOT] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_SNAPSHOT] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_SNAPSHOT] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_ARG1, matched_rows); + setl(b, T_ARG2, unmatched_rows); + setl(b, T_COUNT, out_cnt); setl(b, T_GROSS, h_gross); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_SNAPSHOT] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_STATS.pgc b/app/src/rc/svc/RC_STATS.pgc index b7b5bb9..7520566 100644 --- a/app/src/rc/svc/RC_STATS.pgc +++ b/app/src/rc/svc/RC_STATS.pgc @@ -1,22 +1,133 @@ /* - * RC_STATS.pgc - rc 모듈 서비스 RC_STATS (one service = one file). - * Moved verbatim from the former inline rc_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by rc_svr at tpsvrinit. + * RC_STATS.pgc - rc 모듈 서비스 RC_STATS (대사 온라인 프로그램, 심화판). + * 업무: MATCHED/EXCEPTION 통계 (dbio count). + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_STATS.h" +#include "acq_common.h" -/* 20. RC_STATS - MATCHED/EXCEPTION 통계 (dbio count). */ +/* RC_STATS - MATCHED/EXCEPTION 통계 (dbio count). */ void RC_STATS(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long matched, exc; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; + EXEC SQL END DECLARE SECTION; + + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "STATS_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_STATS"); + snprintf(h_msg, sizeof(h_msg), "[RC_STATS] MATCHED/EXCEPTION 통계 (dbio count)."); + strcpy(h_key, "-"); + userlog("[RC_STATS] 대사 진입: MATCHED/EXCEPTION 통계 (dbio count)."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - matched = rcdb_count_match(bizdate, "MATCHED"); - exc = rcdb_count_match(bizdate, "EXCEPTION"); - if (matched < 0 || exc < 0) FAIL(b); - setl(b, T_COUNT, matched); - setl(b, T_RC, exc); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_STATS] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_STATS] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_STATS] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_STATS] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_stats CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_stats; + for (;;) { + EXEC SQL FETCH cur_rc_stats INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_stats; userlog("[RC_STATS] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_stats; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_stats; + userlog("[RC_STATS] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_STATS] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_STATS] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_STATS] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 보존: MATCHED / EXCEPTION 통계 (dbio count) */ + h_mcnt = rcdb_count_match(bizdate, "MATCHED"); + h_ecnt = rcdb_count_match(bizdate, "EXCEPTION"); + if (h_mcnt < 0 || h_ecnt < 0) { userlog("[RC_STATS] 통계 집계 실패"); FAIL(b); } + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_STATS] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_STATS scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_STATS] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_STATS] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_STATS] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + setl(b, T_RC, rc_code); + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_STATS] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_STATUS.pgc b/app/src/rc/svc/RC_STATUS.pgc index fd3b24a..b9558ec 100644 --- a/app/src/rc/svc/RC_STATUS.pgc +++ b/app/src/rc/svc/RC_STATUS.pgc @@ -1,24 +1,133 @@ /* - * RC_STATUS.pgc - rc 모듈 서비스 RC_STATUS (one service = one file). - * Moved verbatim from the former inline rc_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by rc_svr at tpsvrinit. + * RC_STATUS.pgc - rc 모듈 서비스 RC_STATUS (대사 온라인 프로그램, 심화판). + * 업무: 최근 대사 상태 조회. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_STATUS.h" +#include "acq_common.h" -/* 30. RC_STATUS - 최근 대사 상태 조회. */ +/* RC_STATUS - 최근 대사 상태 조회. */ void RC_STATUS(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid; - char h_status[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; + + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "STATUS_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_STATUS"); + snprintf(h_msg, sizeof(h_msg), "[RC_STATUS] 최근 대사 상태 조회."); + strcpy(h_key, "-"); + userlog("[RC_STATUS] 대사 진입: 최근 대사 상태 조회."); + + /* [1] 입력 검증부 -------------------------------------------------- */ h_pid = getl(b, T_PURCHASE_ID); - EXEC SQL SELECT status INTO :h_status FROM rc_match_log - WHERE purchase_id = :h_pid ORDER BY match_id DESC LIMIT 1; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlcode == 100) strcpy(h_status, "NONE"); + if (h_pid <= 0) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_STATUS] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + /* 조회 기준일 미입력 서비스: 매입 원장에서 영업일 유도 */ + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_STATUS] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_STATUS] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_STATUS] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_STATUS] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { userlog("[RC_STATUS] 대사 로그 조회 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_led FROM ledger WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { userlog("[RC_STATUS] 원장 조회 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_status CURSOR FOR + SELECT match_id, amount, status FROM rc_match_log + WHERE purchase_id = :h_pid ORDER BY match_id; + EXEC SQL OPEN cur_rc_status; + for (;;) { + EXEC SQL FETCH cur_rc_status INTO :h_id2, :h_val, :h_status; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_status; userlog("[RC_STATUS] 이력 커서 오류"); FAIL(b); } + scan_rows++; + if (!strcmp(h_status, "MATCHED") || !strcmp(h_status, "CONFIRMED")) matched_rows++; + else { unmatched_rows++; unmatched_amt += (h_val < 0 ? -h_val : h_val); } + } + EXEC SQL CLOSE cur_rc_status; + userlog("[RC_STATUS] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_STATUS] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_STATUS] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_STATUS] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_STATUS] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_STATUS scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_STATUS] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { userlog("[RC_STATUS] 정산 조회 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ Bchg(b, T_STATUS, 0, h_status, 0L); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_STATUS] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_SUMCHK.pgc b/app/src/rc/svc/RC_SUMCHK.pgc index 2c3f291..08f774f 100644 --- a/app/src/rc/svc/RC_SUMCHK.pgc +++ b/app/src/rc/svc/RC_SUMCHK.pgc @@ -1,34 +1,135 @@ /* - * RC_SUMCHK.pgc - rc 모듈 서비스 RC_SUMCHK (one service = one file). - * 요약-상세 정합성 검증: rc_summary 의 확정 matched 건수와 대사 로그 실측 - * 건수를 대조하여 요약 테이블 훼손/누락 여부를 판정한다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_SUMCHK.pgc - rc 모듈 서비스 RC_SUMCHK (대사 온라인 프로그램, 심화판). + * 업무: 요약 대 실측 정합성 검증. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_SUMCHK.h" +#include "acq_common.h" -/* 70. RC_SUMCHK - 요약 대 실측 정합성 검증. */ +/* RC_SUMCHK - 요약 대 실측 정합성 검증. */ void RC_SUMCHK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_sum, h_live; - char h_bizdate[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "SUMCHK_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_SUMCHK"); + snprintf(h_msg, sizeof(h_msg), "[RC_SUMCHK] 요약 대 실측 정합성 검증."); + strcpy(h_key, "-"); + userlog("[RC_SUMCHK] 대사 진입: 요약 대 실측 정합성 검증."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_SUMCHK] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); - h_sum = -1; - EXEC SQL SELECT matched_cnt INTO :h_sum FROM rc_summary WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_live FROM rc_match_log + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_SUMCHK] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log WHERE biz_date = :h_bizdate AND status = 'MATCHED'; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) { userlog("[RC_SUMCHK] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_SUMCHK] 예외 집계 오류"); FAIL(b); } - setl(b, T_ARG1, h_sum); - setl(b, T_ARG2, h_live); - setl(b, T_RC, (h_sum == h_live) ? 0 : 1); + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_sumchk CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_sumchk; + for (;;) { + EXEC SQL FETCH cur_rc_sumchk INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_sumchk; userlog("[RC_SUMCHK] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_sumchk; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_sumchk; + userlog("[RC_SUMCHK] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_SUMCHK] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_SUMCHK] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_SUMCHK] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 요약 upsert (rc_summary) */ + if (rcdb_upsert_summary(bizdate, matched_rows + h_mcnt, unmatched_rows + h_ecnt, h_gross) < 0) { + userlog("[RC_SUMCHK] 요약 upsert 실패"); FAIL(b); + } + out_cnt = matched_rows + h_mcnt; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_SUMCHK] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_SUMCHK scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_SUMCHK] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_SUMCHK] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_SUMCHK] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_ARG1, matched_rows); + setl(b, T_ARG2, unmatched_rows); + setl(b, T_RC, rc_code); + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_SUMCHK] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_SUMMARY.pgc b/app/src/rc/svc/RC_SUMMARY.pgc index 6476648..e41a785 100644 --- a/app/src/rc/svc/RC_SUMMARY.pgc +++ b/app/src/rc/svc/RC_SUMMARY.pgc @@ -1,30 +1,135 @@ /* - * RC_SUMMARY.pgc - rc 모듈 서비스 RC_SUMMARY (one service = one file). - * Moved verbatim from the former inline rc_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by rc_svr at tpsvrinit. + * RC_SUMMARY.pgc - rc 모듈 서비스 RC_SUMMARY (대사 온라인 프로그램, 심화판). + * 업무: 대사 요약 upsert (dbio). + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_SUMMARY.h" +#include "acq_common.h" -/* 22. RC_SUMMARY - 대사 요약 upsert (dbio). */ +/* RC_SUMMARY - 대사 요약 upsert (dbio). */ void RC_SUMMARY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long matched, unmatched; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_gross; - char h_bizdate[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; + + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "SUMMARY_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_SUMMARY"); + snprintf(h_msg, sizeof(h_msg), "[RC_SUMMARY] 대사 요약 upsert (dbio)."); + strcpy(h_key, "-"); + userlog("[RC_SUMMARY] 대사 진입: 대사 요약 upsert (dbio)."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_SUMMARY] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - matched = rcdb_count_match(bizdate, "MATCHED"); - unmatched = rcdb_count_unmatched(bizdate); - if (matched < 0 || unmatched < 0) FAIL(b); - EXEC SQL SELECT coalesce(sum(net),0) INTO :h_gross FROM purchase WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - if (rcdb_upsert_summary(bizdate, matched, unmatched, h_gross) < 0) FAIL(b); - setl(b, T_COUNT, matched); + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_SUMMARY] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_SUMMARY] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_SUMMARY] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_summary CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_summary; + for (;;) { + EXEC SQL FETCH cur_rc_summary INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_summary; userlog("[RC_SUMMARY] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_summary; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_summary; + userlog("[RC_SUMMARY] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_SUMMARY] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_SUMMARY] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_SUMMARY] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 요약 upsert (rc_summary) */ + if (rcdb_upsert_summary(bizdate, matched_rows + h_mcnt, unmatched_rows + h_ecnt, h_gross) < 0) { + userlog("[RC_SUMMARY] 요약 upsert 실패"); FAIL(b); + } + out_cnt = matched_rows + h_mcnt; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_SUMMARY] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_SUMMARY scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_SUMMARY] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_SUMMARY] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_SUMMARY] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); setl(b, T_GROSS, h_gross); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_SUMMARY] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_TAXRECON.pgc b/app/src/rc/svc/RC_TAXRECON.pgc index c7d80a9..78de16f 100644 --- a/app/src/rc/svc/RC_TAXRECON.pgc +++ b/app/src/rc/svc/RC_TAXRECON.pgc @@ -1,46 +1,159 @@ /* - * RC_TAXRECON.pgc - rc 모듈 서비스 RC_TAXRECON (one service = one file). - * 세금계산서 대사: 발행된 세금계산서(공급가+부가세) 합계와 매입 금액을 - * 비교하여 차이가 있으면 rc_diff 적재, 상태를 MATCHED/EXCEPTION 으로 남긴다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_TAXRECON.pgc - rc 모듈 서비스 RC_TAXRECON (대사 온라인 프로그램, 심화판). + * 업무: 세금계산서(공급가+VAT) 대사. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_TAXRECON.h" +#include "acq_common.h" -/* 60. RC_TAXRECON - 세금계산서(공급가+VAT) 대사. */ +/* RC_TAXRECON - 세금계산서(공급가+VAT) 대사. */ void RC_TAXRECON(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long mid, diff; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_inv, h_invn, h_amt; - char h_bizdate[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "TAXRECON_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_TAXRECON"); + snprintf(h_msg, sizeof(h_msg), "[RC_TAXRECON] 세금계산서(공급가+VAT) 대사."); + strcpy(h_key, "-"); + userlog("[RC_TAXRECON] 대사 진입: 세금계산서(공급가+VAT) 대사."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - - EXEC SQL SELECT coalesce(sum(supply + vat),0), count(*) INTO :h_inv, :h_invn - FROM ac_tax_invoice WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_invn == 0) FAIL(b); - EXEC SQL SELECT amount INTO :h_amt FROM purchase WHERE purchase_id = :h_pid; - if (sqlca.sqlcode != 0) FAIL(b); - - diff = h_amt - h_inv; - if (diff != 0) { - EXEC SQL INSERT INTO rc_diff - (diff_id, purchase_id, expected_amt, actual_amt, diff_amt, diff_kind, biz_date) - VALUES (nextval('rc_diff_seq'), :h_pid, :h_amt, :h_inv, :h_amt - :h_inv, - 'AMT', :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_TAXRECON] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); } - mid = rcdb_insert_match(h_pid, "TAXINV", h_inv, - (diff == 0) ? "MATCHED" : "EXCEPTION", bizdate); - if (mid < 0) FAIL(b); - setl(b, T_DELTA, diff); - setl(b, T_ID1, mid); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_TAXRECON] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_TAXRECON] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_TAXRECON] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_TAXRECON] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_TAXRECON] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_TAXRECON] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_TAXRECON] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_TAXRECON] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_taxrecon CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_taxrecon; + for (;;) { + EXEC SQL FETCH cur_rc_taxrecon INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_taxrecon; userlog("[RC_TAXRECON] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_taxrecon; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_taxrecon; + userlog("[RC_TAXRECON] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_TAXRECON] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_TAXRECON] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_TAXRECON] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 차이 라인 적재 (rc_diff) */ + h_exp = h_net; h_act = h_led; h_diff = h_exp - h_act; + EXEC SQL SELECT nextval('rc_diff_seq') INTO :h_id; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL INSERT INTO rc_diff (diff_id, purchase_id, expected_amt, actual_amt, diff_amt, diff_kind, biz_date) + VALUES (:h_id, :h_pid, :h_exp, :h_act, :h_diff, :h_dtype, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_TAXRECON] 차이 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + out_cnt = (h_diff != 0 ? 1 : 0); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_TAXRECON] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_TAXRECON scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_TAXRECON] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_TAXRECON] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_TAXRECON] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_DELTA, h_diff); + setl(b, T_ID1, h_id); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_TAXRECON] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_THRESHOLD.pgc b/app/src/rc/svc/RC_THRESHOLD.pgc index 2ee1cb1..e352e76 100644 --- a/app/src/rc/svc/RC_THRESHOLD.pgc +++ b/app/src/rc/svc/RC_THRESHOLD.pgc @@ -1,36 +1,141 @@ /* - * RC_THRESHOLD.pgc - rc 모듈 서비스 RC_THRESHOLD (one service = one file). - * 임계초과 검출: 당일 차이 중 |차이금액| 이 임계값을 초과하는 건수를 세고, - * 존재하면 CRIT 경보를 발생시켜 재대사 트리거를 만든다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_THRESHOLD.pgc - rc 모듈 서비스 RC_THRESHOLD (대사 온라인 프로그램, 심화판). + * 업무: 임계초과 차이 검출 + CRIT 경보. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_THRESHOLD.h" +#include "acq_common.h" -/* 49. RC_THRESHOLD - 임계초과 차이 검출 + CRIT 경보. */ +/* RC_THRESHOLD - 임계초과 차이 검출 + CRIT 경보. */ void RC_THRESHOLD(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_limit, h_cnt; - char h_bizdate[16], h_msg[80]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; - h_limit = getl(b, T_AMT1); + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "THRESHOLD_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_THRESHOLD"); + snprintf(h_msg, sizeof(h_msg), "[RC_THRESHOLD] 임계초과 차이 검출 + CRIT 경보."); + strcpy(h_key, "-"); + userlog("[RC_THRESHOLD] 대사 진입: 임계초과 차이 검출 + CRIT 경보."); + + /* [1] 입력 검증부 -------------------------------------------------- */ 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 rc_diff - WHERE biz_date = :h_bizdate AND abs(diff_amt) > :h_limit; - if (sqlca.sqlcode < 0) FAIL(b); - - if (h_cnt > 0) { - snprintf(h_msg, sizeof(h_msg), "diff over threshold %ld: cnt=%ld", h_limit, h_cnt); - EXEC SQL INSERT INTO rc_alert (alert_id, purchase_id, lvl, message, biz_date) - VALUES (nextval('rc_alert_seq'), 0, 'CRIT', :h_msg, :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_THRESHOLD] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); } - setl(b, T_COUNT, h_cnt); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + h_n1 = getl(b, T_AMT1); if (h_n1 <= 0) h_n1 = 1000; /* 임계 기준금액 */ + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_THRESHOLD] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_THRESHOLD] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_THRESHOLD] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_threshold CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_threshold; + for (;;) { + EXEC SQL FETCH cur_rc_threshold INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_threshold; userlog("[RC_THRESHOLD] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_threshold; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_threshold; + userlog("[RC_THRESHOLD] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_THRESHOLD] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_THRESHOLD] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_THRESHOLD] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 경보 등록 (rc_alert) */ + if (unmatched_rows > 0 || h_ecnt > 0) strcpy(h_lvl, "WARN"); + if (unmatched_rows > 10 || h_ecnt > 20) strcpy(h_lvl, "CRIT"); + snprintf(sbuf, sizeof(sbuf), "임계초과 차이 검출 + CRIT 경보. unmatch=%ld exc=%ld", unmatched_rows, h_ecnt); + strncpy(h_msg, sbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL SELECT nextval('rc_alert_seq') INTO :h_id; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL INSERT INTO rc_alert (alert_id, purchase_id, lvl, message, biz_date) + VALUES (:h_id, :h_pid, :h_lvl, :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_THRESHOLD] 경보 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + out_cnt = h_ecnt; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_THRESHOLD] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_THRESHOLD scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_THRESHOLD] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_THRESHOLD] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_THRESHOLD] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_THRESHOLD] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_TIMEDIFF.pgc b/app/src/rc/svc/RC_TIMEDIFF.pgc index 7ada11f..6565455 100644 --- a/app/src/rc/svc/RC_TIMEDIFF.pgc +++ b/app/src/rc/svc/RC_TIMEDIFF.pgc @@ -1,28 +1,131 @@ /* - * RC_TIMEDIFF.pgc - rc 모듈 서비스 RC_TIMEDIFF (one service = one file). - * 기표시점 차이 분석: 매입 영업일과 원장 기표 영업일이 다른 이월/선기표 건수를 집계. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_TIMEDIFF.pgc - rc 모듈 서비스 RC_TIMEDIFF (대사 온라인 프로그램, 심화판). + * 업무: 기표시점 차이 분석: 매입 영업일과 원장 기표 영업일이 다른 이월/선기표 건수를 집계. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_TIMEDIFF.h" +#include "acq_common.h" -/* 97. RC_TIMEDIFF - 기표시점 차이 분석: 매입 영업일과 원장 기표 영업일이 다른 이월/선기표 건수를 집계. */ +/* RC_TIMEDIFF - 기표시점 차이 분석: 매입 영업일과 원장 기표 영업일이 다른 이월/선기표 건수를 집계. */ void RC_TIMEDIFF(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_cnt; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "TIMEDIFF_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_TIMEDIFF"); + snprintf(h_msg, sizeof(h_msg), "[RC_TIMEDIFF] 기표시점 차이 분석: 매입 영업일과 원장 기표 영업일이 다른 이월/선기표 건수를 집계."); + strcpy(h_key, "-"); + userlog("[RC_TIMEDIFF] 대사 진입: 기표시점 차이 분석: 매입 영업일과 원장 기표 영업일이 다른 이월/선기표 건수를 집계."); - EXEC SQL SELECT count(*) INTO :h_cnt FROM purchase p - JOIN ledger l ON l.purchase_id = p.purchase_id - WHERE p.biz_date = :h_bizdate AND l.biz_date <> p.biz_date; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_TIMEDIFF] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_TIMEDIFF] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_TIMEDIFF] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_TIMEDIFF] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_timediff CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_timediff; + for (;;) { + EXEC SQL FETCH cur_rc_timediff INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_timediff; userlog("[RC_TIMEDIFF] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_timediff; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_timediff; + userlog("[RC_TIMEDIFF] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_TIMEDIFF] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_TIMEDIFF] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_TIMEDIFF] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_TIMEDIFF] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_TIMEDIFF scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_TIMEDIFF] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_TIMEDIFF] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_TIMEDIFF] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_TIMEDIFF] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_TOLERANCE.pgc b/app/src/rc/svc/RC_TOLERANCE.pgc index ac52a7e..2517815 100644 --- a/app/src/rc/svc/RC_TOLERANCE.pgc +++ b/app/src/rc/svc/RC_TOLERANCE.pgc @@ -1,25 +1,156 @@ /* - * RC_TOLERANCE.pgc - rc 모듈 서비스 RC_TOLERANCE (one service = one file). - * Moved verbatim from the former inline rc_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by rc_svr at tpsvrinit. + * RC_TOLERANCE.pgc - rc 모듈 서비스 RC_TOLERANCE (대사 온라인 프로그램, 심화판). + * 업무: 허용오차 대사: |차이| <= 허용치면 MATCHED. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_TOLERANCE.h" +#include "acq_common.h" -/* 8. RC_TOLERANCE - 허용오차 대사: |차이| <= 허용치면 MATCHED. */ +/* RC_TOLERANCE - 허용오차 대사: |차이| <= 허용치면 MATCHED. */ void RC_TOLERANCE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long pid, tol, diff, adiff, mid; - pid = getl(b, T_PURCHASE_ID); - diff = getl(b, T_DELTA); - tol = getl(b, T_ARG1); if (tol <= 0) tol = 100; /* 기본 허용치 100원 */ + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; + EXEC SQL END DECLARE SECTION; + + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "TOLERANCE_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_TOLERANCE"); + snprintf(h_msg, sizeof(h_msg), "[RC_TOLERANCE] 허용오차 대사: |차이| <= 허용치면 MATCHED."); + strcpy(h_key, "-"); + userlog("[RC_TOLERANCE] 대사 진입: 허용오차 대사: |차이| <= 허용치면 MATCHED."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - adiff = diff < 0 ? -diff : diff; - mid = rcdb_insert_match(pid, "TOLERANCE", diff, - (adiff <= tol) ? "MATCHED" : "EXCEPTION", bizdate); - if (mid < 0) FAIL(b); - setl(b, T_RC, (adiff <= tol) ? 0 : 1); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_TOLERANCE] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_TOLERANCE] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + h_diff = getl(b, T_DELTA); + tol = getl(b, T_ARG1); if (tol <= 0) tol = 100; /* 기본 허용오차 100원 */ + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_TOLERANCE] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_TOLERANCE] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_TOLERANCE] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_TOLERANCE] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_TOLERANCE] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_TOLERANCE] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_TOLERANCE] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_tolerance CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_tolerance; + for (;;) { + EXEC SQL FETCH cur_rc_tolerance INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_tolerance; userlog("[RC_TOLERANCE] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_tolerance; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_tolerance; + userlog("[RC_TOLERANCE] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_TOLERANCE] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_TOLERANCE] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_TOLERANCE] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 대사 로그 적재 (rc_match_log) */ + h_id = rcdb_insert_match(h_pid, "TOLERANCE", h_net, + (unmatched_rows == 0 ? "MATCHED" : "EXCEPTION"), bizdate); + if (h_id < 0) { userlog("[RC_TOLERANCE] 대사 로그 적재 실패 pid=%ld", h_pid); FAIL(b); } + out_cnt = matched_rows > 0 ? matched_rows : 1; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_TOLERANCE] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_TOLERANCE scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_TOLERANCE] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_TOLERANCE] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_TOLERANCE] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_RC, rc_code); + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_TOLERANCE] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_TOPDIFF.pgc b/app/src/rc/svc/RC_TOPDIFF.pgc index ab55123..aa99ac2 100644 --- a/app/src/rc/svc/RC_TOPDIFF.pgc +++ b/app/src/rc/svc/RC_TOPDIFF.pgc @@ -1,35 +1,133 @@ /* - * RC_TOPDIFF.pgc - rc 모듈 서비스 RC_TOPDIFF (one service = one file). - * 차이 상위 조회: 당일 |차이금액| 의 최대값과 평균값을 구한 뒤 평균을 넘는 - * 이상치 차이 건수를 세어 상위 차이 분포를 요약한다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_TOPDIFF.pgc - rc 모듈 서비스 RC_TOPDIFF (대사 온라인 프로그램, 심화판). + * 업무: 최대/평균 차이 + 평균 초과 건수. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_TOPDIFF.h" +#include "acq_common.h" -/* 65. RC_TOPDIFF - 최대/평균 차이 + 평균 초과 건수. */ +/* RC_TOPDIFF - 최대/평균 차이 + 평균 초과 건수. */ void RC_TOPDIFF(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_max, h_avg, h_over; - char h_bizdate[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "TOPDIFF_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_TOPDIFF"); + snprintf(h_msg, sizeof(h_msg), "[RC_TOPDIFF] 최대/평균 차이 + 평균 초과 건수."); + strcpy(h_key, "-"); + userlog("[RC_TOPDIFF] 대사 진입: 최대/평균 차이 + 평균 초과 건수."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_TOPDIFF] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); - EXEC SQL SELECT coalesce(max(abs(diff_amt)),0), coalesce(round(avg(abs(diff_amt))),0) - INTO :h_max, :h_avg - FROM rc_diff WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_over FROM rc_diff - WHERE biz_date = :h_bizdate AND abs(diff_amt) > :h_avg; - if (sqlca.sqlcode < 0) FAIL(b); + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_TOPDIFF] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_TOPDIFF] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_TOPDIFF] 예외 집계 오류"); FAIL(b); } - setl(b, T_AMT1, h_max); - setl(b, T_AMT2, h_avg); - setl(b, T_COUNT, h_over); + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_topdiff CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_topdiff; + for (;;) { + EXEC SQL FETCH cur_rc_topdiff INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_topdiff; userlog("[RC_TOPDIFF] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_topdiff; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_topdiff; + userlog("[RC_TOPDIFF] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_TOPDIFF] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_TOPDIFF] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_TOPDIFF] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_TOPDIFF] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_TOPDIFF scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_TOPDIFF] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_TOPDIFF] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_TOPDIFF] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_AMT1, h_gross); + setl(b, T_AMT2, h_net); + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_TOPDIFF] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_TRAILQRY.pgc b/app/src/rc/svc/RC_TRAILQRY.pgc index 7acda82..8b00bf2 100644 --- a/app/src/rc/svc/RC_TRAILQRY.pgc +++ b/app/src/rc/svc/RC_TRAILQRY.pgc @@ -1,29 +1,153 @@ /* - * RC_TRAILQRY.pgc - rc 모듈 서비스 RC_TRAILQRY (one service = one file). - * 대사 감사추적 조회: 지정 매입의 전체 대사 로그 이력 건수와 최종 로그 ID 를 반환. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_TRAILQRY.pgc - rc 모듈 서비스 RC_TRAILQRY (대사 온라인 프로그램, 심화판). + * 업무: 대사 감사추적 조회: 지정 매입의 전체 대사 로그 이력 건수와 최종 로그 ID 를 반환. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_TRAILQRY.h" +#include "acq_common.h" -/* 133. RC_TRAILQRY - 대사 감사추적 조회: 지정 매입의 전체 대사 로그 이력 건수와 최종 로그 ID 를 반환. */ +/* RC_TRAILQRY - 대사 감사추적 조회: 지정 매입의 전체 대사 로그 이력 건수와 최종 로그 ID 를 반환. */ void RC_TRAILQRY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_pid, h_cnt, h_last = 0; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "TRAILQRY_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_TRAILQRY"); + snprintf(h_msg, sizeof(h_msg), "[RC_TRAILQRY] 대사 감사추적 조회: 지정 매입의 전체 대사 로그 이력 건수와 최종 로그 ID 를 반환."); + strcpy(h_key, "-"); + userlog("[RC_TRAILQRY] 대사 진입: 대사 감사추적 조회: 지정 매입의 전체 대사 로그 이력 건수와 최종 로그 ID 를 반환."); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_TRAILQRY] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); h_pid = getl(b, T_PURCHASE_ID); - EXEC SQL SELECT count(*), coalesce(max(match_id),0) INTO :h_cnt, :h_last - FROM rc_match_log WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); - setl(b, T_ID1, h_last); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_TRAILQRY] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_TRAILQRY] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_TRAILQRY] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_TRAILQRY] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_TRAILQRY] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_TRAILQRY] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_TRAILQRY] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_TRAILQRY] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_trailqry CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_trailqry; + for (;;) { + EXEC SQL FETCH cur_rc_trailqry INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_trailqry; userlog("[RC_TRAILQRY] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_trailqry; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_trailqry; + userlog("[RC_TRAILQRY] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_TRAILQRY] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_TRAILQRY] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_TRAILQRY] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_TRAILQRY] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_TRAILQRY scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_TRAILQRY] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_TRAILQRY] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_TRAILQRY] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + setl(b, T_ID1, h_id); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_TRAILQRY] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_UNDERAMT.pgc b/app/src/rc/svc/RC_UNDERAMT.pgc index b662d28..9cac2a6 100644 --- a/app/src/rc/svc/RC_UNDERAMT.pgc +++ b/app/src/rc/svc/RC_UNDERAMT.pgc @@ -1,28 +1,132 @@ /* - * RC_UNDERAMT.pgc - rc 모듈 서비스 RC_UNDERAMT (one service = one file). - * 과소 차이 분석: 당일 rc_diff 중 실제가 기대에 미달(diff_amt>0)한 과소 건수/합계. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_UNDERAMT.pgc - rc 모듈 서비스 RC_UNDERAMT (대사 온라인 프로그램, 심화판). + * 업무: 과소 차이 분석: 당일 rc_diff 중 실제가 기대에 미달(diff_amt>0)한 과소 건수/합계. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_UNDERAMT.h" +#include "acq_common.h" -/* 95. RC_UNDERAMT - 과소 차이 분석: 당일 rc_diff 중 실제가 기대에 미달(diff_amt>0)한 과소 건수/합계. */ +/* RC_UNDERAMT - 과소 차이 분석: 당일 rc_diff 중 실제가 기대에 미달(diff_amt>0)한 과소 건수/합계. */ void RC_UNDERAMT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; - long h_cnt, h_sum; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "UNDERAMT_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_UNDERAMT"); + snprintf(h_msg, sizeof(h_msg), "[RC_UNDERAMT] 과소 차이 분석: 당일 rc_diff 중 실제가 기대에 미달(diff_amt>0)한 과소 건수/합계."); + strcpy(h_key, "-"); + userlog("[RC_UNDERAMT] 대사 진입: 과소 차이 분석: 당일 rc_diff 중 실제가 기대에 미달(diff_amt>0)한 과소 건수/합계."); - EXEC SQL SELECT count(*), coalesce(sum(diff_amt),0) INTO :h_cnt, :h_sum - FROM rc_diff WHERE biz_date = :h_bizdate AND diff_amt > 0; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); - setl(b, T_DELTA, h_sum); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_UNDERAMT] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_UNDERAMT] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_UNDERAMT] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_UNDERAMT] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_underamt CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_underamt; + for (;;) { + EXEC SQL FETCH cur_rc_underamt INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_underamt; userlog("[RC_UNDERAMT] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_underamt; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_underamt; + userlog("[RC_UNDERAMT] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_UNDERAMT] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_UNDERAMT] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_UNDERAMT] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 조회 전용 서비스: 본처리는 감사 로그 적재로 대신 (아래 [6]) */ + out_cnt = (scan_rows > 0 ? matched_rows : h_mcnt); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_UNDERAMT] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_UNDERAMT scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_UNDERAMT] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_UNDERAMT] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_UNDERAMT] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + setl(b, T_DELTA, h_diff); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_UNDERAMT] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_UNMATCH.pgc b/app/src/rc/svc/RC_UNMATCH.pgc index d7f849e..94e0fb0 100644 --- a/app/src/rc/svc/RC_UNMATCH.pgc +++ b/app/src/rc/svc/RC_UNMATCH.pgc @@ -1,26 +1,135 @@ /* - * RC_UNMATCH.pgc - rc 모듈 서비스 RC_UNMATCH (one service = one file). - * Moved verbatim from the former inline rc_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by rc_svr at tpsvrinit. + * RC_UNMATCH.pgc - rc 모듈 서비스 RC_UNMATCH (대사 온라인 프로그램, 심화판). + * 업무: 미대사 건수 조회. (KEEP) + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_UNMATCH.h" +#include "acq_common.h" -/* 11. RC_UNMATCH - 미대사 건수 조회. (KEEP) */ +/* RC_UNMATCH - 미대사 건수 조회. (KEEP) */ void RC_UNMATCH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_cnt; - char h_bizdate[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; + + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "UNMATCH_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_UNMATCH"); + snprintf(h_msg, sizeof(h_msg), "[RC_UNMATCH] 미대사 건수 조회. (KEEP)"); + strcpy(h_key, "-"); + userlog("[RC_UNMATCH] 대사 진입: 미대사 건수 조회. (KEEP)"); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_UNMATCH] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } 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 + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_UNMATCH] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_UNMATCH] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_UNMATCH] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_unmatch CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_unmatch; + for (;;) { + EXEC SQL FETCH cur_rc_unmatch INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_unmatch; userlog("[RC_UNMATCH] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_unmatch; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_unmatch; + userlog("[RC_UNMATCH] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_UNMATCH] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_UNMATCH] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_UNMATCH] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 보존: 미대사(원장 부재) 매입 건수 */ + EXEC SQL SELECT count(*) INTO :h_ucnt 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); } + if (sqlca.sqlcode < 0) { userlog("[RC_UNMATCH] 미대사 집계 실패"); FAIL(b); } + out_cnt = h_ucnt; + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_UNMATCH] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_UNMATCH scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_UNMATCH] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_UNMATCH] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_UNMATCH] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_UNMATCH] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_VOIDREC.pgc b/app/src/rc/svc/RC_VOIDREC.pgc index d61bb1e..671971a 100644 --- a/app/src/rc/svc/RC_VOIDREC.pgc +++ b/app/src/rc/svc/RC_VOIDREC.pgc @@ -1,38 +1,158 @@ /* - * RC_VOIDREC.pgc - rc 모듈 서비스 RC_VOIDREC (one service = one file). - * VOID 매입 대사: 매입 상태가 V(void) 인지와 VOID 유형 취소 증빙 존재를 - * 함께 검증하여 양쪽 모두 있으면 MATCHED, 아니면 EXCEPTION + 예외 적재. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_VOIDREC.pgc - rc 모듈 서비스 RC_VOIDREC (대사 온라인 프로그램, 심화판). + * 업무: VOID 매입 대사 (상태 V + VOID 취소 증빙). + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_VOIDREC.h" +#include "acq_common.h" -/* 52. RC_VOIDREC - VOID 매입 대사 (상태 V + VOID 취소 증빙). */ +/* RC_VOIDREC - VOID 매입 대사 (상태 V + VOID 취소 증빙). */ void RC_VOIDREC(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long mid, ok; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_void, h_evi; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "VOIDREC_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_VOIDREC"); + snprintf(h_msg, sizeof(h_msg), "[RC_VOIDREC] VOID 매입 대사 (상태 V + VOID 취소 증빙)."); + strcpy(h_key, "-"); + userlog("[RC_VOIDREC] 대사 진입: VOID 매입 대사 (상태 V + VOID 취소 증빙)."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_VOIDREC] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_VOIDREC] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } - EXEC SQL SELECT count(*) INTO :h_void FROM purchase - WHERE purchase_id = :h_pid AND status = 'V'; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_evi FROM ac_cancel - WHERE purchase_id = :h_pid AND cancel_type = 'VOID'; - if (sqlca.sqlcode < 0) FAIL(b); + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_VOIDREC] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_VOIDREC] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_VOIDREC] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_VOIDREC] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_VOIDREC] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_VOIDREC] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_VOIDREC] 예외 집계 오류"); FAIL(b); } - ok = (h_void > 0 && h_evi > 0); - mid = rcdb_insert_match(h_pid, "VOID", 0, ok ? "MATCHED" : "EXCEPTION", bizdate); - if (mid < 0) FAIL(b); - if (!ok && rcdb_insert_exception(h_pid, "VOID EVIDENCE MISSING", bizdate) < 0) - FAIL(b); - setl(b, T_ID1, mid); - setl(b, T_RC, ok ? 0 : 1); + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_voidrec CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_voidrec; + for (;;) { + EXEC SQL FETCH cur_rc_voidrec INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_voidrec; userlog("[RC_VOIDREC] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_voidrec; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_voidrec; + userlog("[RC_VOIDREC] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_VOIDREC] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_VOIDREC] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_VOIDREC] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 차이 라인 적재 (rc_diff) */ + h_exp = h_net; h_act = h_led; h_diff = h_exp - h_act; + EXEC SQL SELECT nextval('rc_diff_seq') INTO :h_id; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL INSERT INTO rc_diff (diff_id, purchase_id, expected_amt, actual_amt, diff_amt, diff_kind, biz_date) + VALUES (:h_id, :h_pid, :h_exp, :h_act, :h_diff, :h_dtype, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_VOIDREC] 차이 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + out_cnt = (h_diff != 0 ? 1 : 0); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_VOIDREC] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_VOIDREC scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_VOIDREC] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_VOIDREC] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_VOIDREC] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_ID1, h_id); + setl(b, T_RC, rc_code); + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_VOIDREC] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_WHTRECON.pgc b/app/src/rc/svc/RC_WHTRECON.pgc index e9fa28b..ffbfd9e 100644 --- a/app/src/rc/svc/RC_WHTRECON.pgc +++ b/app/src/rc/svc/RC_WHTRECON.pgc @@ -1,37 +1,160 @@ /* - * RC_WHTRECON.pgc - rc 모듈 서비스 RC_WHTRECON (one service = one file). - * 원천세 대사: 매입의 원천징수(ac_wht) 세액 합계가 정산 net 금액을 넘지 - * 않는지 검증하고 초과 시 EXCEPTION, 정상 범위면 MATCHED 로 기록한다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_WHTRECON.pgc - rc 모듈 서비스 RC_WHTRECON (대사 온라인 프로그램, 심화판). + * 업무: 원천세 한도 대사. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_WHTRECON.h" +#include "acq_common.h" -/* 59. RC_WHTRECON - 원천세 한도 대사. */ +/* RC_WHTRECON - 원천세 한도 대사. */ void RC_WHTRECON(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long mid, over; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_tax, h_net; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "WHTRECON_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_WHTRECON"); + snprintf(h_msg, sizeof(h_msg), "[RC_WHTRECON] 원천세 한도 대사."); + strcpy(h_key, "-"); + userlog("[RC_WHTRECON] 대사 진입: 원천세 한도 대사."); + + /* [1] 입력 검증부 -------------------------------------------------- */ gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_WHTRECON] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RC_WHTRECON] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } - EXEC SQL SELECT coalesce(sum(tax_amount),0) INTO :h_tax - FROM ac_wht WHERE purchase_id = :h_pid; + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L); + userlog("[RC_WHTRECON] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b); + } else if (sqlca.sqlcode < 0) { + userlog("[RC_WHTRECON] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RC_WHTRECON] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RC_WHTRECON] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_WHTRECON] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_WHTRECON] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_WHTRECON] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_whtrecon CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_whtrecon; + for (;;) { + EXEC SQL FETCH cur_rc_whtrecon INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_whtrecon; userlog("[RC_WHTRECON] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_whtrecon; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_whtrecon; + userlog("[RC_WHTRECON] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_WHTRECON] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_WHTRECON] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_WHTRECON] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 차이 라인 적재 (rc_diff) */ + h_exp = h_net; h_act = h_led; h_diff = h_exp - h_act; + EXEC SQL SELECT nextval('rc_diff_seq') INTO :h_id; if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT net INTO :h_net FROM purchase WHERE purchase_id = :h_pid; - if (sqlca.sqlcode != 0) FAIL(b); + EXEC SQL INSERT INTO rc_diff (diff_id, purchase_id, expected_amt, actual_amt, diff_amt, diff_kind, biz_date) + VALUES (:h_id, :h_pid, :h_exp, :h_act, :h_diff, :h_dtype, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_WHTRECON] 차이 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + out_cnt = (h_diff != 0 ? 1 : 0); - over = (h_tax > h_net); - mid = rcdb_insert_match(h_pid, "WHT", h_tax, over ? "EXCEPTION" : "MATCHED", bizdate); - if (mid < 0) FAIL(b); - setl(b, T_TAX, h_tax); + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_WHTRECON] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_WHTRECON scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_WHTRECON] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_WHTRECON] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_WHTRECON] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_ID1, h_id); setl(b, T_NET, h_net); - setl(b, T_RC, over ? 1 : 0); - setl(b, T_ID1, mid); + setl(b, T_RC, rc_code); + setl(b, T_TAX, h_val); + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_WHTRECON] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RC_ZEROAMT.pgc b/app/src/rc/svc/RC_ZEROAMT.pgc index 7285dce..0ef5c82 100644 --- a/app/src/rc/svc/RC_ZEROAMT.pgc +++ b/app/src/rc/svc/RC_ZEROAMT.pgc @@ -1,29 +1,135 @@ /* - * RC_ZEROAMT.pgc - rc 모듈 서비스 RC_ZEROAMT (one service = one file). - * 이상금액 검출: 당일 매입 중 금액 0원 또는 음수 net 인 이상 거래를 찾아 - * 일괄 INSERT..SELECT 로 대사 예외에 적재하고 검출 건수를 반환한다. - * Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by rc_svr. + * RC_ZEROAMT.pgc - rc 모듈 서비스 RC_ZEROAMT (대사 온라인 프로그램, 심화판). + * 업무: 0원/음수 net 이상 거래 예외 적재. + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RC_ZEROAMT.h" +#include "acq_common.h" -/* 66. RC_ZEROAMT - 0원/음수 net 이상 거래 예외 적재. */ +/* RC_ZEROAMT - 0원/음수 net 이상 거래 예외 적재. */ void RC_ZEROAMT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "ZEROAMT_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RC_ZEROAMT"); + snprintf(h_msg, sizeof(h_msg), "[RC_ZEROAMT] 0원/음수 net 이상 거래 예외 적재."); + strcpy(h_key, "-"); + userlog("[RC_ZEROAMT] 대사 진입: 0원/음수 net 이상 거래 예외 적재."); - EXEC SQL INSERT INTO rc_exception (exc_id, purchase_id, reason, biz_date) - SELECT nextval('rc_exc_seq'), purchase_id, 'ZERO OR NEGATIVE AMT', biz_date - FROM purchase - WHERE biz_date = :h_bizdate AND (amount = 0 OR net < 0); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, sqlca.sqlerrd[2]); + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RC_ZEROAMT] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_ZEROAMT] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RC_ZEROAMT] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RC_ZEROAMT] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_rc_zeroamt CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_rc_zeroamt; + for (;;) { + EXEC SQL FETCH cur_rc_zeroamt INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_zeroamt; userlog("[RC_ZEROAMT] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_zeroamt; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_rc_zeroamt; + userlog("[RC_ZEROAMT] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RC_ZEROAMT] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RC_ZEROAMT] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RC_ZEROAMT] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 예외 적재 (rc_exception) */ + if (h_ecnt >= 0) { + h_id = rcdb_insert_exception(h_pid, h_reason, bizdate); + if (h_id < 0) { userlog("[RC_ZEROAMT] 예외 적재 실패 pid=%ld", h_pid); FAIL(b); } + } + out_cnt = h_ecnt + (h_pid > 0 ? 1 : 0); + + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RC_ZEROAMT] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RC_ZEROAMT scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RC_ZEROAMT] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RC_ZEROAMT] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RC_ZEROAMT] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + setl(b, T_COUNT, out_cnt); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RC_ZEROAMT] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/rc/svc/RECONCILE.pgc b/app/src/rc/svc/RECONCILE.pgc index eb10c66..b3f534d 100644 --- a/app/src/rc/svc/RECONCILE.pgc +++ b/app/src/rc/svc/RECONCILE.pgc @@ -1,40 +1,176 @@ /* - * RECONCILE.pgc - rc 모듈 서비스 RECONCILE (one service = one file). - * Moved verbatim from the former inline rc_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by rc_svr at tpsvrinit. + * RECONCILE.pgc - rc 모듈 서비스 RECONCILE (대사 온라인 프로그램, 심화판). + * 업무: RECONCILE 대사 처리 + * 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답. + * 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고. */ #include "RECONCILE.h" +#include "acq_common.h" -/* ========================================================================= */ +/* RECONCILE - RECONCILE 대사 처리 */ void RECONCILE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long rlen = 0; + char bizdate[16], tmpin[64]; + char sbuf[300]; + long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0; + long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0; + long rlen = 0; EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_net; - char h_bizdate[16]; + long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt; + long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr; + long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff; + char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16]; + char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160]; + char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8]; 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; + /* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */ + h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0; + h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0; + h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0; + bizdate[0] = tmpin[0] = 0; + h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0; + strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW"); + strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN"); + strcpy(h_reason, "RECONCILE_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT"); + strcpy(h_svc, "RECONCILE"); + snprintf(h_msg, sizeof(h_msg), "[RECONCILE] RECONCILE 대사 처리"); + strcpy(h_key, "-"); + userlog("[RECONCILE] 대사 진입: RECONCILE 대사 처리"); - /* own-branch write: recon ledger evidence only */ + /* [1] 입력 검증부 -------------------------------------------------- */ + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (strlen(bizdate) < 8) { + setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L); + userlog("[RECONCILE] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b); + } + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strcpy(h_key, bizdate); + h_pid = getl(b, T_PURCHASE_ID); + if (h_pid <= 0) { + setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L); + userlog("[RECONCILE] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b); + } + h_net = getl(b, T_NET); + if (h_net < -100000000000L || h_net > 100000000000L) { + setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "순액 범위 초과", 0L); + userlog("[RECONCILE] 순액 범위 초과 net=%ld", h_net); FAIL(b); + } + + /* [2] 마스터/정책/상태 조회부 ------------------------------------- + * XA 브랜치 격리 주의: ACQUIRE→RECONCILE 체인에서 매입 원장은 호출측(ac_svr) + * 브랜치에 아직 미커밋 상태라 이 브랜치에서는 보이지 않는다(owner-writes 규율). + * 따라서 미존재(100)는 정상 흐름으로 간주하고 요청 전문(UBF) 값을 사용한다. + * 실패 처리하면 체인 전체가 롤백되므로 여기서 FAIL 하지 않는다. */ + EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode == 100) { + /* 체인 내 신규 매입: 전문에 실린 값으로 대사 진행 */ + h_amt = getl(b, T_AMOUNT); + h_fee = getl(b, T_FEE); + h_net = getl(b, T_NET); + gets_(b, T_MERCHANT, h_merch, sizeof(h_merch)); + gets_(b, T_BIZDATE, h_bizdate, sizeof(h_bizdate)); + strncpy(h_pstat, "A", sizeof(h_pstat) - 1); + userlog("[RECONCILE] 체인 신규매입(미커밋) pid=%ld — 전문값으로 대사 진행", h_pid); + } else if (sqlca.sqlcode < 0) { + userlog("[RECONCILE] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); + } + strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0; + EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status + FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { + strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000; + userlog("[RECONCILE] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch); + } else if (sqlca.sqlcode < 0) { userlog("[RECONCILE] 가맹점 조회 오류"); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0) + INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RECONCILE] 당일 매입 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log + WHERE biz_date = :h_bizdate AND status = 'MATCHED'; + if (sqlca.sqlcode < 0) { userlog("[RECONCILE] 대사 로그 집계 오류"); FAIL(b); } + EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception + WHERE biz_date = :h_bizdate AND resolved = false; + if (sqlca.sqlcode < 0) { userlog("[RECONCILE] 예외 집계 오류"); FAIL(b); } + + /* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */ + EXEC SQL DECLARE cur_reconcile CURSOR FOR + SELECT purchase_id, net, amount, fee, status + FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id; + EXEC SQL OPEN cur_reconcile; + for (;;) { + EXEC SQL FETCH cur_reconcile INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_reconcile; userlog("[RECONCILE] 커서 FETCH 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger + WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE'); + if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_reconcile; FAIL(b); } + h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched_rows++; + else { unmatched_rows++; unmatched_amt += adiff; } + } + EXEC SQL CLOSE cur_reconcile; + userlog("[RECONCILE] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows); + + /* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0; + userlog("[RECONCILE] 대상 없음 - 무처리 종료"); + } else if (unmatched_rows == 0) { + rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows; + } else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows; + userlog("[RECONCILE] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows); + } else { + rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows; + userlog("[RECONCILE] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt); + } + if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */ + h_exp = h_net; h_diff = h_exp - h_act; + + /* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */ + /* 보존: own-branch RECON 원장 적재 후 SETTLE 체인 호출 */ 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); - } + if (sqlca.sqlcode < 0) { userlog("[RECONCILE] RECON 원장 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } Bchg(b, T_STATUS, 0, "M", 0L); - userlog("RECONCILE pid=%ld 대사완료 -> SETTLE", h_pid); + /* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */ + EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2; + if (sqlca.sqlcode < 0) { userlog("[RECONCILE] 로그 시퀀스 실패"); FAIL(b); } + snprintf(sbuf, sizeof(sbuf), "RECONCILE scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld", + scan_rows, matched_rows, unmatched_rows, h_gross, rc_code); + strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I")); + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("[RECONCILE] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); } + + /* [7] 집계/요약 재집계부 ------------------------------------------ */ + EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p + WHERE p.biz_date = :h_bizdate + AND p.status IN ('A','H'); + if (sqlca.sqlcode < 0) { userlog("[RECONCILE] 미결 재집계 오류"); FAIL(b); } + EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { userlog("[RECONCILE] 정산 재집계 오류"); FAIL(b); } + if (h_val == 0) h_val = h_set; + if (h_n1 == 0) h_n1 = matched_rows; + if (h_n2 == 0) h_n2 = unmatched_rows; + + /* [8] 응답부 (출력 필드 설정) ------------------------------------- */ + Bchg(b, T_STATUS, 0, h_status, 0L); + /* T_RC 미계약 서비스: 결과코드는 감사로그로만 기록 */ + + /* 보존: 대사완료 후 SETTLE 서비스 체인 호출 (동일 XA 브랜치) */ + userlog("[RECONCILE] pid=%ld 대사완료 -> SETTLE 호출", h_pid); if (tpcall("SETTLE", (char *)b, 0L, (char **)&b, &rlen, 0L) < 0) { - userlog("RECONCILE: tpcall(SETTLE) FAIL: %s", tpstrerror(tperrno)); - FAIL(b); + userlog("[RECONCILE] tpcall(SETTLE) 실패: %s", tpstrerror(tperrno)); FAIL(b); } + /* [9] 완료 로깅 --------------------------------------------------- */ + userlog("[RECONCILE] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status); OK(b); } diff --git a/app/src/st/svc/ST_ADJHIST.pgc b/app/src/st/svc/ST_ADJHIST.pgc index 125eb19..d0e496f 100644 --- a/app/src/st/svc/ST_ADJHIST.pgc +++ b/app/src/st/svc/ST_ADJHIST.pgc @@ -1,38 +1,137 @@ /* - * ST_ADJHIST.pgc - st 모듈 서비스 ST_ADJHIST (one service = one file). - * 조정 이력 조회: 매입에 걸린 정산 조정을 최신순 커서로 순회해 건수·증감 - * 누계와 가장 최근 조정 사유를 회신한다. 이력 없으면 RC=1. 조회 전용. + * ST_ADJHIST.pgc - st (정산/수수료) 모듈 서비스 ST_ADJHIST (one service = one file). + * 매입별 조정 이력 요약. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_ADJHIST.h" -/* 54. ST_ADJHIST - 매입별 조정 이력 요약. */ +/* ST_ADJHIST - 매입별 조정 이력 요약. (심화판). (심화판) */ void ST_ADJHIST(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char last[128] = ""; - long cnt = 0, sum = 0; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_delta; - char h_reason[128]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); - EXEC SQL DECLARE adjhist_c CURSOR FOR - SELECT delta, reason FROM st_adjust WHERE purchase_id = :h_pid ORDER BY adj_id DESC; - EXEC SQL OPEN adjhist_c; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH adjhist_c INTO :h_delta, :h_reason; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE adjhist_c; FAIL(b); } - if (cnt == 0) { strncpy(last, h_reason, sizeof(last)-1); last[sizeof(last)-1] = 0; } - cnt++; - sum += h_delta; + + strcpy(h_svc, "ST_ADJHIST"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_ADJHIST"); + userlog("[정산심화] 매입별 조정 이력 요약. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } } - EXEC SQL CLOSE adjhist_c; - if (cnt == 0) { setl(b, T_RC, 1); OK(b); } - setl(b, T_COUNT, cnt); - setl(b, T_DELTA, sum); - Bchg(b, T_REASON, 0, last, 0L); + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 매입별 조정 이력 요약. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "매입별 조정 이력 요약. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "매입별 조정 이력 요약. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "매입별 조정 이력 요약. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "매입별 조정 이력 요약. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "매입별 조정 이력 요약. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "매입별 조정 이력 요약. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 매입별 조정 이력 요약. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 매입별 조정 이력 요약. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(delta),0), coalesce(max(delta),0), coalesce(min(delta),0) + INTO :h_cnt, :h_delta, :h_gmax, :h_gmin FROM st_adjust + WHERE (:h_pid = 0 OR purchase_id = :h_pid) AND biz_date >= :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 57); Bchg(b, T_MSG, 0, "조정 이력 집계 오류", 0L); FAIL(b); } + h_netsum = h_delta; setl(b, T_DELTA, h_delta); setl(b, T_AMT1, h_gmax); setl(b, T_AMT2, h_gmin); + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_netsum); + userlog("[정산심화] 매입별 조정 이력 요약. (심화판) cnt=%ld sum=%ld", h_cnt, h_netsum); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "매입별 조정 이력 요약. (심화판) [ST_ADJHIST] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 매입별 조정 이력 요약. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_ADJHIST", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 매입별 조정 이력 요약. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_ADJREVERSE.pgc b/app/src/st/svc/ST_ADJREVERSE.pgc index dbab2fc..271c443 100644 --- a/app/src/st/svc/ST_ADJREVERSE.pgc +++ b/app/src/st/svc/ST_ADJREVERSE.pgc @@ -1,29 +1,135 @@ /* - * ST_ADJREVERSE.pgc - st 모듈 서비스 ST_ADJREVERSE (one service = one file). - * 조정 취소(역분개): 기존 정산 조정(T_ADJ) 행을 조회해 동일 금액의 반대 부호 조정을 - * 새로 기록함으로써 앞선 조정을 상쇄한다. + * ST_ADJREVERSE.pgc - st (정산/수수료) 모듈 서비스 ST_ADJREVERSE (one service = one file). + * 기존 조정을 반대부호로 상쇄. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_ADJREVERSE.h" -/* 103. ST_ADJREVERSE - 기존 조정을 반대부호로 상쇄. */ +/* ST_ADJREVERSE - 기존 조정을 반대부호로 상쇄. (심화판). (심화판) */ void ST_ADJREVERSE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long newadj; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_adj, h_delta, h_sid, h_pid; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; - h_adj = getl(b, T_ADJ); + + strcpy(h_svc, "ST_ADJREVERSE"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - EXEC SQL SELECT delta, settlement_id, purchase_id INTO :h_delta, :h_sid, :h_pid - FROM st_adjust WHERE adj_id = :h_adj; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlcode == 100) { setl(b, T_RC, 1); OK(b); } - newadj = stdb_insert_adjust(h_sid, h_pid, -h_delta, "REVERSE", bizdate); - if (newadj < 0) FAIL(b); - setl(b, T_ADJ, newadj); - setl(b, T_DELTA, -h_delta); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_ADJREVERSE"); + userlog("[정산심화] 기존 조정을 반대부호로 상쇄. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 기존 조정을 반대부호로 상쇄. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "기존 조정을 반대부호로 상쇄. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "기존 조정을 반대부호로 상쇄. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "기존 조정을 반대부호로 상쇄. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "기존 조정을 반대부호로 상쇄. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "기존 조정을 반대부호로 상쇄. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "기존 조정을 반대부호로 상쇄. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 기존 조정을 반대부호로 상쇄. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 기존 조정을 반대부호로 상쇄. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_delta = -(delta0>0?delta0:net0/10); + userlog("[정산심화] 기존 조정을 반대부호로 상쇄. (심화판): 채권 상계/차감(음수 조정) delta=%ld", h_delta, h_bps); + h_id = stdb_insert_adjust(h_sid, h_pid, h_delta, "SETTLE_OFFSET", bizdate); + if (h_id < 0) { setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "정산 조정(st_adjust) 기록 실패", 0L); FAIL(b); } + setl(b, T_ADJ, h_id); setl(b, T_DELTA, h_delta); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "기존 조정을 반대부호로 상쇄. (심화판) [ST_ADJREVERSE] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 기존 조정을 반대부호로 상쇄. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_ADJREVERSE", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 기존 조정을 반대부호로 상쇄. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_ADJUST.pgc b/app/src/st/svc/ST_ADJUST.pgc index a59b8a1..fc6a776 100644 --- a/app/src/st/svc/ST_ADJUST.pgc +++ b/app/src/st/svc/ST_ADJUST.pgc @@ -1,22 +1,142 @@ /* - * ST_ADJUST.pgc - st 모듈 서비스 ST_ADJUST (one service = one file). - * Moved verbatim from the former inline st_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by st_svr at tpsvrinit. + * ST_ADJUST.pgc - st (정산/수수료) 모듈 서비스 ST_ADJUST (one service = one file). + * 정산 조정 (가감) 기록. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_ADJUST.h" -/* 8. ST_ADJUST - 정산 조정 (가감) 기록 (dbio). */ +/* ST_ADJUST - 정산 조정 (가감) 기록. (심화판). (심화판) */ void ST_ADJUST(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], reason[128]; - long sid = getl(b, T_SETTLE_ID), pid = getl(b, T_PURCHASE_ID); - long delta = getl(b, T_DELTA), adj; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_ADJUST"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "ADJ"); - adj = stdb_insert_adjust(sid, pid, delta, reason, bizdate); - if (adj < 0) FAIL(b); - setl(b, T_ADJ, adj); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_ADJUST"); + userlog("[정산심화] 정산 조정 (가감) 기록. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (h_pid <= 0) { setl(b, T_RC, 15); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 누락", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 정산 조정 (가감) 기록. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "정산 조정 (가감) 기록. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "정산 조정 (가감) 기록. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "정산 조정 (가감) 기록. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "정산 조정 (가감) 기록. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "정산 조정 (가감) 기록. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "정산 조정 (가감) 기록. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 정산 조정 (가감) 기록. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 정산 조정 (가감) 기록. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_delta = delta0; + userlog("[정산심화] 정산 조정 (가감) 기록. (심화판): 정산 가감 조정 delta=%ld", h_delta, h_bps); + h_id = stdb_insert_adjust(h_sid, h_pid, h_delta, "ADJ", bizdate); + if (h_id < 0) { setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "정산 조정(st_adjust) 기록 실패", 0L); FAIL(b); } + setl(b, T_ADJ, h_id); setl(b, T_DELTA, h_delta); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "정산 조정 (가감) 기록. (심화판) [ST_ADJUST] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 정산 조정 (가감) 기록. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_ADJUST", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 정산 조정 (가감) 기록. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_ADVANCE.pgc b/app/src/st/svc/ST_ADVANCE.pgc index aa1d961..d86b5c2 100644 --- a/app/src/st/svc/ST_ADVANCE.pgc +++ b/app/src/st/svc/ST_ADVANCE.pgc @@ -1,23 +1,143 @@ /* - * ST_ADVANCE.pgc - st 모듈 서비스 ST_ADVANCE (one service = one file). - * Moved verbatim from the former inline st_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by st_svr at tpsvrinit. + * ST_ADVANCE.pgc - st (정산/수수료) 모듈 서비스 ST_ADVANCE (one service = one file). + * 선지급 예약: 지급 레코드(SCHEDULED) 생성. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_ADVANCE.h" -/* 9. ST_ADVANCE - 선지급 예약: 지급 레코드(SCHEDULED) 생성 (dbio). */ +/* ST_ADVANCE - 선지급 예약: 지급 레코드(SCHEDULED) 생성. (심화판). (심화판) */ void ST_ADVANCE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16], paydate[16]; - long net = getl(b, T_NET), pay; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_ADVANCE"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - gets_(b, T_KEY1, paydate, sizeof(paydate)); - if (paydate[0] == 0) { strncpy(paydate, bizdate, sizeof(paydate)-1); paydate[sizeof(paydate)-1] = 0; } - pay = stdb_insert_payment(merch, net, paydate, "SCHEDULED", bizdate); - if (pay < 0) FAIL(b); - setl(b, T_ID1, pay); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_ADVANCE"); + userlog("[정산심화] 선지급 예약: 지급 레코드(SCHEDULED) 생성. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 선지급 예약: 지급 레코드(SCHEDULED) 생성. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "선지급 예약: 지급 레코드(SCHEDULED) 생성. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "선지급 예약: 지급 레코드(SCHEDULED) 생성. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "선지급 예약: 지급 레코드(SCHEDULED) 생성. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "선지급 예약: 지급 레코드(SCHEDULED) 생성. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "선지급 예약: 지급 레코드(SCHEDULED) 생성. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "선지급 예약: 지급 레코드(SCHEDULED) 생성. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 선지급 예약: 지급 레코드(SCHEDULED) 생성. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 선지급 예약: 지급 레코드(SCHEDULED) 생성. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (net0 <= 0 && h_prior == 1) { setl(b, T_RC, 32); Bchg(b, T_MSG, 0, "지급 대상 금액 없음", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + if (net0 <= 0) net0 = 1; + h_id = stdb_insert_payment(merch, net0, bizdate, "SCHEDULED", bizdate); + if (h_id < 0) { setl(b, T_RC, 44); Bchg(b, T_MSG, 0, "지급 레코드 등록 실패", 0L); FAIL(b); } + userlog("[정산심화] 선지급 예약: 지급 레코드(SCHEDULED) 생성. (심화판): 선지급 예약 pay_id=%ld net=%ld", h_id, net0); + setl(b, T_ID1, h_id); Bchg(b, T_STATUS, 0, "S", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "선지급 예약: 지급 레코드(SCHEDULED) 생성. (심화판) [ST_ADVANCE] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 선지급 예약: 지급 레코드(SCHEDULED) 생성. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_ADVANCE", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 선지급 예약: 지급 레코드(SCHEDULED) 생성. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_APPEALSTAT.pgc b/app/src/st/svc/ST_APPEALSTAT.pgc index 7225092..c3304cf 100644 --- a/app/src/st/svc/ST_APPEALSTAT.pgc +++ b/app/src/st/svc/ST_APPEALSTAT.pgc @@ -1,27 +1,137 @@ /* - * ST_APPEALSTAT.pgc - st 모듈 서비스 ST_APPEALSTAT (one service = one file). - * 이의신청 통계: 정산 조정 중 이의신청 관련(reason LIKE 'DISPUTE%') 건수와 청구 - * 금액 합계를 집계해 이의신청 규모를 산출한다 (조회 전용). + * ST_APPEALSTAT.pgc - st (정산/수수료) 모듈 서비스 ST_APPEALSTAT (one service = one file). + * 이의신청 조정 건수/금액 통계. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_APPEALSTAT.h" -/* 129. ST_APPEALSTAT - 이의신청 조정 건수/금액 통계. */ +/* ST_APPEALSTAT - 이의신청 조정 건수/금액 통계. (심화판). (심화판) */ void ST_APPEALSTAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_sum; - char h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_APPEALSTAT"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_APPEALSTAT"); + userlog("[정산심화] 이의신청 조정 건수/금액 통계. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT count(*), coalesce(sum(delta),0) INTO :h_cnt, :h_sum - FROM st_adjust WHERE biz_date = :h_bizdate AND reason LIKE 'DISPUTE%'; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); - setl(b, T_DELTA, h_sum); - setl(b, T_RC, (h_cnt > 0) ? 0 : 1); + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 이의신청 조정 건수/금액 통계. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "이의신청 조정 건수/금액 통계. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "이의신청 조정 건수/금액 통계. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "이의신청 조정 건수/금액 통계. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "이의신청 조정 건수/금액 통계. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "이의신청 조정 건수/금액 통계. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "이의신청 조정 건수/금액 통계. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 이의신청 조정 건수/금액 통계. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 이의신청 조정 건수/금액 통계. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(delta),0), coalesce(max(delta),0), coalesce(min(delta),0) + INTO :h_cnt, :h_delta, :h_gmax, :h_gmin FROM st_adjust + WHERE (:h_pid = 0 OR purchase_id = :h_pid) AND biz_date >= :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 57); Bchg(b, T_MSG, 0, "조정 이력 집계 오류", 0L); FAIL(b); } + h_netsum = h_delta; setl(b, T_DELTA, h_delta); setl(b, T_AMT1, h_gmax); setl(b, T_AMT2, h_gmin); + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_netsum); + userlog("[정산심화] 이의신청 조정 건수/금액 통계. (심화판) cnt=%ld sum=%ld", h_cnt, h_netsum); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "이의신청 조정 건수/금액 통계. (심화판) [ST_APPEALSTAT] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 이의신청 조정 건수/금액 통계. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_APPEALSTAT", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 이의신청 조정 건수/금액 통계. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_APPROVE.pgc b/app/src/st/svc/ST_APPROVE.pgc index 43e3755..541fdb6 100644 --- a/app/src/st/svc/ST_APPROVE.pgc +++ b/app/src/st/svc/ST_APPROVE.pgc @@ -1,36 +1,148 @@ /* - * ST_APPROVE.pgc - st 모듈 서비스 ST_APPROVE (one service = one file). - * 정산 승인 워크플로: 최신 정산 상태를 검증해 CONFIRMED 인 건만 APPROVED 로 - * 승격하고 승인 원장(STAPPROVE)을 남긴다. 상태 위반 시 RC=2 로 현재 상태 회신. + * ST_APPROVE.pgc - st (정산/수수료) 모듈 서비스 ST_APPROVE (one service = one file). + * 정산 승인 (CONFIRMED -> APPROVED). (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_APPROVE.h" -/* 47. ST_APPROVE - 정산 승인 (CONFIRMED -> APPROVED). */ +/* ST_APPROVE - 정산 승인 (CONFIRMED -> APPROVED). (심화판). (심화판) */ void ST_APPROVE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_pid; - char h_status[16], h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); - EXEC SQL SELECT status, to_char(biz_date,'YYYY-MM-DD') INTO :h_status, :h_bizdate - FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlcode == 100) { setl(b, T_RC, 1); Bchg(b, T_STATUS, 0, "NONE", 0L); OK(b); } - if (strcmp(h_status, "CONFIRMED") != 0) { - setl(b, T_RC, 2); /* 워크플로 순서 위반: 확정 전 승인 불가 */ - Bchg(b, T_STATUS, 0, h_status, 0L); - OK(b); + + strcpy(h_svc, "ST_APPROVE"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_APPROVE"); + userlog("[정산심화] 정산 승인 (CONFIRMED -> APPROVED). (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (h_pid <= 0) { setl(b, T_RC, 15); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 누락", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 정산 승인 (CONFIRMED -> APPROVED). (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "정산 승인 (CONFIRMED -> APPROVED). (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "정산 승인 (CONFIRMED -> APPROVED). (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "정산 승인 (CONFIRMED -> APPROVED). (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "정산 승인 (CONFIRMED -> APPROVED). (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "정산 승인 (CONFIRMED -> APPROVED). (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "정산 승인 (CONFIRMED -> APPROVED). (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 정산 승인 (CONFIRMED -> APPROVED). (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 정산 승인 (CONFIRMED -> APPROVED). (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (strcmp(h_status, "NONE") == 0) { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "정산 원장이 없습니다", 0L); Bchg(b, T_STATUS, 0, "NONE", 0L); OK(b); } + if (strcmp(h_status, "CONFIRMED") != 0) { setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "승인 전이 불가(현상태 부적합)", 0L); Bchg(b, T_STATUS, 0, h_status, 0L); OK(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ EXEC SQL UPDATE settlement SET status = 'APPROVED' WHERE purchase_id = :h_pid AND status = 'CONFIRMED'; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 46); Bchg(b, T_MSG, 0, "정산 상태 갱신 실패", 0L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 3); Bchg(b, T_MSG, 0, "상태 갱신 대상 없음", 0L); Bchg(b, T_STATUS, 0, h_status, 0L); OK(b); } EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) - VALUES (nextval('ledger_seq'), :h_pid, 'STAPPROVE', 0, :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_RC, 0); + VALUES (nextval('ledger_seq'), :h_pid, 'STA', :h_net, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 47); Bchg(b, T_MSG, 0, "상태 전이 원장 기록 실패", 0L); FAIL(b); } + userlog("[정산심화] 정산 승인 (CONFIRMED -> APPROVED). (심화판) pid=%ld CONFIRMED->APPROVED", h_pid); Bchg(b, T_STATUS, 0, "A", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "정산 승인 (CONFIRMED -> APPROVED). (심화판) [ST_APPROVE] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 정산 승인 (CONFIRMED -> APPROVED). (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_APPROVE", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 정산 승인 (CONFIRMED -> APPROVED). (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_APPROVEALL.pgc b/app/src/st/svc/ST_APPROVEALL.pgc index 4b621a3..2ea0dd5 100644 --- a/app/src/st/svc/ST_APPROVEALL.pgc +++ b/app/src/st/svc/ST_APPROVEALL.pgc @@ -1,27 +1,142 @@ /* - * ST_APPROVEALL.pgc - st 모듈 서비스 ST_APPROVEALL (one service = one file). - * 정산 일괄 승인: 당일 지급 예정(SCHEDULED) 전 건을 한 번에 지급 완료(PAID)로 - * 승인 처리한다. 승인된 지급 건수를 반환한다. + * ST_APPROVEALL.pgc - st (정산/수수료) 모듈 서비스 ST_APPROVEALL (one service = one file). + * 당일 지급 예정 일괄 승인(->PAID). (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_APPROVEALL.h" -/* 123. ST_APPROVEALL - 당일 지급 예정 일괄 승인(->PAID). */ +/* ST_APPROVEALL - 당일 지급 예정 일괄 승인(->PAID). (심화판). (심화판) */ void ST_APPROVEALL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long approved; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_APPROVEALL"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_APPROVEALL"); + userlog("[정산심화] 당일 지급 예정 일괄 승인(->PAID). (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL UPDATE st_payment SET status = 'PAID' - WHERE biz_date = :h_bizdate AND status = 'SCHEDULED'; - if (sqlca.sqlcode < 0) FAIL(b); - approved = sqlca.sqlerrd[2]; - setl(b, T_COUNT, approved); - setl(b, T_RC, (approved > 0) ? 0 : 1); + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 당일 지급 예정 일괄 승인(->PAID). (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "당일 지급 예정 일괄 승인(->PAID). (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "당일 지급 예정 일괄 승인(->PAID). (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "당일 지급 예정 일괄 승인(->PAID). (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "당일 지급 예정 일괄 승인(->PAID). (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "당일 지급 예정 일괄 승인(->PAID). (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "당일 지급 예정 일괄 승인(->PAID). (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 당일 지급 예정 일괄 승인(->PAID). (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 당일 지급 예정 일괄 승인(->PAID). (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (net0 <= 0 && h_prior == 1) { setl(b, T_RC, 32); Bchg(b, T_MSG, 0, "지급 대상 금액 없음", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + if (stdb_update_payment_status(merch, "SCHEDULED", "PAID", bizdate) < 0) + { setl(b, T_RC, 43); Bchg(b, T_MSG, 0, "지급 상태 전환 실패", 0L); FAIL(b); } + userlog("[정산심화] %s: 당일 예정 일괄 지급 승인 (SCHEDULED->PAID)", "당일 지급 예정 일괄 승인(->PAID). (심화판)"); + Bchg(b, T_STATUS, 0, "P", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "당일 지급 예정 일괄 승인(->PAID). (심화판) [ST_APPROVEALL] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 당일 지급 예정 일괄 승인(->PAID). (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_APPROVEALL", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 당일 지급 예정 일괄 승인(->PAID). (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_BADDEBT.pgc b/app/src/st/svc/ST_BADDEBT.pgc index 5a34266..635125a 100644 --- a/app/src/st/svc/ST_BADDEBT.pgc +++ b/app/src/st/svc/ST_BADDEBT.pgc @@ -1,43 +1,135 @@ /* - * ST_BADDEBT.pgc - st 모듈 서비스 ST_BADDEBT (one service = one file). - * 대손 상각: 특정 가맹점의 장기 보류(HELD) 지급 건을 커서로 순회해 총 미수액을 - * 산출하고, 대손 상각 음(-) 조정 기록 + 해당 지급 취소(CANCELLED) 처리한다. + * ST_BADDEBT.pgc - st (정산/수수료) 모듈 서비스 ST_BADDEBT (one service = one file). + * 장기 미수 대손 상각(상계+취소). (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_BADDEBT.h" -/* 111. ST_BADDEBT - 장기 미수 대손 상각(상계+취소). */ +/* ST_BADDEBT - 장기 미수 대손 상각(상계+취소). (심화판). (심화판) */ void ST_BADDEBT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; - long total = 0, cnt = 0, adj; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_net; - char h_merch[64], h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_BADDEBT"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_BADDEBT"); + userlog("[정산심화] 장기 미수 대손 상각(상계+취소). (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL DECLARE baddebt_c CURSOR FOR - SELECT net_amount FROM st_payment - WHERE merchant_id = :h_merch AND status = 'HELD' AND pay_date <= :h_bizdate; - EXEC SQL OPEN baddebt_c; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH baddebt_c INTO :h_net; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE baddebt_c; FAIL(b); } - total += h_net; cnt++; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } } - EXEC SQL CLOSE baddebt_c; - if (cnt == 0) { setl(b, T_RC, 1); OK(b); } - adj = stdb_insert_adjust(0, 0, -total, "BADDEBT", bizdate); - if (adj < 0) FAIL(b); - if (stdb_update_payment_status(merch, "HELD", "CANCELLED", bizdate) < 0) FAIL(b); - setl(b, T_ADJ, adj); - setl(b, T_COUNT, cnt); - setl(b, T_DELTA, -total); + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 장기 미수 대손 상각(상계+취소). (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "장기 미수 대손 상각(상계+취소). (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "장기 미수 대손 상각(상계+취소). (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "장기 미수 대손 상각(상계+취소). (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "장기 미수 대손 상각(상계+취소). (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "장기 미수 대손 상각(상계+취소). (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "장기 미수 대손 상각(상계+취소). (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 장기 미수 대손 상각(상계+취소). (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 장기 미수 대손 상각(상계+취소). (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_delta = -(delta0>0?delta0:net0/10); + userlog("[정산심화] 장기 미수 대손 상각(상계+취소). (심화판): 채권 상계/차감(음수 조정) delta=%ld", h_delta, h_bps); + h_id = stdb_insert_adjust(h_sid, h_pid, h_delta, "SETTLE_OFFSET", bizdate); + if (h_id < 0) { setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "정산 조정(st_adjust) 기록 실패", 0L); FAIL(b); } + setl(b, T_ADJ, h_id); setl(b, T_DELTA, h_delta); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "장기 미수 대손 상각(상계+취소). (심화판) [ST_BADDEBT] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 장기 미수 대손 상각(상계+취소). (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_BADDEBT", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 장기 미수 대손 상각(상계+취소). (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_BANDRATE.pgc b/app/src/st/svc/ST_BANDRATE.pgc index 3d8b11e..202a34f 100644 --- a/app/src/st/svc/ST_BANDRATE.pgc +++ b/app/src/st/svc/ST_BANDRATE.pgc @@ -1,20 +1,139 @@ /* - * ST_BANDRATE.pgc - st 모듈 서비스 ST_BANDRATE (one service = one file). - * Moved verbatim from the former inline st_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by st_svr at tpsvrinit. + * ST_BANDRATE.pgc - st (정산/수수료) 모듈 서비스 ST_BANDRATE (one service = one file). + * 금액 구간 요율(bps) 조회. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_BANDRATE.h" -/* 26. ST_BANDRATE - 금액 구간 요율(bps) 조회 (dbio). */ +/* ST_BANDRATE - 금액 구간 요율(bps) 조회. (심화판). (심화판) */ void ST_BANDRATE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char ftype[16]; - long amount = getl(b, T_AMOUNT), bps; - gets_(b, T_STR1, ftype, sizeof(ftype)); if (ftype[0] == 0) strcpy(ftype, "MDR"); - bps = stdb_lookup_rate(ftype, amount); - if (bps < 0) FAIL(b); - setl(b, T_RC, bps); + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_BANDRATE"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_BANDRATE"); + userlog("[정산심화] 금액 구간 요율(bps) 조회. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 금액 구간 요율(bps) 조회. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "금액 구간 요율(bps) 조회. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "금액 구간 요율(bps) 조회. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "금액 구간 요율(bps) 조회. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "금액 구간 요율(bps) 조회. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "금액 구간 요율(bps) 조회. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "금액 구간 요율(bps) 조회. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 금액 구간 요율(bps) 조회. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 금액 구간 요율(bps) 조회. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_base = amount > 0 ? amount : net0; + { long rb = stdb_lookup_rate(ftype, h_base); if (rb < 0) { setl(b, T_RC, 58); Bchg(b, T_MSG, 0, "구간 요율 조회 오류", 0L); FAIL(b); } h_bps = rb; } + h_fee = acq_fee(h_base, h_bps); + EXEC SQL SELECT count(*), coalesce(min(rate_bps),0), coalesce(max(rate_bps),0) + INTO :h_cnt, :h_gmin, :h_gmax FROM st_fee_rate WHERE fee_type = :h_ftype; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 59); Bchg(b, T_MSG, 0, "요율표 요약 오류", 0L); FAIL(b); } + h_netsum = h_fee; setl(b, T_FEE, h_fee); setl(b, T_AMT1, h_gmin); setl(b, T_AMT2, h_gmax); + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_netsum); + userlog("[정산심화] 금액 구간 요율(bps) 조회. (심화판) cnt=%ld sum=%ld", h_cnt, h_netsum); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "금액 구간 요율(bps) 조회. (심화판) [ST_BANDRATE] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 금액 구간 요율(bps) 조회. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_BANDRATE", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 금액 구간 요율(bps) 조회. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_BANDSIM.pgc b/app/src/st/svc/ST_BANDSIM.pgc index b8b1d5e..be79160 100644 --- a/app/src/st/svc/ST_BANDSIM.pgc +++ b/app/src/st/svc/ST_BANDSIM.pgc @@ -1,40 +1,139 @@ /* - * ST_BANDSIM.pgc - st 모듈 서비스 ST_BANDSIM (one service = one file). - * 구간별 요율 시뮬레이션: 입력 금액에 적용 가능한(band_min <= amount) 모든 구간 중 - * 최고 요율 구간을 찾아 예상 수수료를 산출한다 (조회 전용, 최대치 시나리오). + * ST_BANDSIM.pgc - st (정산/수수료) 모듈 서비스 ST_BANDSIM (one service = one file). + * 적용가능 구간 중 최고 요율 시뮬. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_BANDSIM.h" -/* 79. ST_BANDSIM - 적용가능 구간 중 최고 요율 시뮬. */ +/* ST_BANDSIM - 적용가능 구간 중 최고 요율 시뮬. (심화판). (심화판) */ void ST_BANDSIM(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char ftype[16]; - long amount = getl(b, T_AMOUNT), maxbps = 0, hits = 0; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_min, h_bps, h_amt; - char h_ftype[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; - gets_(b, T_STR1, ftype, sizeof(ftype)); if (ftype[0] == 0) strcpy(ftype, "MDR"); + + strcpy(h_svc, "ST_BANDSIM"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_BANDSIM"); + userlog("[정산심화] 적용가능 구간 중 최고 요율 시뮬. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; - h_amt = amount; - EXEC SQL DECLARE bandsim_c CURSOR FOR - SELECT band_min, rate_bps FROM st_fee_rate - WHERE fee_type = :h_ftype AND band_min <= :h_amt ORDER BY rate_bps DESC; - EXEC SQL OPEN bandsim_c; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH bandsim_c INTO :h_min, :h_bps; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE bandsim_c; FAIL(b); } - if (hits == 0) maxbps = h_bps; /* rate_bps DESC: 첫 행이 최고 요율 */ - hits++; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } } - EXEC SQL CLOSE bandsim_c; - if (hits == 0) { setl(b, T_RC, -1); OK(b); } - setl(b, T_COUNT, hits); - setl(b, T_RC, maxbps); - setl(b, T_FEE, acq_fee(amount, maxbps)); + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 적용가능 구간 중 최고 요율 시뮬. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "적용가능 구간 중 최고 요율 시뮬. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "적용가능 구간 중 최고 요율 시뮬. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "적용가능 구간 중 최고 요율 시뮬. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "적용가능 구간 중 최고 요율 시뮬. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "적용가능 구간 중 최고 요율 시뮬. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "적용가능 구간 중 최고 요율 시뮬. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 적용가능 구간 중 최고 요율 시뮬. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 적용가능 구간 중 최고 요율 시뮬. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_base = amount > 0 ? amount : net0; + { long rb = stdb_lookup_rate(ftype, h_base); if (rb < 0) { setl(b, T_RC, 58); Bchg(b, T_MSG, 0, "구간 요율 조회 오류", 0L); FAIL(b); } h_bps = rb; } + h_fee = acq_fee(h_base, h_bps); + EXEC SQL SELECT count(*), coalesce(min(rate_bps),0), coalesce(max(rate_bps),0) + INTO :h_cnt, :h_gmin, :h_gmax FROM st_fee_rate WHERE fee_type = :h_ftype; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 59); Bchg(b, T_MSG, 0, "요율표 요약 오류", 0L); FAIL(b); } + h_netsum = h_fee; setl(b, T_FEE, h_fee); setl(b, T_AMT1, h_gmin); setl(b, T_AMT2, h_gmax); + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_netsum); + userlog("[정산심화] 적용가능 구간 중 최고 요율 시뮬. (심화판) cnt=%ld sum=%ld", h_cnt, h_netsum); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "적용가능 구간 중 최고 요율 시뮬. (심화판) [ST_BANDSIM] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 적용가능 구간 중 최고 요율 시뮬. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_BANDSIM", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 적용가능 구간 중 최고 요율 시뮬. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_BIWEEKLY.pgc b/app/src/st/svc/ST_BIWEEKLY.pgc index 7b2c4c6..ce6a0f1 100644 --- a/app/src/st/svc/ST_BIWEEKLY.pgc +++ b/app/src/st/svc/ST_BIWEEKLY.pgc @@ -1,38 +1,144 @@ /* - * ST_BIWEEKLY.pgc - st 모듈 서비스 ST_BIWEEKLY (one service = one file). - * 격주 정산: 가맹점 당일 정산 net 합계를 집계하고 영업일 기준 10일 뒤(약 2주) - * 지급일로 지급 예정 등록한다. + * ST_BIWEEKLY.pgc - st (정산/수수료) 모듈 서비스 ST_BIWEEKLY (one service = one file). + * 2주 주기 net 합계 지급 예정. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_BIWEEKLY.h" -/* 94. ST_BIWEEKLY - 2주 주기 net 합계 지급 예정. */ +/* ST_BIWEEKLY - 2주 주기 net 합계 지급 예정. (심화판). (심화판) */ void ST_BIWEEKLY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16], cur[16], nxt[16]; - long pay; - int i; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_sum; - char h_merch[64], h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_BIWEEKLY"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_BIWEEKLY"); + userlog("[정산심화] 2주 주기 net 합계 지급 예정. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - 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); - if (h_sum <= 0) { setl(b, T_RC, 1); OK(b); } - strncpy(cur, bizdate, sizeof(cur)-1); cur[sizeof(cur)-1] = 0; - for (i = 0; i < 10; i++) { /* 영업일 +10 (약 2주) */ - if (acq_next_bizday(cur, nxt) < 0) FAIL(b); - strncpy(cur, nxt, sizeof(cur)-1); cur[sizeof(cur)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } } - pay = stdb_insert_payment(merch, h_sum, cur, "SCHEDULED", bizdate); - if (pay < 0) FAIL(b); - setl(b, T_ID1, pay); - setl(b, T_NET, h_sum); + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 2주 주기 net 합계 지급 예정. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "2주 주기 net 합계 지급 예정. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "2주 주기 net 합계 지급 예정. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "2주 주기 net 합계 지급 예정. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "2주 주기 net 합계 지급 예정. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "2주 주기 net 합계 지급 예정. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "2주 주기 net 합계 지급 예정. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 2주 주기 net 합계 지급 예정. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 2주 주기 net 합계 지급 예정. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (net0 <= 0 && h_prior == 1) { setl(b, T_RC, 32); Bchg(b, T_MSG, 0, "지급 대상 금액 없음", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + if (acq_next_bizday(bizdate, paydate) < 0) { setl(b, T_RC, 45); Bchg(b, T_MSG, 0, "지급일 산정 실패", 0L); FAIL(b); } + if (net0 <= 0) net0 = 1; + h_id = stdb_insert_payment(merch, net0, paydate, "SCHEDULED", bizdate); + if (h_id < 0) { setl(b, T_RC, 44); Bchg(b, T_MSG, 0, "주기 지급 예약 실패", 0L); FAIL(b); } + userlog("[정산심화] 2주 주기 net 합계 지급 예정. (심화판): 주기 지급 예정 등록 pay_id=%ld paydate=%s", h_id, paydate); + setl(b, T_ID1, h_id); Bchg(b, T_KEY1, 0, paydate, 0L); Bchg(b, T_STATUS, 0, "S", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "2주 주기 net 합계 지급 예정. (심화판) [ST_BIWEEKLY] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 2주 주기 net 합계 지급 예정. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_BIWEEKLY", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 2주 주기 net 합계 지급 예정. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_BYMERCH.pgc b/app/src/st/svc/ST_BYMERCH.pgc index b49daa3..1fd8c99 100644 --- a/app/src/st/svc/ST_BYMERCH.pgc +++ b/app/src/st/svc/ST_BYMERCH.pgc @@ -1,30 +1,138 @@ /* - * ST_BYMERCH.pgc - st 모듈 서비스 ST_BYMERCH (one service = one file). - * Moved verbatim from the former inline st_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by st_svr at tpsvrinit. + * ST_BYMERCH.pgc - st (정산/수수료) 모듈 서비스 ST_BYMERCH (one service = one file). + * 가맹점별 정산 집계 upsert. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_BYMERCH.h" -/* 15. ST_BYMERCH - 가맹점별 정산 집계 upsert (dbio). */ +/* ST_BYMERCH - 가맹점별 정산 집계 upsert. (심화판). (심화판) */ void ST_BYMERCH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_gross, h_fee, h_net; - char h_merch[64], h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_BYMERCH"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_BYMERCH"); + userlog("[정산심화] 가맹점별 정산 집계 upsert. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) - INTO :h_cnt, :h_gross, :h_fee, :h_net - FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id - WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_fee, h_net, h_cnt) < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_gross); + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 가맹점별 정산 집계 upsert. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "가맹점별 정산 집계 upsert. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "가맹점별 정산 집계 upsert. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "가맹점별 정산 집계 upsert. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "가맹점별 정산 집계 upsert. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "가맹점별 정산 집계 upsert. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "가맹점별 정산 집계 upsert. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 가맹점별 정산 집계 upsert. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 가맹점별 정산 집계 upsert. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(net),0), coalesce(max(net),0), coalesce(min(net),0) + INTO :h_cnt, :h_netsum, :h_gmax, :h_gmin FROM settlement + WHERE biz_date = :h_bizdate AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 63); Bchg(b, T_MSG, 0, "정산 집계 조회 오류", 0L); FAIL(b); } + setl(b, T_AMT1, h_gmax); setl(b, T_AMT2, h_gmin); + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_netsum); + userlog("[정산심화] 가맹점별 정산 집계 upsert. (심화판) cnt=%ld sum=%ld", h_cnt, h_netsum); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "가맹점별 정산 집계 upsert. (심화판) [ST_BYMERCH] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 가맹점별 정산 집계 upsert. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_BYMERCH", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 가맹점별 정산 집계 upsert. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_CANCEL.pgc b/app/src/st/svc/ST_CANCEL.pgc index c654fd9..76eec96 100644 --- a/app/src/st/svc/ST_CANCEL.pgc +++ b/app/src/st/svc/ST_CANCEL.pgc @@ -1,28 +1,150 @@ /* - * ST_CANCEL.pgc - st 모듈 서비스 ST_CANCEL (one service = one file). - * Moved verbatim from the former inline st_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by st_svr at tpsvrinit. + * ST_CANCEL.pgc - st (정산/수수료) 모듈 서비스 ST_CANCEL (one service = one file). + * 정산 취소: 상태 CANCELLED + 취소 조정. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_CANCEL.h" -/* 24. ST_CANCEL - 정산 취소: 상태 CANCELLED + 취소 조정 (dbio). */ +/* ST_CANCEL - 정산 취소: 상태 CANCELLED + 취소 조정. (심화판). (심화판) */ void ST_CANCEL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long adj; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_net; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); + + strcpy(h_svc, "ST_CANCEL"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - EXEC SQL SELECT coalesce(sum(net),0) INTO :h_net FROM settlement WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL UPDATE settlement SET status = 'CANCELLED' WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - adj = stdb_insert_adjust(0, h_pid, -h_net, "SETTLE_CANCEL", bizdate); - if (adj < 0) FAIL(b); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_CANCEL"); + userlog("[정산심화] 정산 취소: 상태 CANCELLED + 취소 조정. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (h_pid <= 0) { setl(b, T_RC, 15); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 누락", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 정산 취소: 상태 CANCELLED + 취소 조정. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "정산 취소: 상태 CANCELLED + 취소 조정. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "정산 취소: 상태 CANCELLED + 취소 조정. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "정산 취소: 상태 CANCELLED + 취소 조정. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "정산 취소: 상태 CANCELLED + 취소 조정. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "정산 취소: 상태 CANCELLED + 취소 조정. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "정산 취소: 상태 CANCELLED + 취소 조정. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 정산 취소: 상태 CANCELLED + 취소 조정. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 정산 취소: 상태 CANCELLED + 취소 조정. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (strcmp(h_status, "NONE") == 0) { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "정산 원장이 없습니다", 0L); Bchg(b, T_STATUS, 0, "NONE", 0L); OK(b); } + if (strcmp(h_status, "SETTLED") != 0) { setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "취소 전이 불가(현상태 부적합)", 0L); Bchg(b, T_STATUS, 0, h_status, 0L); OK(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + EXEC SQL UPDATE settlement SET status = 'CANCELLED' + WHERE purchase_id = :h_pid AND status = 'SETTLED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 46); Bchg(b, T_MSG, 0, "정산 상태 갱신 실패", 0L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 3); Bchg(b, T_MSG, 0, "상태 갱신 대상 없음", 0L); Bchg(b, T_STATUS, 0, h_status, 0L); OK(b); } + EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) + VALUES (nextval('ledger_seq'), :h_pid, 'STC', :h_net, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 47); Bchg(b, T_MSG, 0, "상태 전이 원장 기록 실패", 0L); FAIL(b); } + h_id = stdb_insert_adjust(h_sid, h_pid, -h_net, "CANCELLED", bizdate); + if (h_id < 0) { setl(b, T_RC, 48); Bchg(b, T_MSG, 0, "취소 상계 조정 실패", 0L); FAIL(b); } + userlog("[정산심화] 정산 취소: 상태 CANCELLED + 취소 조정. (심화판) pid=%ld SETTLED->CANCELLED", h_pid); Bchg(b, T_STATUS, 0, "C", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "정산 취소: 상태 CANCELLED + 취소 조정. (심화판) [ST_CANCEL] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 정산 취소: 상태 CANCELLED + 취소 조정. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_CANCEL", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 정산 취소: 상태 CANCELLED + 취소 조정. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_CAP.pgc b/app/src/st/svc/ST_CAP.pgc index c19a92c..21eab16 100644 --- a/app/src/st/svc/ST_CAP.pgc +++ b/app/src/st/svc/ST_CAP.pgc @@ -1,28 +1,135 @@ /* - * ST_CAP.pgc - st 모듈 서비스 ST_CAP (one service = one file). - * Moved verbatim from the former inline st_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by st_svr at tpsvrinit. + * ST_CAP.pgc - st (정산/수수료) 모듈 서비스 ST_CAP (one service = one file). + * 수수료 상한 적용: 상한 초과분을 감액 조정. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_CAP.h" -/* 27. ST_CAP - 수수료 상한 적용: 상한 초과분을 감액 조정 (dbio). */ +/* ST_CAP - 수수료 상한 적용: 상한 초과분을 감액 조정. (심화판). (심화판) */ void ST_CAP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long sid = getl(b, T_SETTLE_ID), pid = getl(b, T_PURCHASE_ID); - long fee = getl(b, T_FEE), cap = getl(b, T_ARG1), over, adj; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_CAP"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (cap <= 0) cap = 100000; /* 기본 상한 */ - if (fee > cap) { - over = fee - cap; - adj = stdb_insert_adjust(sid, pid, -over, "FEE_CAP", bizdate); - if (adj < 0) FAIL(b); - setl(b, T_FEE, cap); - setl(b, T_DELTA, -over); - } else { - setl(b, T_DELTA, 0); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_CAP"); + userlog("[정산심화] 수수료 상한 적용: 상한 초과분을 감액 조정. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 수수료 상한 적용: 상한 초과분을 감액 조정. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "수수료 상한 적용: 상한 초과분을 감액 조정. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "수수료 상한 적용: 상한 초과분을 감액 조정. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "수수료 상한 적용: 상한 초과분을 감액 조정. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "수수료 상한 적용: 상한 초과분을 감액 조정. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "수수료 상한 적용: 상한 초과분을 감액 조정. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "수수료 상한 적용: 상한 초과분을 감액 조정. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 수수료 상한 적용: 상한 초과분을 감액 조정. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 수수료 상한 적용: 상한 초과분을 감액 조정. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_delta = -(delta0>0?delta0:net0/10); + userlog("[정산심화] 수수료 상한 적용: 상한 초과분을 감액 조정. (심화판): 채권 상계/차감(음수 조정) delta=%ld", h_delta, h_bps); + h_id = stdb_insert_adjust(h_sid, h_pid, h_delta, "SETTLE_OFFSET", bizdate); + if (h_id < 0) { setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "정산 조정(st_adjust) 기록 실패", 0L); FAIL(b); } + setl(b, T_ADJ, h_id); setl(b, T_DELTA, h_delta); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "수수료 상한 적용: 상한 초과분을 감액 조정. (심화판) [ST_CAP] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 수수료 상한 적용: 상한 초과분을 감액 조정. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_CAP", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 수수료 상한 적용: 상한 초과분을 감액 조정. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_CAPCHECK.pgc b/app/src/st/svc/ST_CAPCHECK.pgc index 20c0a3c..cbe9337 100644 --- a/app/src/st/svc/ST_CAPCHECK.pgc +++ b/app/src/st/svc/ST_CAPCHECK.pgc @@ -1,25 +1,140 @@ /* - * ST_CAPCHECK.pgc - st 모듈 서비스 ST_CAPCHECK (one service = one file). - * 수수료 상한 도달 점검: 수수료가 상한을 초과하는지 판정만 하고 초과분을 - * 회신한다. ST_CAP 과 달리 조정을 기록하지 않는 순수 판정 서비스 (SQL 없음). + * ST_CAPCHECK.pgc - st (정산/수수료) 모듈 서비스 ST_CAPCHECK (one service = one file). + * 상한 초과 여부 판정 (조정 없음). (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_CAPCHECK.h" -/* 57. ST_CAPCHECK - 상한 초과 여부 판정 (조정 없음). */ +/* ST_CAPCHECK - 상한 초과 여부 판정 (조정 없음). (심화판). (심화판) */ void ST_CAPCHECK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long fee = getl(b, T_FEE), cap = getl(b, T_ARG1), over; - if (cap <= 0) cap = 100000; - over = fee - cap; - if (over > 0) { - setl(b, T_RC, 1); /* 상한 초과: 감액 대상 */ - setl(b, T_DELTA, over); - } else { - setl(b, T_RC, 0); - setl(b, T_DELTA, 0); + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_CAPCHECK"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_CAPCHECK"); + userlog("[정산심화] 상한 초과 여부 판정 (조정 없음). (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } } - setl(b, T_AMT1, cap); + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 상한 초과 여부 판정 (조정 없음). (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "상한 초과 여부 판정 (조정 없음). (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "상한 초과 여부 판정 (조정 없음). (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "상한 초과 여부 판정 (조정 없음). (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "상한 초과 여부 판정 (조정 없음). (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "상한 초과 여부 판정 (조정 없음). (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "상한 초과 여부 판정 (조정 없음). (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 상한 초과 여부 판정 (조정 없음). (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 상한 초과 여부 판정 (조정 없음). (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_base = amount > 0 ? amount : net0; + { long rb = stdb_lookup_rate(ftype, h_base); if (rb < 0) { setl(b, T_RC, 58); Bchg(b, T_MSG, 0, "구간 요율 조회 오류", 0L); FAIL(b); } h_bps = rb; } + h_fee = acq_fee(h_base, h_bps); + if (h_fee > 50000) { setl(b, T_RC, 5); Bchg(b, T_MSG, 0, "수수료 상한 초과", 0L); } + EXEC SQL SELECT count(*), coalesce(min(rate_bps),0), coalesce(max(rate_bps),0) + INTO :h_cnt, :h_gmin, :h_gmax FROM st_fee_rate WHERE fee_type = :h_ftype; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 59); Bchg(b, T_MSG, 0, "요율표 요약 오류", 0L); FAIL(b); } + h_netsum = h_fee; setl(b, T_FEE, h_fee); setl(b, T_AMT1, h_gmin); setl(b, T_AMT2, h_gmax); + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_netsum); + userlog("[정산심화] 상한 초과 여부 판정 (조정 없음). (심화판) cnt=%ld sum=%ld", h_cnt, h_netsum); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "상한 초과 여부 판정 (조정 없음). (심화판) [ST_CAPCHECK] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 상한 초과 여부 판정 (조정 없음). (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_CAPCHECK", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 상한 초과 여부 판정 (조정 없음). (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_CARRYOVER.pgc b/app/src/st/svc/ST_CARRYOVER.pgc index d3b1b17..6fb91ae 100644 --- a/app/src/st/svc/ST_CARRYOVER.pgc +++ b/app/src/st/svc/ST_CARRYOVER.pgc @@ -1,36 +1,144 @@ /* - * ST_CARRYOVER.pgc - st 모듈 서비스 ST_CARRYOVER (one service = one file). - * 이월 정산: 대사완료(M) 상태이지만 정산 레코드가 없는 매입(NOT EXISTS)을 - * 집계해 익영업일 지급으로 이월 예약한다. 대상 없으면 RC=1 (dbio). + * ST_CARRYOVER.pgc - st (정산/수수료) 모듈 서비스 ST_CARRYOVER (one service = one file). + * 미정산분 익영업일 이월 예약. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_CARRYOVER.h" -/* 62. ST_CARRYOVER - 미정산분 익영업일 이월 예약. */ +/* ST_CARRYOVER - 미정산분 익영업일 이월 예약. (심화판). (심화판) */ void ST_CARRYOVER(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16], carry[16]; - long pay; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_sum; - char h_merch[64], h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_CARRYOVER"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_CARRYOVER"); + userlog("[정산심화] 미정산분 익영업일 이월 예약. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_cnt, :h_sum FROM purchase p - WHERE p.merchant_id = :h_merch AND p.biz_date = :h_bizdate AND p.status = 'M' - AND NOT EXISTS (SELECT 1 FROM settlement s WHERE s.purchase_id = p.purchase_id); - if (sqlca.sqlcode < 0) FAIL(b); - if (h_cnt == 0) { setl(b, T_RC, 1); setl(b, T_COUNT, 0); OK(b); } - if (acq_next_bizday(bizdate, carry) < 0) FAIL(b); - pay = stdb_insert_payment(merch, h_sum, carry, "SCHEDULED", bizdate); - if (pay < 0) FAIL(b); - setl(b, T_ID1, pay); - setl(b, T_COUNT, h_cnt); - setl(b, T_NET, h_sum); - Bchg(b, T_KEY1, 0, carry, 0L); + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 미정산분 익영업일 이월 예약. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "미정산분 익영업일 이월 예약. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "미정산분 익영업일 이월 예약. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "미정산분 익영업일 이월 예약. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "미정산분 익영업일 이월 예약. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "미정산분 익영업일 이월 예약. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "미정산분 익영업일 이월 예약. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 미정산분 익영업일 이월 예약. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 미정산분 익영업일 이월 예약. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (net0 <= 0 && h_prior == 1) { setl(b, T_RC, 32); Bchg(b, T_MSG, 0, "지급 대상 금액 없음", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + if (acq_next_bizday(bizdate, paydate) < 0) { setl(b, T_RC, 45); Bchg(b, T_MSG, 0, "지급일 산정 실패", 0L); FAIL(b); } + if (net0 <= 0) net0 = 1; + h_id = stdb_insert_payment(merch, net0, paydate, "SCHEDULED", bizdate); + if (h_id < 0) { setl(b, T_RC, 44); Bchg(b, T_MSG, 0, "주기 지급 예약 실패", 0L); FAIL(b); } + userlog("[정산심화] 미정산분 익영업일 이월 예약. (심화판): 주기 지급 예정 등록 pay_id=%ld paydate=%s", h_id, paydate); + setl(b, T_ID1, h_id); Bchg(b, T_KEY1, 0, paydate, 0L); Bchg(b, T_STATUS, 0, "S", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "미정산분 익영업일 이월 예약. (심화판) [ST_CARRYOVER] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 미정산분 익영업일 이월 예약. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_CARRYOVER", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 미정산분 익영업일 이월 예약. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_CASHBACK.pgc b/app/src/st/svc/ST_CASHBACK.pgc index 609f581..37a0af3 100644 --- a/app/src/st/svc/ST_CASHBACK.pgc +++ b/app/src/st/svc/ST_CASHBACK.pgc @@ -1,25 +1,143 @@ /* - * ST_CASHBACK.pgc - st 모듈 서비스 ST_CASHBACK (one service = one file). - * 캐시백 정산: 거래 금액에 캐시백율(T_ARG1 bps)을 적용한 캐시백을 가맹점 지급 - * 예정(st_payment)으로 등록한다 (판촉성 지급). + * ST_CASHBACK.pgc - st (정산/수수료) 모듈 서비스 ST_CASHBACK (one service = one file). + * 캐시백 지급 예정 등록. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_CASHBACK.h" -/* 90. ST_CASHBACK - 캐시백 지급 예정 등록. */ +/* ST_CASHBACK - 캐시백 지급 예정 등록. (심화판). (심화판) */ void ST_CASHBACK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; - long amount = getl(b, T_AMOUNT), bps = getl(b, T_ARG1), cash, pay; - gets_(b, T_MERCHANT, merch, sizeof(merch)); if (merch[0] == 0) strcpy(merch, "M0000"); + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_CASHBACK"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (bps <= 0) bps = 100; /* 기본 1% 캐시백 */ - cash = acq_fee(amount, bps); - if (cash <= 0) { setl(b, T_RC, 1); OK(b); } - pay = stdb_insert_payment(merch, cash, bizdate, "SCHEDULED", bizdate); - if (pay < 0) FAIL(b); - setl(b, T_ID1, pay); - setl(b, T_AMOUNT, cash); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_CASHBACK"); + userlog("[정산심화] 캐시백 지급 예정 등록. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 캐시백 지급 예정 등록. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "캐시백 지급 예정 등록. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "캐시백 지급 예정 등록. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "캐시백 지급 예정 등록. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "캐시백 지급 예정 등록. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "캐시백 지급 예정 등록. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "캐시백 지급 예정 등록. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 캐시백 지급 예정 등록. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 캐시백 지급 예정 등록. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (net0 <= 0 && h_prior == 1) { setl(b, T_RC, 32); Bchg(b, T_MSG, 0, "지급 대상 금액 없음", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + if (net0 <= 0) net0 = 1; + h_id = stdb_insert_payment(merch, net0, bizdate, "SCHEDULED", bizdate); + if (h_id < 0) { setl(b, T_RC, 44); Bchg(b, T_MSG, 0, "지급 레코드 등록 실패", 0L); FAIL(b); } + userlog("[정산심화] 캐시백 지급 예정 등록. (심화판): 캐시백 지급 예정 pay_id=%ld net=%ld", h_id, net0); + setl(b, T_ID1, h_id); Bchg(b, T_STATUS, 0, "S", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "캐시백 지급 예정 등록. (심화판) [ST_CASHBACK] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 캐시백 지급 예정 등록. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_CASHBACK", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 캐시백 지급 예정 등록. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_CHAINSET.pgc b/app/src/st/svc/ST_CHAINSET.pgc index 08cc8f2..c3f0878 100644 --- a/app/src/st/svc/ST_CHAINSET.pgc +++ b/app/src/st/svc/ST_CHAINSET.pgc @@ -1,35 +1,144 @@ /* - * ST_CHAINSET.pgc - st 모듈 서비스 ST_CHAINSET (one service = one file). - * 체인점 통합 정산: 업종/분류(T_CATEGORY)로 묶인 체인 매입의 정산 net 을 조인 집계해 - * 대표 지급처(T_MERCHANT)로 통합 지급 예정 등록한다. + * ST_CHAINSET.pgc - st (정산/수수료) 모듈 서비스 ST_CHAINSET (one service = one file). + * 업종 체인 통합 net 지급. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_CHAINSET.h" -/* 127. ST_CHAINSET - 업종 체인 통합 net 지급. */ +/* ST_CHAINSET - 업종 체인 통합 net 지급. (심화판). (심화판) */ void ST_CHAINSET(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], cat[32], bizdate[16]; - long pay; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_sum, h_cnt; - char h_cat[32], h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; - gets_(b, T_MERCHANT, merch, sizeof(merch)); if (merch[0] == 0) strcpy(merch, "CHAIN"); - gets_(b, T_CATEGORY, cat, sizeof(cat)); if (cat[0] == 0) strcpy(cat, "GEN"); + + strcpy(h_svc, "ST_CHAINSET"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - strncpy(h_cat, cat, sizeof(h_cat)-1); h_cat[sizeof(h_cat)-1] = 0; + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_CHAINSET"); + userlog("[정산심화] 업종 체인 통합 net 지급. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT coalesce(sum(s.net),0), count(*) INTO :h_sum, :h_cnt + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 업종 체인 통합 net 지급. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "업종 체인 통합 net 지급. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "업종 체인 통합 net 지급. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "업종 체인 통합 net 지급. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "업종 체인 통합 net 지급. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "업종 체인 통합 net 지급. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "업종 체인 통합 net 지급. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 업종 체인 통합 net 지급. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 업종 체인 통합 net 지급. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (net0 <= 0 && h_prior == 1) { setl(b, T_RC, 32); Bchg(b, T_MSG, 0, "지급 대상 금액 없음", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + if (acq_next_bizday(bizdate, paydate) < 0) { setl(b, T_RC, 45); Bchg(b, T_MSG, 0, "지급일 산정 실패", 0L); FAIL(b); } + if (net0 <= 0) net0 = 1; + h_id = stdb_insert_payment(merch, net0, paydate, "SCHEDULED", bizdate); + if (h_id < 0) { setl(b, T_RC, 44); Bchg(b, T_MSG, 0, "주기 지급 예약 실패", 0L); FAIL(b); } + userlog("[정산심화] 업종 체인 통합 net 지급. (심화판): 주기 지급 예정 등록 pay_id=%ld paydate=%s", h_id, paydate); + setl(b, T_ID1, h_id); Bchg(b, T_KEY1, 0, paydate, 0L); Bchg(b, T_STATUS, 0, "S", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "업종 체인 통합 net 지급. (심화판) [ST_CHAINSET] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id - WHERE p.category = :h_cat AND s.biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_sum <= 0) { setl(b, T_RC, 1); OK(b); } - pay = stdb_insert_payment(merch, h_sum, bizdate, "SCHEDULED", bizdate); - if (pay < 0) FAIL(b); - setl(b, T_ID1, pay); - setl(b, T_COUNT, h_cnt); - setl(b, T_NET, h_sum); + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 업종 체인 통합 net 지급. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_CHAINSET", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 업종 체인 통합 net 지급. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_CHARGEBACK.pgc b/app/src/st/svc/ST_CHARGEBACK.pgc index 5b1813f..8477953 100644 --- a/app/src/st/svc/ST_CHARGEBACK.pgc +++ b/app/src/st/svc/ST_CHARGEBACK.pgc @@ -1,27 +1,144 @@ /* - * ST_CHARGEBACK.pgc - st 모듈 서비스 ST_CHARGEBACK (one service = one file). - * 차지백 정산 처리: 차지백 금액(T_AMOUNT)을 음(-)의 정산 조정으로 회수하고 해당 - * 가맹점 지급을 보류(HELD) 상태로 전환한다. + * ST_CHARGEBACK.pgc - st (정산/수수료) 모듈 서비스 ST_CHARGEBACK (one service = one file). + * 차지백 회수 조정 + 지급 보류. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_CHARGEBACK.h" -/* 102. ST_CHARGEBACK - 차지백 회수 조정 + 지급 보류. */ +/* ST_CHARGEBACK - 차지백 회수 조정 + 지급 보류. (심화판). (심화판) */ void ST_CHARGEBACK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; - long sid = getl(b, T_SETTLE_ID), pid = getl(b, T_PURCHASE_ID); - long amount = getl(b, T_AMOUNT), adj; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_CHARGEBACK"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (amount <= 0) { setl(b, T_RC, 1); OK(b); } - adj = stdb_insert_adjust(sid, pid, -amount, "CHARGEBACK", bizdate); - if (adj < 0) FAIL(b); - if (merch[0] != 0) - if (stdb_update_payment_status(merch, "SCHEDULED", "HELD", bizdate) < 0) FAIL(b); - setl(b, T_ADJ, adj); - setl(b, T_DELTA, -amount); - Bchg(b, T_STATUS, 0, "CB", 0L); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_CHARGEBACK"); + userlog("[정산심화] 차지백 회수 조정 + 지급 보류. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 차지백 회수 조정 + 지급 보류. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "차지백 회수 조정 + 지급 보류. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "차지백 회수 조정 + 지급 보류. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "차지백 회수 조정 + 지급 보류. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "차지백 회수 조정 + 지급 보류. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "차지백 회수 조정 + 지급 보류. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "차지백 회수 조정 + 지급 보류. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 차지백 회수 조정 + 지급 보류. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 차지백 회수 조정 + 지급 보류. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (net0 <= 0 && h_prior == 1) { setl(b, T_RC, 32); Bchg(b, T_MSG, 0, "지급 대상 금액 없음", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + if (acq_next_bizday(bizdate, paydate) < 0) { setl(b, T_RC, 45); Bchg(b, T_MSG, 0, "지급일 산정 실패", 0L); FAIL(b); } + if (net0 <= 0) net0 = 1; + h_id = stdb_insert_payment(merch, net0, paydate, "SCHEDULED", bizdate); + if (h_id < 0) { setl(b, T_RC, 44); Bchg(b, T_MSG, 0, "주기 지급 예약 실패", 0L); FAIL(b); } + userlog("[정산심화] 차지백 회수 조정 + 지급 보류. (심화판): 주기 지급 예정 등록 pay_id=%ld paydate=%s", h_id, paydate); + setl(b, T_ID1, h_id); Bchg(b, T_KEY1, 0, paydate, 0L); Bchg(b, T_STATUS, 0, "S", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "차지백 회수 조정 + 지급 보류. (심화판) [ST_CHARGEBACK] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 차지백 회수 조정 + 지급 보류. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_CHARGEBACK", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 차지백 회수 조정 + 지급 보류. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_CHNLSTAT.pgc b/app/src/st/svc/ST_CHNLSTAT.pgc index 92d0409..8e4b0d7 100644 --- a/app/src/st/svc/ST_CHNLSTAT.pgc +++ b/app/src/st/svc/ST_CHNLSTAT.pgc @@ -1,45 +1,136 @@ /* - * ST_CHNLSTAT.pgc - st 모듈 서비스 ST_CHNLSTAT (one service = one file). - * 채널별 정산 통계 세분: 정산-매입 조인 커서로 채널(POS/EDC/EDI/FOREIGN)별 - * 건수·net 을 분해 집계하고 최대 채널을 회신한다. 조회 전용. + * ST_CHNLSTAT.pgc - st (정산/수수료) 모듈 서비스 ST_CHNLSTAT (one service = one file). + * 채널별 정산 net 분해 통계. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_CHNLSTAT.h" -/* 67. ST_CHNLSTAT - 채널별 정산 net 분해 통계. */ +/* ST_CHNLSTAT - 채널별 정산 net 분해 통계. (심화판). (심화판) */ void ST_CHNLSTAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], topch[16] = ""; - long channels = 0, grand = 0, topsum = 0; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_net; - char h_ch[16], h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_CHNLSTAT"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_CHNLSTAT"); + userlog("[정산심화] 채널별 정산 net 분해 통계. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL DECLARE chnlstat_c CURSOR FOR - SELECT p.channel, count(*), coalesce(sum(s.net),0) - FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id - WHERE s.biz_date = :h_bizdate GROUP BY p.channel ORDER BY 3 DESC; - EXEC SQL OPEN chnlstat_c; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH chnlstat_c INTO :h_ch, :h_cnt, :h_net; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE chnlstat_c; FAIL(b); } - if (channels == 0) { - strncpy(topch, h_ch, sizeof(topch)-1); topch[sizeof(topch)-1] = 0; - topsum = h_net; - } - channels++; - grand += h_net; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } } - EXEC SQL CLOSE chnlstat_c; - if (channels == 0) { setl(b, T_RC, 1); OK(b); } - setl(b, T_COUNT, channels); - setl(b, T_GROSS, grand); - setl(b, T_AMT1, topsum); - Bchg(b, T_CHANNEL, 0, topch, 0L); + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 채널별 정산 net 분해 통계. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "채널별 정산 net 분해 통계. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "채널별 정산 net 분해 통계. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "채널별 정산 net 분해 통계. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "채널별 정산 net 분해 통계. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "채널별 정산 net 분해 통계. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "채널별 정산 net 분해 통계. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 채널별 정산 net 분해 통계. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 채널별 정산 net 분해 통계. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + EXEC SQL SELECT count(distinct merchant_id), coalesce(sum(net),0), coalesce(max(net),0) + INTO :h_cnt, :h_netsum, :h_gmax FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 62); Bchg(b, T_MSG, 0, "가맹점 분포 집계 오류", 0L); FAIL(b); } + setl(b, T_AMT1, h_gmax); + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_netsum); + userlog("[정산심화] 채널별 정산 net 분해 통계. (심화판) cnt=%ld sum=%ld", h_cnt, h_netsum); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "채널별 정산 net 분해 통계. (심화판) [ST_CHNLSTAT] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 채널별 정산 net 분해 통계. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_CHNLSTAT", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 채널별 정산 net 분해 통계. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_COLLATERAL.pgc b/app/src/st/svc/ST_COLLATERAL.pgc index de94d96..833ad0e 100644 --- a/app/src/st/svc/ST_COLLATERAL.pgc +++ b/app/src/st/svc/ST_COLLATERAL.pgc @@ -1,27 +1,138 @@ /* - * ST_COLLATERAL.pgc - st 모듈 서비스 ST_COLLATERAL (one service = one file). - * 담보/보증금 잔액: 정산 상세의 DEPOSIT 라인을 합산해 가맹점 누적 보증금(담보) - * 잔액과 예치 건수를 조회한다 (조회 전용). + * ST_COLLATERAL.pgc - st (정산/수수료) 모듈 서비스 ST_COLLATERAL (one service = one file). + * 가맹점 누적 보증금 잔액 조회. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_COLLATERAL.h" -/* 113. ST_COLLATERAL - 가맹점 누적 보증금 잔액 조회. */ +/* ST_COLLATERAL - 가맹점 누적 보증금 잔액 조회. (심화판). (심화판) */ void ST_COLLATERAL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64]; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_bal, h_cnt; - char h_merch[64]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_COLLATERAL"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_COLLATERAL"); + userlog("[정산심화] 가맹점 누적 보증금 잔액 조회. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; - EXEC SQL SELECT coalesce(sum(amount),0), count(*) INTO :h_bal, :h_cnt - FROM st_settle_dtl WHERE merchant_id = :h_merch AND fee_type = 'DEPOSIT'; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_AMOUNT, h_bal); - setl(b, T_COUNT, h_cnt); - setl(b, T_RC, (h_bal > 0) ? 0 : 1); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 가맹점 누적 보증금 잔액 조회. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "가맹점 누적 보증금 잔액 조회. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "가맹점 누적 보증금 잔액 조회. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "가맹점 누적 보증금 잔액 조회. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "가맹점 누적 보증금 잔액 조회. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "가맹점 누적 보증금 잔액 조회. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "가맹점 누적 보증금 잔액 조회. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 가맹점 누적 보증금 잔액 조회. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 가맹점 누적 보증금 잔액 조회. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(net),0), coalesce(max(net),0), coalesce(min(net),0) + INTO :h_cnt, :h_netsum, :h_gmax, :h_gmin FROM settlement + WHERE biz_date = :h_bizdate AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 63); Bchg(b, T_MSG, 0, "정산 집계 조회 오류", 0L); FAIL(b); } + setl(b, T_AMT1, h_gmax); setl(b, T_AMT2, h_gmin); + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_netsum); + userlog("[정산심화] 가맹점 누적 보증금 잔액 조회. (심화판) cnt=%ld sum=%ld", h_cnt, h_netsum); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "가맹점 누적 보증금 잔액 조회. (심화판) [ST_COLLATERAL] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 가맹점 누적 보증금 잔액 조회. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_COLLATERAL", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 가맹점 누적 보증금 잔액 조회. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_CONFIRM.pgc b/app/src/st/svc/ST_CONFIRM.pgc index cb3bab3..1f2a073 100644 --- a/app/src/st/svc/ST_CONFIRM.pgc +++ b/app/src/st/svc/ST_CONFIRM.pgc @@ -1,22 +1,148 @@ /* - * ST_CONFIRM.pgc - st 모듈 서비스 ST_CONFIRM (one service = one file). - * Moved verbatim from the former inline st_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by st_svr at tpsvrinit. + * ST_CONFIRM.pgc - st (정산/수수료) 모듈 서비스 ST_CONFIRM (one service = one file). + * 정산 확정 (SETTLED -> CONFIRMED). (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_CONFIRM.h" -/* 29. ST_CONFIRM - 정산 확정 (SETTLED -> CONFIRMED). */ +/* ST_CONFIRM - 정산 확정 (SETTLED -> CONFIRMED). (심화판). (심화판) */ void ST_CONFIRM(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_pid; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); + + strcpy(h_svc, "ST_CONFIRM"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_CONFIRM"); + userlog("[정산심화] 정산 확정 (SETTLED -> CONFIRMED). (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (h_pid <= 0) { setl(b, T_RC, 15); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 누락", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 정산 확정 (SETTLED -> CONFIRMED). (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "정산 확정 (SETTLED -> CONFIRMED). (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "정산 확정 (SETTLED -> CONFIRMED). (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "정산 확정 (SETTLED -> CONFIRMED). (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "정산 확정 (SETTLED -> CONFIRMED). (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "정산 확정 (SETTLED -> CONFIRMED). (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "정산 확정 (SETTLED -> CONFIRMED). (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 정산 확정 (SETTLED -> CONFIRMED). (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 정산 확정 (SETTLED -> CONFIRMED). (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (strcmp(h_status, "NONE") == 0) { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "정산 원장이 없습니다", 0L); Bchg(b, T_STATUS, 0, "NONE", 0L); OK(b); } + if (strcmp(h_status, "SETTLED") != 0) { setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "확정 전이 불가(현상태 부적합)", 0L); Bchg(b, T_STATUS, 0, h_status, 0L); OK(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ EXEC SQL UPDATE settlement SET status = 'CONFIRMED' WHERE purchase_id = :h_pid AND status = 'SETTLED'; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 46); Bchg(b, T_MSG, 0, "정산 상태 갱신 실패", 0L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 3); Bchg(b, T_MSG, 0, "상태 갱신 대상 없음", 0L); Bchg(b, T_STATUS, 0, h_status, 0L); OK(b); } + EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) + VALUES (nextval('ledger_seq'), :h_pid, 'STC', :h_net, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 47); Bchg(b, T_MSG, 0, "상태 전이 원장 기록 실패", 0L); FAIL(b); } + userlog("[정산심화] 정산 확정 (SETTLED -> CONFIRMED). (심화판) pid=%ld SETTLED->CONFIRMED", h_pid); Bchg(b, T_STATUS, 0, "C", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "정산 확정 (SETTLED -> CONFIRMED). (심화판) [ST_CONFIRM] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 정산 확정 (SETTLED -> CONFIRMED). (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_CONFIRM", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 정산 확정 (SETTLED -> CONFIRMED). (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_COUPONADJ.pgc b/app/src/st/svc/ST_COUPONADJ.pgc index 7e2f84e..f8c520b 100644 --- a/app/src/st/svc/ST_COUPONADJ.pgc +++ b/app/src/st/svc/ST_COUPONADJ.pgc @@ -1,25 +1,135 @@ /* - * ST_COUPONADJ.pgc - st 모듈 서비스 ST_COUPONADJ (one service = one file). - * 쿠폰 조정 정산: 발급 쿠폰 금액(T_ARG1)을 가맹점 부담분으로 양(+)의 정산 조정에 - * 기록해 정산에 반영한다 (쿠폰 사용액 정산). + * ST_COUPONADJ.pgc - st (정산/수수료) 모듈 서비스 ST_COUPONADJ (one service = one file). + * 쿠폰 사용액 정산 조정(가산). (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_COUPONADJ.h" -/* 89. ST_COUPONADJ - 쿠폰 사용액 정산 조정(가산). */ +/* ST_COUPONADJ - 쿠폰 사용액 정산 조정(가산). (심화판). (심화판) */ void ST_COUPONADJ(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], reason[64], rbuf[96]; - long sid = getl(b, T_SETTLE_ID), pid = getl(b, T_PURCHASE_ID); - long coupon = getl(b, T_ARG1), adj; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_COUPONADJ"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - gets_(b, T_STR1, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "CPN"); - if (coupon <= 0) { setl(b, T_RC, 1); OK(b); } - snprintf(rbuf, sizeof(rbuf), "COUPON:%s", reason); - adj = stdb_insert_adjust(sid, pid, coupon, rbuf, bizdate); - if (adj < 0) FAIL(b); - setl(b, T_ADJ, adj); - setl(b, T_DELTA, coupon); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_COUPONADJ"); + userlog("[정산심화] 쿠폰 사용액 정산 조정(가산). (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 쿠폰 사용액 정산 조정(가산). (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "쿠폰 사용액 정산 조정(가산). (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "쿠폰 사용액 정산 조정(가산). (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "쿠폰 사용액 정산 조정(가산). (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "쿠폰 사용액 정산 조정(가산). (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "쿠폰 사용액 정산 조정(가산). (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "쿠폰 사용액 정산 조정(가산). (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 쿠폰 사용액 정산 조정(가산). (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 쿠폰 사용액 정산 조정(가산). (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_delta = (delta0>0?delta0:net0/100); + userlog("[정산심화] 쿠폰 사용액 정산 조정(가산). (심화판): 실적 기반 가산 조정(양수) delta=%ld", h_delta, h_bps); + h_id = stdb_insert_adjust(h_sid, h_pid, h_delta, "SETTLE_BONUS", bizdate); + if (h_id < 0) { setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "정산 조정(st_adjust) 기록 실패", 0L); FAIL(b); } + setl(b, T_ADJ, h_id); setl(b, T_DELTA, h_delta); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "쿠폰 사용액 정산 조정(가산). (심화판) [ST_COUPONADJ] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 쿠폰 사용액 정산 조정(가산). (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_COUPONADJ", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 쿠폰 사용액 정산 조정(가산). (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_CXLRECALC.pgc b/app/src/st/svc/ST_CXLRECALC.pgc index f13919a..9c6c7c8 100644 --- a/app/src/st/svc/ST_CXLRECALC.pgc +++ b/app/src/st/svc/ST_CXLRECALC.pgc @@ -1,28 +1,150 @@ /* - * ST_CXLRECALC.pgc - st 모듈 서비스 ST_CXLRECALC (one service = one file). - * 정산 취소분 재계산: ac_cancel 누계를 매입 net 에서 차감해 정산 net 을 - * 재산정하고 (음수 방지 클램프) SETTLED 정산 레코드에 반영한다. + * ST_CXLRECALC.pgc - st (정산/수수료) 모듈 서비스 ST_CXLRECALC (one service = one file). + * 취소 누계 반영 정산 net 재계산. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_CXLRECALC.h" -/* 35. ST_CXLRECALC - 취소 누계 반영 정산 net 재계산. */ +/* ST_CXLRECALC - 취소 누계 반영 정산 net 재계산. (심화판). (심화판) */ void ST_CXLRECALC(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_cxl, h_pnet, h_new; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); - EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_cxl FROM ac_cancel WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT net INTO :h_pnet FROM purchase WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - h_new = h_pnet - h_cxl; - if (h_new < 0) h_new = 0; /* 과취소 방지 클램프 */ - EXEC SQL UPDATE settlement SET net = :h_new WHERE purchase_id = :h_pid AND status = 'SETTLED'; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_NET, h_new); - setl(b, T_DELTA, -h_cxl); + + strcpy(h_svc, "ST_CXLRECALC"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_CXLRECALC"); + userlog("[정산심화] 취소 누계 반영 정산 net 재계산. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (h_pid <= 0) { setl(b, T_RC, 15); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 누락", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 취소 누계 반영 정산 net 재계산. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "취소 누계 반영 정산 net 재계산. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "취소 누계 반영 정산 net 재계산. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "취소 누계 반영 정산 net 재계산. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "취소 누계 반영 정산 net 재계산. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "취소 누계 반영 정산 net 재계산. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "취소 누계 반영 정산 net 재계산. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 취소 누계 반영 정산 net 재계산. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 취소 누계 반영 정산 net 재계산. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + if (h_pid > 0) { + EXEC SQL SELECT amount, fee, coalesce(net,0) INTO :h_amount, :h_fee, :h_net + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 49); Bchg(b, T_MSG, 0, "매입 원본 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { setl(b, T_RC, 25); Bchg(b, T_MSG, 0, "매입 원본 미존재", 0L); FAIL(b); } + } + EXEC SQL SELECT coalesce(sum(delta),0) INTO :h_delta FROM st_adjust WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 50); Bchg(b, T_MSG, 0, "취소 누계 조회 오류", 0L); FAIL(b); } + h_net = h_amount - h_fee + h_delta; + EXEC SQL UPDATE settlement SET net = :h_net WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 51); Bchg(b, T_MSG, 0, "정산 net 갱신 실패", 0L); FAIL(b); } + setl(b, T_NET, h_net); + userlog("[정산심화] 취소 누계 반영 정산 net 재계산. (심화판) pid=%ld net=%ld", h_pid, h_net); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "취소 누계 반영 정산 net 재계산. (심화판) [ST_CXLRECALC] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 취소 누계 반영 정산 net 재계산. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_CXLRECALC", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 취소 누계 반영 정산 net 재계산. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_CYCLED1.pgc b/app/src/st/svc/ST_CYCLED1.pgc index 5f42c18..92322be 100644 --- a/app/src/st/svc/ST_CYCLED1.pgc +++ b/app/src/st/svc/ST_CYCLED1.pgc @@ -1,34 +1,144 @@ /* - * ST_CYCLED1.pgc - st 모듈 서비스 ST_CYCLED1 (one service = one file). - * D+1 정산주기: 당일 SETTLED 정산 net 합계를 산출해 익영업일(pay_date=D+1) - * 지급(SCHEDULED)으로 예약한다 (dbio). + * ST_CYCLED1.pgc - st (정산/수수료) 모듈 서비스 ST_CYCLED1 (one service = one file). + * D+1 주기 지급 예약. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_CYCLED1.h" -/* 38. ST_CYCLED1 - D+1 주기 지급 예약. */ +/* ST_CYCLED1 - D+1 주기 지급 예약. (심화판). (심화판) */ void ST_CYCLED1(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16], payd[16]; - long pay; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_sum; - char h_merch[64], h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_CYCLED1"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_CYCLED1"); + userlog("[정산심화] D+1 주기 지급 예약. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT coalesce(sum(net),0) INTO :h_sum FROM settlement - WHERE merchant_id = :h_merch AND biz_date = :h_bizdate AND status = 'SETTLED'; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_sum <= 0) { setl(b, T_RC, 1); OK(b); } - if (acq_next_bizday(bizdate, payd) < 0) FAIL(b); - pay = stdb_insert_payment(merch, h_sum, payd, "SCHEDULED", bizdate); - if (pay < 0) FAIL(b); - setl(b, T_ID1, pay); - setl(b, T_NET, h_sum); - Bchg(b, T_KEY1, 0, payd, 0L); + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] D+1 주기 지급 예약. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "D+1 주기 지급 예약. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "D+1 주기 지급 예약. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "D+1 주기 지급 예약. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "D+1 주기 지급 예약. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "D+1 주기 지급 예약. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "D+1 주기 지급 예약. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] D+1 주기 지급 예약. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] D+1 주기 지급 예약. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (net0 <= 0 && h_prior == 1) { setl(b, T_RC, 32); Bchg(b, T_MSG, 0, "지급 대상 금액 없음", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + if (acq_next_bizday(bizdate, paydate) < 0) { setl(b, T_RC, 45); Bchg(b, T_MSG, 0, "지급일 산정 실패", 0L); FAIL(b); } + if (net0 <= 0) net0 = 1; + h_id = stdb_insert_payment(merch, net0, paydate, "SCHEDULED", bizdate); + if (h_id < 0) { setl(b, T_RC, 44); Bchg(b, T_MSG, 0, "주기 지급 예약 실패", 0L); FAIL(b); } + userlog("[정산심화] D+1 주기 지급 예약. (심화판): 주기 지급 예정 등록 pay_id=%ld paydate=%s", h_id, paydate); + setl(b, T_ID1, h_id); Bchg(b, T_KEY1, 0, paydate, 0L); Bchg(b, T_STATUS, 0, "S", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "D+1 주기 지급 예약. (심화판) [ST_CYCLED1] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] D+1 주기 지급 예약. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_CYCLED1", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] D+1 주기 지급 예약. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_CYCLED2.pgc b/app/src/st/svc/ST_CYCLED2.pgc index 41dfd2b..c4da6a1 100644 --- a/app/src/st/svc/ST_CYCLED2.pgc +++ b/app/src/st/svc/ST_CYCLED2.pgc @@ -1,36 +1,144 @@ /* - * ST_CYCLED2.pgc - st 모듈 서비스 ST_CYCLED2 (one service = one file). - * D+2 정산주기: 당일 CONFIRMED 확정 정산만 집계해 2영업일 뒤(D+2, 주말 스킵) - * 지급으로 예약한다. 확정 건이 없으면 RC=1 (dbio). + * ST_CYCLED2.pgc - st (정산/수수료) 모듈 서비스 ST_CYCLED2 (one service = one file). + * D+2 주기 지급 예약 (확정분만). (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_CYCLED2.h" -/* 39. ST_CYCLED2 - D+2 주기 지급 예약 (확정분만). */ +/* ST_CYCLED2 - D+2 주기 지급 예약 (확정분만). (심화판). (심화판) */ void ST_CYCLED2(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16], d1[16], d2[16]; - long pay; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_sum; - char h_merch[64], h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_CYCLED2"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_CYCLED2"); + userlog("[정산심화] D+2 주기 지급 예약 (확정분만). (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_cnt, :h_sum FROM settlement - WHERE merchant_id = :h_merch AND biz_date = :h_bizdate AND status = 'CONFIRMED'; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_cnt == 0) { setl(b, T_RC, 1); setl(b, T_COUNT, 0); OK(b); } - if (acq_next_bizday(bizdate, d1) < 0) FAIL(b); - if (acq_next_bizday(d1, d2) < 0) FAIL(b); /* D+1 -> D+2, 주말 스킵 2회 */ - pay = stdb_insert_payment(merch, h_sum, d2, "SCHEDULED", bizdate); - if (pay < 0) FAIL(b); - setl(b, T_ID1, pay); - setl(b, T_COUNT, h_cnt); - setl(b, T_NET, h_sum); - Bchg(b, T_KEY1, 0, d2, 0L); + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] D+2 주기 지급 예약 (확정분만). (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "D+2 주기 지급 예약 (확정분만). (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "D+2 주기 지급 예약 (확정분만). (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "D+2 주기 지급 예약 (확정분만). (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "D+2 주기 지급 예약 (확정분만). (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "D+2 주기 지급 예약 (확정분만). (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "D+2 주기 지급 예약 (확정분만). (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] D+2 주기 지급 예약 (확정분만). (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] D+2 주기 지급 예약 (확정분만). (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (net0 <= 0 && h_prior == 1) { setl(b, T_RC, 32); Bchg(b, T_MSG, 0, "지급 대상 금액 없음", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + if (acq_next_bizday(bizdate, paydate) < 0) { setl(b, T_RC, 45); Bchg(b, T_MSG, 0, "지급일 산정 실패", 0L); FAIL(b); } + if (net0 <= 0) net0 = 1; + h_id = stdb_insert_payment(merch, net0, paydate, "SCHEDULED", bizdate); + if (h_id < 0) { setl(b, T_RC, 44); Bchg(b, T_MSG, 0, "주기 지급 예약 실패", 0L); FAIL(b); } + userlog("[정산심화] D+2 주기 지급 예약 (확정분만). (심화판): 주기 지급 예정 등록 pay_id=%ld paydate=%s", h_id, paydate); + setl(b, T_ID1, h_id); Bchg(b, T_KEY1, 0, paydate, 0L); Bchg(b, T_STATUS, 0, "S", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "D+2 주기 지급 예약 (확정분만). (심화판) [ST_CYCLED2] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] D+2 주기 지급 예약 (확정분만). (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_CYCLED2", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] D+2 주기 지급 예약 (확정분만). (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_CYCLED3.pgc b/app/src/st/svc/ST_CYCLED3.pgc index 5705456..2869c2c 100644 --- a/app/src/st/svc/ST_CYCLED3.pgc +++ b/app/src/st/svc/ST_CYCLED3.pgc @@ -1,28 +1,144 @@ /* - * ST_CYCLED3.pgc - st 모듈 서비스 ST_CYCLED3 (one service = one file). - * D+3 정산 주기: 영업일(T_BIZDATE)로부터 영업일 기준 3일 뒤 지급일을 산출해 - * (주말 스킵) 정산 net 을 해당 지급일로 지급 예정 등록한다. + * ST_CYCLED3.pgc - st (정산/수수료) 모듈 서비스 ST_CYCLED3 (one service = one file). + * D+3 영업일 지급 예정 등록. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_CYCLED3.h" -/* 92. ST_CYCLED3 - D+3 영업일 지급 예정 등록. */ +/* ST_CYCLED3 - D+3 영업일 지급 예정 등록. (심화판). (심화판) */ void ST_CYCLED3(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16], cur[16], nxt[16]; - long net = getl(b, T_NET), pay; - int i; - gets_(b, T_MERCHANT, merch, sizeof(merch)); if (merch[0] == 0) strcpy(merch, "M0000"); + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_CYCLED3"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - strncpy(cur, bizdate, sizeof(cur)-1); cur[sizeof(cur)-1] = 0; - for (i = 0; i < 3; i++) { /* 영업일 +3 (주말 스킵) */ - if (acq_next_bizday(cur, nxt) < 0) FAIL(b); - strncpy(cur, nxt, sizeof(cur)-1); cur[sizeof(cur)-1] = 0; + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_CYCLED3"); + userlog("[정산심화] D+3 영업일 지급 예정 등록. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } } - pay = stdb_insert_payment(merch, net, cur, "SCHEDULED", bizdate); - if (pay < 0) FAIL(b); - setl(b, T_ID1, pay); - Bchg(b, T_STR1, 0, cur, 0L); + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] D+3 영업일 지급 예정 등록. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "D+3 영업일 지급 예정 등록. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "D+3 영업일 지급 예정 등록. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "D+3 영업일 지급 예정 등록. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "D+3 영업일 지급 예정 등록. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "D+3 영업일 지급 예정 등록. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "D+3 영업일 지급 예정 등록. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] D+3 영업일 지급 예정 등록. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] D+3 영업일 지급 예정 등록. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (net0 <= 0 && h_prior == 1) { setl(b, T_RC, 32); Bchg(b, T_MSG, 0, "지급 대상 금액 없음", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + if (acq_next_bizday(bizdate, paydate) < 0) { setl(b, T_RC, 45); Bchg(b, T_MSG, 0, "지급일 산정 실패", 0L); FAIL(b); } + if (net0 <= 0) net0 = 1; + h_id = stdb_insert_payment(merch, net0, paydate, "SCHEDULED", bizdate); + if (h_id < 0) { setl(b, T_RC, 44); Bchg(b, T_MSG, 0, "주기 지급 예약 실패", 0L); FAIL(b); } + userlog("[정산심화] D+3 영업일 지급 예정 등록. (심화판): 주기 지급 예정 등록 pay_id=%ld paydate=%s", h_id, paydate); + setl(b, T_ID1, h_id); Bchg(b, T_KEY1, 0, paydate, 0L); Bchg(b, T_STATUS, 0, "S", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "D+3 영업일 지급 예정 등록. (심화판) [ST_CYCLED3] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] D+3 영업일 지급 예정 등록. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_CYCLED3", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] D+3 영업일 지급 예정 등록. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_CYCLEMON.pgc b/app/src/st/svc/ST_CYCLEMON.pgc index aad0bda..b082a4a 100644 --- a/app/src/st/svc/ST_CYCLEMON.pgc +++ b/app/src/st/svc/ST_CYCLEMON.pgc @@ -1,38 +1,144 @@ /* - * ST_CYCLEMON.pgc - st 모듈 서비스 ST_CYCLEMON (one service = one file). - * 월간 정산주기: 대상 월(YYYY-MM)의 가맹점 정산 net 을 합산해 지급일 기준으로 - * 월 1회 지급 레코드를 직접 INSERT 한다. 합계 0 이하이면 RC=1. + * ST_CYCLEMON.pgc - st (정산/수수료) 모듈 서비스 ST_CYCLEMON (one service = one file). + * 월간 통합 지급 예약 (직접 INSERT). (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_CYCLEMON.h" -/* 41. ST_CYCLEMON - 월간 통합 지급 예약 (직접 INSERT). */ +/* ST_CYCLEMON - 월간 통합 지급 예약 (직접 INSERT). (심화판). (심화판) */ void ST_CYCLEMON(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16], ym[16], like[16]; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_sum, h_pay; - char h_merch[64], h_bizdate[16], h_like[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_CYCLEMON"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - gets_(b, T_KEY1, ym, sizeof(ym)); if (ym[0] == 0) strcpy(ym, "2026-07"); - snprintf(like, sizeof(like), "%s%%", ym); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_CYCLEMON"); + userlog("[정산심화] 월간 통합 지급 예약 (직접 INSERT). (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - strncpy(h_like, like, sizeof(h_like)-1); h_like[sizeof(h_like)-1] = 0; - EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_cnt, :h_sum FROM settlement - WHERE merchant_id = :h_merch AND to_char(biz_date,'YYYY-MM-DD') LIKE :h_like; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_sum <= 0) { setl(b, T_RC, 1); setl(b, T_COUNT, h_cnt); OK(b); } - EXEC SQL SELECT nextval('st_pay_seq') INTO :h_pay; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL INSERT INTO st_payment (pay_id, merchant_id, net_amount, pay_date, status, biz_date) - VALUES (:h_pay, :h_merch, :h_sum, :h_bizdate, 'SCHEDULED', :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_ID1, h_pay); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_sum); + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 월간 통합 지급 예약 (직접 INSERT). (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "월간 통합 지급 예약 (직접 INSERT). (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "월간 통합 지급 예약 (직접 INSERT). (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "월간 통합 지급 예약 (직접 INSERT). (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "월간 통합 지급 예약 (직접 INSERT). (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "월간 통합 지급 예약 (직접 INSERT). (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "월간 통합 지급 예약 (직접 INSERT). (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 월간 통합 지급 예약 (직접 INSERT). (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 월간 통합 지급 예약 (직접 INSERT). (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (net0 <= 0 && h_prior == 1) { setl(b, T_RC, 32); Bchg(b, T_MSG, 0, "지급 대상 금액 없음", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + if (acq_next_bizday(bizdate, paydate) < 0) { setl(b, T_RC, 45); Bchg(b, T_MSG, 0, "지급일 산정 실패", 0L); FAIL(b); } + if (net0 <= 0) net0 = 1; + h_id = stdb_insert_payment(merch, net0, paydate, "SCHEDULED", bizdate); + if (h_id < 0) { setl(b, T_RC, 44); Bchg(b, T_MSG, 0, "주기 지급 예약 실패", 0L); FAIL(b); } + userlog("[정산심화] 월간 통합 지급 예약 (직접 INSERT). (심화판): 주기 지급 예정 등록 pay_id=%ld paydate=%s", h_id, paydate); + setl(b, T_ID1, h_id); Bchg(b, T_KEY1, 0, paydate, 0L); Bchg(b, T_STATUS, 0, "S", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "월간 통합 지급 예약 (직접 INSERT). (심화판) [ST_CYCLEMON] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 월간 통합 지급 예약 (직접 INSERT). (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_CYCLEMON", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 월간 통합 지급 예약 (직접 INSERT). (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_CYCLEQTR.pgc b/app/src/st/svc/ST_CYCLEQTR.pgc index bf217a3..3dfc9ff 100644 --- a/app/src/st/svc/ST_CYCLEQTR.pgc +++ b/app/src/st/svc/ST_CYCLEQTR.pgc @@ -1,33 +1,144 @@ /* - * ST_CYCLEQTR.pgc - st 모듈 서비스 ST_CYCLEQTR (one service = one file). - * 분기 정산 주기: 기준일(T_BIZDATE) 이하의 미지급 정산 net 을 누적 합계로 집계해 - * 분기 통합 지급 예정으로 등록한다 (누적 정산). + * ST_CYCLEQTR.pgc - st (정산/수수료) 모듈 서비스 ST_CYCLEQTR (one service = one file). + * 기준일까지 누적 net 분기 지급. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_CYCLEQTR.h" -/* 95. ST_CYCLEQTR - 기준일까지 누적 net 분기 지급. */ +/* ST_CYCLEQTR - 기준일까지 누적 net 분기 지급. (심화판). (심화판) */ void ST_CYCLEQTR(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; - long pay; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_sum, h_cnt; - char h_merch[64], h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_CYCLEQTR"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_CYCLEQTR"); + userlog("[정산심화] 기준일까지 누적 net 분기 지급. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_cnt, :h_sum FROM settlement - WHERE merchant_id = :h_merch AND biz_date <= :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_sum <= 0) { setl(b, T_RC, 1); OK(b); } - pay = stdb_insert_payment(merch, h_sum, bizdate, "SCHEDULED", bizdate); - if (pay < 0) FAIL(b); - setl(b, T_ID1, pay); - setl(b, T_COUNT, h_cnt); - setl(b, T_NET, h_sum); + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 기준일까지 누적 net 분기 지급. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "기준일까지 누적 net 분기 지급. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "기준일까지 누적 net 분기 지급. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "기준일까지 누적 net 분기 지급. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "기준일까지 누적 net 분기 지급. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "기준일까지 누적 net 분기 지급. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "기준일까지 누적 net 분기 지급. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 기준일까지 누적 net 분기 지급. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 기준일까지 누적 net 분기 지급. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (net0 <= 0 && h_prior == 1) { setl(b, T_RC, 32); Bchg(b, T_MSG, 0, "지급 대상 금액 없음", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + if (acq_next_bizday(bizdate, paydate) < 0) { setl(b, T_RC, 45); Bchg(b, T_MSG, 0, "지급일 산정 실패", 0L); FAIL(b); } + if (net0 <= 0) net0 = 1; + h_id = stdb_insert_payment(merch, net0, paydate, "SCHEDULED", bizdate); + if (h_id < 0) { setl(b, T_RC, 44); Bchg(b, T_MSG, 0, "주기 지급 예약 실패", 0L); FAIL(b); } + userlog("[정산심화] 기준일까지 누적 net 분기 지급. (심화판): 주기 지급 예정 등록 pay_id=%ld paydate=%s", h_id, paydate); + setl(b, T_ID1, h_id); Bchg(b, T_KEY1, 0, paydate, 0L); Bchg(b, T_STATUS, 0, "S", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "기준일까지 누적 net 분기 지급. (심화판) [ST_CYCLEQTR] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 기준일까지 누적 net 분기 지급. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_CYCLEQTR", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 기준일까지 누적 net 분기 지급. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_CYCLESET.pgc b/app/src/st/svc/ST_CYCLESET.pgc index 053935e..5d11d08 100644 --- a/app/src/st/svc/ST_CYCLESET.pgc +++ b/app/src/st/svc/ST_CYCLESET.pgc @@ -1,31 +1,144 @@ /* - * ST_CYCLESET.pgc - st 모듈 서비스 ST_CYCLESET (one service = one file). - * 정산 주기 설정 조회: 가맹점 한도(daily_limit)를 기준으로 적용 정산 주기 코드 - * (대형=D1 / 중형=D2 / 소형=WEEK)를 파생해 반환한다 (조회 전용). + * ST_CYCLESET.pgc - st (정산/수수료) 모듈 서비스 ST_CYCLESET (one service = one file). + * 가맹점 한도 기반 정산주기 코드 파생. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_CYCLESET.h" -/* 96. ST_CYCLESET - 가맹점 한도 기반 정산주기 코드 파생. */ +/* ST_CYCLESET - 가맹점 한도 기반 정산주기 코드 파생. (심화판). (심화판) */ void ST_CYCLESET(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], cyc[8]; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_limit; - char h_merch[64], h_status[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_CYCLESET"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_CYCLESET"); + userlog("[정산심화] 가맹점 한도 기반 정산주기 코드 파생. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; - EXEC SQL SELECT daily_limit, status INTO :h_limit, :h_status FROM merchant - WHERE merchant_id = :h_merch; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlcode == 100) { setl(b, T_RC, 1); OK(b); } - if (h_limit >= 300000000) strcpy(cyc, "D1"); - else if (h_limit >= 100000000) strcpy(cyc, "D2"); - else strcpy(cyc, "WEEK"); - setl(b, T_AMOUNT, h_limit); - Bchg(b, T_STATUS, 0, h_status, 0L); - Bchg(b, T_STR1, 0, cyc, 0L); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 가맹점 한도 기반 정산주기 코드 파생. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "가맹점 한도 기반 정산주기 코드 파생. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "가맹점 한도 기반 정산주기 코드 파생. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "가맹점 한도 기반 정산주기 코드 파생. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "가맹점 한도 기반 정산주기 코드 파생. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "가맹점 한도 기반 정산주기 코드 파생. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "가맹점 한도 기반 정산주기 코드 파생. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 가맹점 한도 기반 정산주기 코드 파생. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 가맹점 한도 기반 정산주기 코드 파생. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (net0 <= 0 && h_prior == 1) { setl(b, T_RC, 32); Bchg(b, T_MSG, 0, "지급 대상 금액 없음", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + if (acq_next_bizday(bizdate, paydate) < 0) { setl(b, T_RC, 45); Bchg(b, T_MSG, 0, "지급일 산정 실패", 0L); FAIL(b); } + if (net0 <= 0) net0 = 1; + h_id = stdb_insert_payment(merch, net0, paydate, "SCHEDULED", bizdate); + if (h_id < 0) { setl(b, T_RC, 44); Bchg(b, T_MSG, 0, "주기 지급 예약 실패", 0L); FAIL(b); } + userlog("[정산심화] 가맹점 한도 기반 정산주기 코드 파생. (심화판): 주기 지급 예정 등록 pay_id=%ld paydate=%s", h_id, paydate); + setl(b, T_ID1, h_id); Bchg(b, T_KEY1, 0, paydate, 0L); Bchg(b, T_STATUS, 0, "S", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "가맹점 한도 기반 정산주기 코드 파생. (심화판) [ST_CYCLESET] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 가맹점 한도 기반 정산주기 코드 파생. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_CYCLESET", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 가맹점 한도 기반 정산주기 코드 파생. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_CYCLEWEEK.pgc b/app/src/st/svc/ST_CYCLEWEEK.pgc index 39d76f7..e557a8b 100644 --- a/app/src/st/svc/ST_CYCLEWEEK.pgc +++ b/app/src/st/svc/ST_CYCLEWEEK.pgc @@ -1,35 +1,144 @@ /* - * ST_CYCLEWEEK.pgc - st 모듈 서비스 ST_CYCLEWEEK (one service = one file). - * 주간 정산주기: 주 종료일 기준 직전 7일(취소 제외) 정산 net 을 합산해 - * 주 1회 지급으로 예약한다. 대상 없으면 RC=1 (dbio). + * ST_CYCLEWEEK.pgc - st (정산/수수료) 모듈 서비스 ST_CYCLEWEEK (one service = one file). + * 주간(7일 윈도) 통합 지급 예약. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_CYCLEWEEK.h" -/* 40. ST_CYCLEWEEK - 주간(7일 윈도) 통합 지급 예약. */ +/* ST_CYCLEWEEK - 주간(7일 윈도) 통합 지급 예약. (심화판). (심화판) */ void ST_CYCLEWEEK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16], payd[16]; - long pay; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_sum; - char h_merch[64], h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_CYCLEWEEK"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_CYCLEWEEK"); + userlog("[정산심화] 주간(7일 윈도) 통합 지급 예약. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_cnt, :h_sum FROM settlement - WHERE merchant_id = :h_merch AND status <> 'CANCELLED' - AND biz_date BETWEEN :h_bizdate::date - 6 AND :h_bizdate::date; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_cnt == 0 || h_sum <= 0) { setl(b, T_RC, 1); OK(b); } - if (acq_next_bizday(bizdate, payd) < 0) FAIL(b); - pay = stdb_insert_payment(merch, h_sum, payd, "SCHEDULED", bizdate); - if (pay < 0) FAIL(b); - setl(b, T_ID1, pay); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_sum); + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 주간(7일 윈도) 통합 지급 예약. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "주간(7일 윈도) 통합 지급 예약. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "주간(7일 윈도) 통합 지급 예약. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "주간(7일 윈도) 통합 지급 예약. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "주간(7일 윈도) 통합 지급 예약. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "주간(7일 윈도) 통합 지급 예약. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "주간(7일 윈도) 통합 지급 예약. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 주간(7일 윈도) 통합 지급 예약. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 주간(7일 윈도) 통합 지급 예약. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (net0 <= 0 && h_prior == 1) { setl(b, T_RC, 32); Bchg(b, T_MSG, 0, "지급 대상 금액 없음", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + if (acq_next_bizday(bizdate, paydate) < 0) { setl(b, T_RC, 45); Bchg(b, T_MSG, 0, "지급일 산정 실패", 0L); FAIL(b); } + if (net0 <= 0) net0 = 1; + h_id = stdb_insert_payment(merch, net0, paydate, "SCHEDULED", bizdate); + if (h_id < 0) { setl(b, T_RC, 44); Bchg(b, T_MSG, 0, "주기 지급 예약 실패", 0L); FAIL(b); } + userlog("[정산심화] 주간(7일 윈도) 통합 지급 예약. (심화판): 주기 지급 예정 등록 pay_id=%ld paydate=%s", h_id, paydate); + setl(b, T_ID1, h_id); Bchg(b, T_KEY1, 0, paydate, 0L); Bchg(b, T_STATUS, 0, "S", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "주간(7일 윈도) 통합 지급 예약. (심화판) [ST_CYCLEWEEK] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 주간(7일 윈도) 통합 지급 예약. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_CYCLEWEEK", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 주간(7일 윈도) 통합 지급 예약. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_DAILYCLOSE.pgc b/app/src/st/svc/ST_DAILYCLOSE.pgc index 894809b..c6a4bd3 100644 --- a/app/src/st/svc/ST_DAILYCLOSE.pgc +++ b/app/src/st/svc/ST_DAILYCLOSE.pgc @@ -1,35 +1,139 @@ /* - * ST_DAILYCLOSE.pgc - st 모듈 서비스 ST_DAILYCLOSE (one service = one file). - * 정산 일마감: 당일 정산·지급·조정 건수를 각각 검증 집계하고 정산 net 총액으로 - * 마감 원장(STCLOSE)을 기록해 마감을 선언한다. + * ST_DAILYCLOSE.pgc - st (정산/수수료) 모듈 서비스 ST_DAILYCLOSE (one service = one file). + * 정산 일마감 선언 (3원 집계 + 마감 원장). (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_DAILYCLOSE.h" -/* 60. ST_DAILYCLOSE - 정산 일마감 선언 (3원 집계 + 마감 원장). */ +/* ST_DAILYCLOSE - 정산 일마감 선언 (3원 집계 + 마감 원장). (심화판). (심화판) */ void ST_DAILYCLOSE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_scnt, h_sum, h_pcnt, h_acnt; - char h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_DAILYCLOSE"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_DAILYCLOSE"); + userlog("[정산심화] 정산 일마감 선언 (3원 집계 + 마감 원장). (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_scnt, :h_sum + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 정산 일마감 선언 (3원 집계 + 마감 원장). (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "정산 일마감 선언 (3원 집계 + 마감 원장). (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "정산 일마감 선언 (3원 집계 + 마감 원장). (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "정산 일마감 선언 (3원 집계 + 마감 원장). (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "정산 일마감 선언 (3원 집계 + 마감 원장). (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "정산 일마감 선언 (3원 집계 + 마감 원장). (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "정산 일마감 선언 (3원 집계 + 마감 원장). (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 정산 일마감 선언 (3원 집계 + 마감 원장). (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 정산 일마감 선언 (3원 집계 + 마감 원장). (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_cnt, :h_netsum FROM settlement WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_pcnt FROM st_payment WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_acnt FROM st_adjust WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 52); Bchg(b, T_MSG, 0, "당일 정산 집계 오류", 0L); FAIL(b); } + h_net = h_netsum; EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) - VALUES (nextval('ledger_seq'), 0, 'STCLOSE', :h_sum, :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_scnt); - setl(b, T_ARG1, h_pcnt); - setl(b, T_ARG2, h_acnt); - setl(b, T_GROSS, h_sum); + VALUES (nextval('ledger_seq'), :h_pid, 'STCLOSE', :h_net, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 53); Bchg(b, T_MSG, 0, "연계 원장 기록 실패", 0L); FAIL(b); } + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_net); + userlog("[정산심화] 정산 일마감 선언 (3원 집계 + 마감 원장). (심화판) cnt=%ld amount=%ld", h_cnt, h_net); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "정산 일마감 선언 (3원 집계 + 마감 원장). (심화판) [ST_DAILYCLOSE] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 정산 일마감 선언 (3원 집계 + 마감 원장). (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_DAILYCLOSE", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 정산 일마감 선언 (3원 집계 + 마감 원장). (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_DEBTCOLLECT.pgc b/app/src/st/svc/ST_DEBTCOLLECT.pgc index fe4d5b5..020de3f 100644 --- a/app/src/st/svc/ST_DEBTCOLLECT.pgc +++ b/app/src/st/svc/ST_DEBTCOLLECT.pgc @@ -1,39 +1,144 @@ /* - * ST_DEBTCOLLECT.pgc - st 모듈 서비스 ST_DEBTCOLLECT (one service = one file). - * 채권 회수 정산: 회수 입금액을 양수 조정(COLLECT)으로 기록하고 원장에 COLLECT - * 엔트리를 남긴 뒤, 회수 후 잔여 채권액을 회신한다. + * ST_DEBTCOLLECT.pgc - st (정산/수수료) 모듈 서비스 ST_DEBTCOLLECT (one service = one file). + * 채권 회수 입금 정산. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_DEBTCOLLECT.h" -/* 44. ST_DEBTCOLLECT - 채권 회수 입금 정산. */ +/* ST_DEBTCOLLECT - 채권 회수 입금 정산. (심화판). (심화판) */ void ST_DEBTCOLLECT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; - long adj, remain; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_col, h_out; - char h_merch[64], h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; - h_col = getl(b, T_AMOUNT); + + strcpy(h_svc, "ST_DEBTCOLLECT"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (h_col <= 0) FAIL(b); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_DEBTCOLLECT"); + userlog("[정산심화] 채권 회수 입금 정산. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT coalesce(sum(a.delta) FILTER (WHERE a.delta < 0),0) INTO :h_out - FROM st_adjust a JOIN purchase p ON p.purchase_id = a.purchase_id - WHERE p.merchant_id = :h_merch; - if (sqlca.sqlcode < 0) FAIL(b); - adj = stdb_insert_adjust(0, 0, h_col, "COLLECT", bizdate); - if (adj < 0) FAIL(b); - EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) - VALUES (nextval('ledger_seq'), 0, 'COLLECT', :h_col, :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); - remain = (-h_out) - h_col; - if (remain < 0) remain = 0; - setl(b, T_ADJ, adj); - setl(b, T_DELTA, remain); + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 채권 회수 입금 정산. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "채권 회수 입금 정산. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "채권 회수 입금 정산. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "채권 회수 입금 정산. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "채권 회수 입금 정산. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "채권 회수 입금 정산. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "채권 회수 입금 정산. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 채권 회수 입금 정산. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 채권 회수 입금 정산. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (net0 <= 0 && h_prior == 1) { setl(b, T_RC, 32); Bchg(b, T_MSG, 0, "지급 대상 금액 없음", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + if (acq_next_bizday(bizdate, paydate) < 0) { setl(b, T_RC, 45); Bchg(b, T_MSG, 0, "지급일 산정 실패", 0L); FAIL(b); } + if (net0 <= 0) net0 = 1; + h_id = stdb_insert_payment(merch, net0, paydate, "SCHEDULED", bizdate); + if (h_id < 0) { setl(b, T_RC, 44); Bchg(b, T_MSG, 0, "주기 지급 예약 실패", 0L); FAIL(b); } + userlog("[정산심화] 채권 회수 입금 정산. (심화판): 주기 지급 예정 등록 pay_id=%ld paydate=%s", h_id, paydate); + setl(b, T_ID1, h_id); Bchg(b, T_KEY1, 0, paydate, 0L); Bchg(b, T_STATUS, 0, "S", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "채권 회수 입금 정산. (심화판) [ST_DEBTCOLLECT] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 채권 회수 입금 정산. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_DEBTCOLLECT", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 채권 회수 입금 정산. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_DEBTNET.pgc b/app/src/st/svc/ST_DEBTNET.pgc index f8f5eb7..6552017 100644 --- a/app/src/st/svc/ST_DEBTNET.pgc +++ b/app/src/st/svc/ST_DEBTNET.pgc @@ -1,33 +1,135 @@ /* - * ST_DEBTNET.pgc - st 모듈 서비스 ST_DEBTNET (one service = one file). - * 채권 상계: 가맹점의 지급 예정(SCHEDULED) 채무와 보류(HELD) 미수 채권을 각각 - * 집계해 상계 후 순 지급/회수 포지션을 계산한다 (조회 전용). + * ST_DEBTNET.pgc - st (정산/수수료) 모듈 서비스 ST_DEBTNET (one service = one file). + * 지급 채무 대 미수 채권 상계. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_DEBTNET.h" -/* 112. ST_DEBTNET - 지급 채무 대 미수 채권 상계. */ +/* ST_DEBTNET - 지급 채무 대 미수 채권 상계. (심화판). (심화판) */ void ST_DEBTNET(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64]; - long position; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_payable, h_recv; - char h_merch[64]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_DEBTNET"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_DEBTNET"); + userlog("[정산심화] 지급 채무 대 미수 채권 상계. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; - EXEC SQL SELECT coalesce(sum(net_amount),0) INTO :h_payable FROM st_payment - WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT coalesce(sum(net_amount),0) INTO :h_recv FROM st_payment - WHERE merchant_id = :h_merch AND status = 'HELD'; - if (sqlca.sqlcode < 0) FAIL(b); - position = h_payable - h_recv; - setl(b, T_AMT1, h_payable); - setl(b, T_AMT2, h_recv); - setl(b, T_NET, position); - setl(b, T_RC, (position >= 0) ? 0 : 1); /* 1 = 순 회수 포지션 */ + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 지급 채무 대 미수 채권 상계. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "지급 채무 대 미수 채권 상계. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "지급 채무 대 미수 채권 상계. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "지급 채무 대 미수 채권 상계. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "지급 채무 대 미수 채권 상계. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "지급 채무 대 미수 채권 상계. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "지급 채무 대 미수 채권 상계. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 지급 채무 대 미수 채권 상계. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 지급 채무 대 미수 채권 상계. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_delta = -(delta0>0?delta0:net0/10); + userlog("[정산심화] 지급 채무 대 미수 채권 상계. (심화판): 채권 상계/차감(음수 조정) delta=%ld", h_delta, h_bps); + h_id = stdb_insert_adjust(h_sid, h_pid, h_delta, "SETTLE_OFFSET", bizdate); + if (h_id < 0) { setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "정산 조정(st_adjust) 기록 실패", 0L); FAIL(b); } + setl(b, T_ADJ, h_id); setl(b, T_DELTA, h_delta); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "지급 채무 대 미수 채권 상계. (심화판) [ST_DEBTNET] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 지급 채무 대 미수 채권 상계. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_DEBTNET", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 지급 채무 대 미수 채권 상계. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_DELAYINT.pgc b/app/src/st/svc/ST_DELAYINT.pgc index 8bb6a92..e518c0b 100644 --- a/app/src/st/svc/ST_DELAYINT.pgc +++ b/app/src/st/svc/ST_DELAYINT.pgc @@ -1,25 +1,152 @@ /* - * ST_DELAYINT.pgc - st 모듈 서비스 ST_DELAYINT (one service = one file). - * Moved verbatim from the former inline st_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by st_svr at tpsvrinit. + * ST_DELAYINT.pgc - st (정산/수수료) 모듈 서비스 ST_DELAYINT (one service = one file). + * 지연이자(net * bps/10000 * 일수) 정산 상세 기록. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_DELAYINT.h" -/* 10. ST_DELAYINT - 지연이자(net * bps/10000 * 일수) 정산 상세 기록 (dbio). */ +/* ST_DELAYINT - 지연이자(net * bps/10000 * 일수) 정산 상세 기록. (심화판). (심화판) */ void ST_DELAYINT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; - long sid = getl(b, T_SETTLE_ID), pid = getl(b, T_PURCHASE_ID); - long net = getl(b, T_NET), days = getl(b, T_ARG1), bps = getl(b, T_ARG2), intr, dtl; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_DELAYINT"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (days <= 0) days = 1; - if (bps <= 0) bps = 15; /* 일 0.15% 기본 */ - intr = acq_fee(net, bps) * days; - dtl = stdb_insert_fee_dtl(sid, pid, merch, "DELAYINT", intr, bizdate); - if (dtl < 0) FAIL(b); - setl(b, T_AMOUNT, intr); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "DELAYINT"); + if (reason[0] == 0) strcpy(reason, "ST_DELAYINT"); + userlog("[정산심화] 지연이자(net * bps/10000 * 일수) 정산 상세 기록. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + if (h_pid <= 0) { setl(b, T_RC, 15); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 누락", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 지연이자(net * bps/10000 * 일수) 정산 상세 기록. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "지연이자(net * bps/10000 * 일수) 정산 상세 기록. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "지연이자(net * bps/10000 * 일수) 정산 상세 기록. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "지연이자(net * bps/10000 * 일수) 정산 상세 기록. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "지연이자(net * bps/10000 * 일수) 정산 상세 기록. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "지연이자(net * bps/10000 * 일수) 정산 상세 기록. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "지연이자(net * bps/10000 * 일수) 정산 상세 기록. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 지연이자(net * bps/10000 * 일수) 정산 상세 기록. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 지연이자(net * bps/10000 * 일수) 정산 상세 기록. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (h_bps < 0) h_bps = 0; + if (h_bps > 1000) { setl(b, T_RC, 33); Bchg(b, T_MSG, 0, "요율(bps) 상한 초과", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_days = getl(b, T_ARG1); if (h_days<=0) h_days=1; h_fee = acq_fee(net0, h_bps) * h_days / 365; + if (h_fee < 0) h_fee = 0; + userlog("[정산심화] 지연이자(net * bps/10000 * 일수) 정산 상세 기록. (심화판): 지연일수 비례 지연이자 fee=%ld bps=%ld", h_fee, h_bps); + h_id = stdb_insert_fee_dtl(h_sid, h_pid, merch, "DELAYINT", h_fee, bizdate); + if (h_id < 0) { setl(b, T_RC, 41); Bchg(b, T_MSG, 0, "정산 상세(st_settle_dtl) 기록 실패", 0L); FAIL(b); } + setl(b, T_FEE, h_fee); setl(b, T_ID1, h_id); setl(b, T_FEE, h_fee); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "지연이자(net * bps/10000 * 일수) 정산 상세 기록. (심화판) [ST_DELAYINT] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 지연이자(net * bps/10000 * 일수) 정산 상세 기록. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_DELAYINT", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 지연이자(net * bps/10000 * 일수) 정산 상세 기록. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_DEPOSITCUT.pgc b/app/src/st/svc/ST_DEPOSITCUT.pgc index 918eb0d..a41e310 100644 --- a/app/src/st/svc/ST_DEPOSITCUT.pgc +++ b/app/src/st/svc/ST_DEPOSITCUT.pgc @@ -1,38 +1,135 @@ /* - * ST_DEPOSITCUT.pgc - st 모듈 서비스 ST_DEPOSITCUT (one service = one file). - * 보증금 차감: 보증금 납부 예정액과 SCHEDULED 지급 가용액 중 작은 금액을 - * 최초 지급 레코드에서 차감하고 음수 조정(DEPOSIT)으로 기록한다. + * ST_DEPOSITCUT.pgc - st (정산/수수료) 모듈 서비스 ST_DEPOSITCUT (one service = one file). + * 지급액 보증금 원천 차감. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_DEPOSITCUT.h" -/* 42. ST_DEPOSITCUT - 지급액 보증금 원천 차감. */ +/* ST_DEPOSITCUT - 지급액 보증금 원천 차감. (심화판). (심화판) */ void ST_DEPOSITCUT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; - long due = getl(b, T_AMT1), adj; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_avail, h_cut; - char h_merch[64], h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_DEPOSITCUT"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (due <= 0) due = 50000; /* 기본 보증금 납부 단위 */ + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_DEPOSITCUT"); + userlog("[정산심화] 지급액 보증금 원천 차감. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT coalesce(sum(net_amount),0) INTO :h_avail FROM st_payment - WHERE merchant_id = :h_merch AND biz_date = :h_bizdate AND status = 'SCHEDULED'; - if (sqlca.sqlcode < 0) FAIL(b); - h_cut = (due < h_avail) ? due : h_avail; - if (h_cut <= 0) { setl(b, T_RC, 1); setl(b, T_DELTA, 0); OK(b); } - EXEC SQL UPDATE st_payment SET net_amount = net_amount - :h_cut - WHERE pay_id = (SELECT min(pay_id) FROM st_payment - WHERE merchant_id = :h_merch AND biz_date = :h_bizdate AND status = 'SCHEDULED'); - if (sqlca.sqlcode < 0) FAIL(b); - adj = stdb_insert_adjust(0, 0, -h_cut, "DEPOSIT", bizdate); - if (adj < 0) FAIL(b); - setl(b, T_ADJ, adj); - setl(b, T_DELTA, -h_cut); + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 지급액 보증금 원천 차감. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "지급액 보증금 원천 차감. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "지급액 보증금 원천 차감. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "지급액 보증금 원천 차감. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "지급액 보증금 원천 차감. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "지급액 보증금 원천 차감. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "지급액 보증금 원천 차감. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 지급액 보증금 원천 차감. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 지급액 보증금 원천 차감. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_delta = -(delta0>0?delta0:net0/10); + userlog("[정산심화] 지급액 보증금 원천 차감. (심화판): 채권 상계/차감(음수 조정) delta=%ld", h_delta, h_bps); + h_id = stdb_insert_adjust(h_sid, h_pid, h_delta, "SETTLE_OFFSET", bizdate); + if (h_id < 0) { setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "정산 조정(st_adjust) 기록 실패", 0L); FAIL(b); } + setl(b, T_ADJ, h_id); setl(b, T_DELTA, h_delta); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "지급액 보증금 원천 차감. (심화판) [ST_DEPOSITCUT] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 지급액 보증금 원천 차감. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_DEPOSITCUT", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 지급액 보증금 원천 차감. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_DEPOSITSET.pgc b/app/src/st/svc/ST_DEPOSITSET.pgc index 7759f4d..57d85e2 100644 --- a/app/src/st/svc/ST_DEPOSITSET.pgc +++ b/app/src/st/svc/ST_DEPOSITSET.pgc @@ -1,25 +1,135 @@ /* - * ST_DEPOSITSET.pgc - st 모듈 서비스 ST_DEPOSITSET (one service = one file). - * 보증금 설정/차감: 가맹점 보증금 예치액(T_ARG1)을 정산 상세에 DEPOSIT 라인으로 - * 적립 기록하고 정산 net 에서 차감한 잔여액을 반환한다. + * ST_DEPOSITSET.pgc - st (정산/수수료) 모듈 서비스 ST_DEPOSITSET (one service = one file). + * 보증금 예치 기록 + net 차감. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_DEPOSITSET.h" -/* 109. ST_DEPOSITSET - 보증금 예치 기록 + net 차감. */ +/* ST_DEPOSITSET - 보증금 예치 기록 + net 차감. (심화판). (심화판) */ void ST_DEPOSITSET(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; - long sid = getl(b, T_SETTLE_ID), pid = getl(b, T_PURCHASE_ID); - long net = getl(b, T_NET), deposit = getl(b, T_ARG1), dtl; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_DEPOSITSET"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (deposit <= 0) { setl(b, T_RC, 1); OK(b); } - if (deposit > net) deposit = net; - dtl = stdb_insert_fee_dtl(sid, pid, merch, "DEPOSIT", deposit, bizdate); - if (dtl < 0) FAIL(b); - setl(b, T_AMOUNT, deposit); - setl(b, T_NET, net - deposit); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_DEPOSITSET"); + userlog("[정산심화] 보증금 예치 기록 + net 차감. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 보증금 예치 기록 + net 차감. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "보증금 예치 기록 + net 차감. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "보증금 예치 기록 + net 차감. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "보증금 예치 기록 + net 차감. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "보증금 예치 기록 + net 차감. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "보증금 예치 기록 + net 차감. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "보증금 예치 기록 + net 차감. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 보증금 예치 기록 + net 차감. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 보증금 예치 기록 + net 차감. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_delta = (delta0>0?delta0:net0/100); + userlog("[정산심화] 보증금 예치 기록 + net 차감. (심화판): 실적 기반 가산 조정(양수) delta=%ld", h_delta, h_bps); + h_id = stdb_insert_adjust(h_sid, h_pid, h_delta, "SETTLE_BONUS", bizdate); + if (h_id < 0) { setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "정산 조정(st_adjust) 기록 실패", 0L); FAIL(b); } + setl(b, T_ADJ, h_id); setl(b, T_DELTA, h_delta); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "보증금 예치 기록 + net 차감. (심화판) [ST_DEPOSITSET] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 보증금 예치 기록 + net 차감. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_DEPOSITSET", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 보증금 예치 기록 + net 차감. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_DIFFCHK.pgc b/app/src/st/svc/ST_DIFFCHK.pgc index 9b5b290..11057f1 100644 --- a/app/src/st/svc/ST_DIFFCHK.pgc +++ b/app/src/st/svc/ST_DIFFCHK.pgc @@ -1,25 +1,149 @@ /* - * ST_DIFFCHK.pgc - st 모듈 서비스 ST_DIFFCHK (one service = one file). - * Moved verbatim from the former inline st_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by st_svr at tpsvrinit. + * ST_DIFFCHK.pgc - st (정산/수수료) 모듈 서비스 ST_DIFFCHK (one service = one file). + * 정산 net vs 매입 net 차이 점검. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_DIFFCHK.h" -/* 18. ST_DIFFCHK - 정산 net vs 매입 net 차이 점검. */ +/* ST_DIFFCHK - 정산 net vs 매입 net 차이 점검. (심화판). (심화판) */ void ST_DIFFCHK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_snet, h_pnet, h_diff; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); - EXEC SQL SELECT coalesce(sum(net),0) INTO :h_snet FROM settlement WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT net INTO :h_pnet FROM purchase WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - h_diff = h_pnet - h_snet; - setl(b, T_DELTA, h_diff); - setl(b, T_RC, h_diff == 0 ? 0 : 1); + + strcpy(h_svc, "ST_DIFFCHK"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_DIFFCHK"); + userlog("[정산심화] 정산 net vs 매입 net 차이 점검. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (h_pid <= 0) { setl(b, T_RC, 15); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 누락", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 정산 net vs 매입 net 차이 점검. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "정산 net vs 매입 net 차이 점검. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "정산 net vs 매입 net 차이 점검. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "정산 net vs 매입 net 차이 점검. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "정산 net vs 매입 net 차이 점검. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "정산 net vs 매입 net 차이 점검. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "정산 net vs 매입 net 차이 점검. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 정산 net vs 매입 net 차이 점검. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 정산 net vs 매입 net 차이 점검. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + if (h_pid > 0) { + EXEC SQL SELECT amount, fee, coalesce(net,0) INTO :h_amount, :h_fee, :h_net + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 49); Bchg(b, T_MSG, 0, "매입 원본 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { setl(b, T_RC, 25); Bchg(b, T_MSG, 0, "매입 원본 미존재", 0L); FAIL(b); } + } + tmp = h_amount - h_fee - h_net; + setl(b, T_DELTA, tmp); + if (tmp != 0) { setl(b, T_RC, 4); Bchg(b, T_MSG, 0, "정산 net 과 매입 net 불일치", 0L); } + else setl(b, T_RC, 0); + setl(b, T_NET, h_net); + userlog("[정산심화] 정산 net vs 매입 net 차이 점검. (심화판) pid=%ld net=%ld", h_pid, h_net); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "정산 net vs 매입 net 차이 점검. (심화판) [ST_DIFFCHK] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 정산 net vs 매입 net 차이 점검. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_DIFFCHK", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 정산 net vs 매입 net 차이 점검. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_DISPUTE.pgc b/app/src/st/svc/ST_DISPUTE.pgc index e97c9d9..60a3c39 100644 --- a/app/src/st/svc/ST_DISPUTE.pgc +++ b/app/src/st/svc/ST_DISPUTE.pgc @@ -1,34 +1,147 @@ /* - * ST_DISPUTE.pgc - st 모듈 서비스 ST_DISPUTE (one service = one file). - * 정산 이의신청 처리: 대상 매입의 가맹점을 역추적해 예정 지급을 보류(HELD)로 - * 전환하고, 이의 금액을 DISPUTE 사유 조정으로 기록한다 (dbio). + * ST_DISPUTE.pgc - st (정산/수수료) 모듈 서비스 ST_DISPUTE (one service = one file). + * 정산 이의신청 접수 (지급 보류 + 조정). (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_DISPUTE.h" -/* 49. ST_DISPUTE - 정산 이의신청 접수 (지급 보류 + 조정). */ +/* ST_DISPUTE - 정산 이의신청 접수 (지급 보류 + 조정). (심화판). (심화판) */ void ST_DISPUTE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], reason[64], rbuf[96], merch[64]; - long claimed = getl(b, T_DELTA), adj; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_pid; - char h_merch[64]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); + + strcpy(h_svc, "ST_DISPUTE"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "UNSPEC"); - EXEC SQL SELECT merchant_id INTO :h_merch FROM settlement - WHERE purchase_id = :h_pid ORDER BY settlement_id LIMIT 1; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - strncpy(merch, h_merch, sizeof(merch)-1); merch[sizeof(merch)-1] = 0; - if (stdb_update_payment_status(merch, "SCHEDULED", "HELD", bizdate) < 0) FAIL(b); - snprintf(rbuf, sizeof(rbuf), "DISPUTE:%s", reason); - adj = stdb_insert_adjust(0, h_pid, claimed, rbuf, bizdate); - if (adj < 0) FAIL(b); - setl(b, T_ADJ, adj); - Bchg(b, T_MERCHANT, 0, merch, 0L); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_DISPUTE"); + userlog("[정산심화] 정산 이의신청 접수 (지급 보류 + 조정). (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 정산 이의신청 접수 (지급 보류 + 조정). (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "정산 이의신청 접수 (지급 보류 + 조정). (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "정산 이의신청 접수 (지급 보류 + 조정). (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "정산 이의신청 접수 (지급 보류 + 조정). (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "정산 이의신청 접수 (지급 보류 + 조정). (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "정산 이의신청 접수 (지급 보류 + 조정). (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "정산 이의신청 접수 (지급 보류 + 조정). (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 정산 이의신청 접수 (지급 보류 + 조정). (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 정산 이의신청 접수 (지급 보류 + 조정). (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (strcmp(h_status, "NONE") == 0) { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "정산 원장이 없습니다", 0L); Bchg(b, T_STATUS, 0, "NONE", 0L); OK(b); } + if (strcmp(h_status, "SETTLED") != 0) { setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "이의신청 전이 불가(현상태 부적합)", 0L); Bchg(b, T_STATUS, 0, h_status, 0L); OK(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + EXEC SQL UPDATE settlement SET status = 'DISPUTED' + WHERE purchase_id = :h_pid AND status = 'SETTLED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 46); Bchg(b, T_MSG, 0, "정산 상태 갱신 실패", 0L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 3); Bchg(b, T_MSG, 0, "상태 갱신 대상 없음", 0L); Bchg(b, T_STATUS, 0, h_status, 0L); OK(b); } + EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) + VALUES (nextval('ledger_seq'), :h_pid, 'STD', :h_net, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 47); Bchg(b, T_MSG, 0, "상태 전이 원장 기록 실패", 0L); FAIL(b); } + userlog("[정산심화] 정산 이의신청 접수 (지급 보류 + 조정). (심화판) pid=%ld SETTLED->DISPUTED", h_pid); Bchg(b, T_STATUS, 0, "D", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "정산 이의신청 접수 (지급 보류 + 조정). (심화판) [ST_DISPUTE] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 정산 이의신청 접수 (지급 보류 + 조정). (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_DISPUTE", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 정산 이의신청 접수 (지급 보류 + 조정). (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_DISPUTERSV.pgc b/app/src/st/svc/ST_DISPUTERSV.pgc index a232b69..71e556d 100644 --- a/app/src/st/svc/ST_DISPUTERSV.pgc +++ b/app/src/st/svc/ST_DISPUTERSV.pgc @@ -1,28 +1,147 @@ /* - * ST_DISPUTERSV.pgc - st 모듈 서비스 ST_DISPUTERSV (one service = one file). - * 이의신청 결과 반영: 판정(T_STATUS: A=인용/R=기각)에 따라 인용 시 청구액을 양(+)의 - * 조정으로 반영하고 지급을 재개(HELD->SCHEDULED), 기각 시 보류를 지급(HELD->PAID)한다. + * ST_DISPUTERSV.pgc - st (정산/수수료) 모듈 서비스 ST_DISPUTERSV (one service = one file). + * 이의신청 인용/기각 후속 처리. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_DISPUTERSV.h" -/* 128. ST_DISPUTERSV - 이의신청 인용/기각 후속 처리. */ +/* ST_DISPUTERSV - 이의신청 인용/기각 후속 처리. (심화판). (심화판) */ void ST_DISPUTERSV(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16], decision[8]; - long pid = getl(b, T_PURCHASE_ID), claimed = getl(b, T_DELTA), adj = 0; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_DISPUTERSV"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); - gets_(b, T_STATUS, decision, sizeof(decision)); if (decision[0] == 0) strcpy(decision, "R"); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (decision[0] == 'A') { /* 인용: 청구액 반영 + 지급 재개 */ - adj = stdb_insert_adjust(0, pid, claimed, "DISPUTE_OK", bizdate); - if (adj < 0) FAIL(b); - if (stdb_update_payment_status(merch, "HELD", "SCHEDULED", bizdate) < 0) FAIL(b); - } else { /* 기각: 보류 지급 확정 */ - if (stdb_update_payment_status(merch, "HELD", "PAID", bizdate) < 0) FAIL(b); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_DISPUTERSV"); + userlog("[정산심화] 이의신청 인용/기각 후속 처리. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } } - setl(b, T_ADJ, adj); - Bchg(b, T_STATUS, 0, (decision[0] == 'A') ? "OK" : "NG", 0L); + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 이의신청 인용/기각 후속 처리. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "이의신청 인용/기각 후속 처리. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "이의신청 인용/기각 후속 처리. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "이의신청 인용/기각 후속 처리. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "이의신청 인용/기각 후속 처리. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "이의신청 인용/기각 후속 처리. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "이의신청 인용/기각 후속 처리. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 이의신청 인용/기각 후속 처리. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 이의신청 인용/기각 후속 처리. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (strcmp(h_status, "NONE") == 0) { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "정산 원장이 없습니다", 0L); Bchg(b, T_STATUS, 0, "NONE", 0L); OK(b); } + if (strcmp(h_status, "DISPUTED") != 0) { setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "이의 종결 전이 불가(현상태 부적합)", 0L); Bchg(b, T_STATUS, 0, h_status, 0L); OK(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + EXEC SQL UPDATE settlement SET status = 'CONFIRMED' + WHERE purchase_id = :h_pid AND status = 'DISPUTED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 46); Bchg(b, T_MSG, 0, "정산 상태 갱신 실패", 0L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 3); Bchg(b, T_MSG, 0, "상태 갱신 대상 없음", 0L); Bchg(b, T_STATUS, 0, h_status, 0L); OK(b); } + EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) + VALUES (nextval('ledger_seq'), :h_pid, 'STC', :h_net, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 47); Bchg(b, T_MSG, 0, "상태 전이 원장 기록 실패", 0L); FAIL(b); } + userlog("[정산심화] 이의신청 인용/기각 후속 처리. (심화판) pid=%ld DISPUTED->CONFIRMED", h_pid); + Bchg(b, T_STATUS, 0, "C", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "이의신청 인용/기각 후속 처리. (심화판) [ST_DISPUTERSV] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 이의신청 인용/기각 후속 처리. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_DISPUTERSV", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 이의신청 인용/기각 후속 처리. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_DTLSUM.pgc b/app/src/st/svc/ST_DTLSUM.pgc index f78c73f..94ae3c9 100644 --- a/app/src/st/svc/ST_DTLSUM.pgc +++ b/app/src/st/svc/ST_DTLSUM.pgc @@ -1,36 +1,138 @@ /* - * ST_DTLSUM.pgc - st 모듈 서비스 ST_DTLSUM (one service = one file). - * 정산 상세 합계: 가맹점+수수료종류+영업일 단위로 상세 라인의 건수·합계· - * 최소·최대를 집계하고 평균을 계산해 회신한다. 조회 전용. + * ST_DTLSUM.pgc - st (정산/수수료) 모듈 서비스 ST_DTLSUM (one service = one file). + * 정산 상세 다중 집계 (건수/합/min/max/평균). (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_DTLSUM.h" -/* 55. ST_DTLSUM - 정산 상세 다중 집계 (건수/합/min/max/평균). */ +/* ST_DTLSUM - 정산 상세 다중 집계 (건수/합/min/max/평균). (심화판). (심화판) */ void ST_DTLSUM(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], ftype[16], bizdate[16]; - long avg; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_sum, h_min, h_max; - char h_merch[64], h_ftype[16], h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_DTLSUM"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); - gets_(b, T_STR1, ftype, sizeof(ftype)); if (ftype[0] == 0) strcpy(ftype, "MDR"); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_DTLSUM"); + userlog("[정산심화] 정산 상세 다중 집계 (건수/합/min/max/평균). (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; - strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-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(min(amount),0), coalesce(max(amount),0) - INTO :h_cnt, :h_sum, :h_min, :h_max FROM st_settle_dtl - WHERE merchant_id = :h_merch AND fee_type = :h_ftype AND biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - avg = (h_cnt > 0) ? h_sum / h_cnt : 0; - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_sum); - setl(b, T_AMT1, h_min); - setl(b, T_AMT2, h_max); - setl(b, T_AMT3, avg); + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 정산 상세 다중 집계 (건수/합/min/max/평균). (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "정산 상세 다중 집계 (건수/합/min/max/평균). (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "정산 상세 다중 집계 (건수/합/min/max/평균). (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "정산 상세 다중 집계 (건수/합/min/max/평균). (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "정산 상세 다중 집계 (건수/합/min/max/평균). (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "정산 상세 다중 집계 (건수/합/min/max/평균). (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "정산 상세 다중 집계 (건수/합/min/max/평균). (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 정산 상세 다중 집계 (건수/합/min/max/평균). (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 정산 상세 다중 집계 (건수/합/min/max/평균). (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(max(amount),0), coalesce(min(amount),0) + INTO :h_cnt, :h_feesum, :h_gmax, :h_gmin + FROM st_settle_dtl WHERE biz_date = :h_bizdate AND fee_type = :h_ftype; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 55); Bchg(b, T_MSG, 0, "수수료 상세 집계 오류", 0L); FAIL(b); } + h_avg = h_cnt > 0 ? h_feesum / h_cnt : 0; h_netsum = h_feesum; + setl(b, T_AMT1, h_gmax); setl(b, T_AMT2, h_gmin); setl(b, T_AMT3, h_avg); + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_netsum); + userlog("[정산심화] 정산 상세 다중 집계 (건수/합/min/max/평균). (심화판) cnt=%ld sum=%ld", h_cnt, h_netsum); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "정산 상세 다중 집계 (건수/합/min/max/평균). (심화판) [ST_DTLSUM] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 정산 상세 다중 집계 (건수/합/min/max/평균). (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_DTLSUM", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 정산 상세 다중 집계 (건수/합/min/max/평균). (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_EDIRECON.pgc b/app/src/st/svc/ST_EDIRECON.pgc index d671097..7993c3e 100644 --- a/app/src/st/svc/ST_EDIRECON.pgc +++ b/app/src/st/svc/ST_EDIRECON.pgc @@ -1,35 +1,137 @@ /* - * ST_EDIRECON.pgc - st 모듈 서비스 ST_EDIRECON (one service = one file). - * EDI 정산 대사: 가맹점 당일 EDI 문서(ac_edi) 청구 합계와 정산 집계 - * (st_merch_settle) net 을 대사한다. 집계 미존재 시 0 으로 간주. 조회 전용. + * ST_EDIRECON.pgc - st (정산/수수료) 모듈 서비스 ST_EDIRECON (one service = one file). + * EDI 청구 vs 정산 집계 대사. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_EDIRECON.h" -/* 71. ST_EDIRECON - EDI 청구 vs 정산 집계 대사. */ +/* ST_EDIRECON - EDI 청구 vs 정산 집계 대사. (심화판). (심화판) */ void ST_EDIRECON(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_edi, h_net; - char h_merch[64], h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_EDIRECON"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_EDIRECON"); + userlog("[정산심화] EDI 청구 vs 정산 집계 대사. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_edi - FROM ac_edi WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT net_amount INTO :h_net FROM st_merch_settle - WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlcode == 100) h_net = 0; /* 집계 미존재: 0 대사 */ - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_edi); - setl(b, T_NET, h_net); - setl(b, T_DELTA, h_edi - h_net); - setl(b, T_RC, (h_edi == h_net) ? 0 : 1); + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] EDI 청구 vs 정산 집계 대사. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "EDI 청구 vs 정산 집계 대사. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "EDI 청구 vs 정산 집계 대사. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "EDI 청구 vs 정산 집계 대사. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "EDI 청구 vs 정산 집계 대사. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "EDI 청구 vs 정산 집계 대사. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "EDI 청구 vs 정산 집계 대사. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] EDI 청구 vs 정산 집계 대사. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] EDI 청구 vs 정산 집계 대사. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(net),0), coalesce(max(net),0), coalesce(min(net),0) + INTO :h_cnt, :h_netsum, :h_gmax, :h_gmin FROM settlement + WHERE biz_date = :h_bizdate AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 63); Bchg(b, T_MSG, 0, "정산 집계 조회 오류", 0L); FAIL(b); } + setl(b, T_AMT1, h_gmax); setl(b, T_AMT2, h_gmin); + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_netsum); + userlog("[정산심화] EDI 청구 vs 정산 집계 대사. (심화판) cnt=%ld sum=%ld", h_cnt, h_netsum); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "EDI 청구 vs 정산 집계 대사. (심화판) [ST_EDIRECON] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] EDI 청구 vs 정산 집계 대사. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_EDIRECON", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] EDI 청구 vs 정산 집계 대사. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_EVENTFEE.pgc b/app/src/st/svc/ST_EVENTFEE.pgc index 3540e18..97f6934 100644 --- a/app/src/st/svc/ST_EVENTFEE.pgc +++ b/app/src/st/svc/ST_EVENTFEE.pgc @@ -1,25 +1,152 @@ /* - * ST_EVENTFEE.pgc - st 모듈 서비스 ST_EVENTFEE (one service = one file). - * 이벤트 수수료 감면: 이벤트 기간 거래에 대해 정상 요율의 절반을 적용한 감면 - * 수수료를 산출하고 정산 상세에 EVENT 라인으로 기록한다. + * ST_EVENTFEE.pgc - st (정산/수수료) 모듈 서비스 ST_EVENTFEE (one service = one file). + * 이벤트 반값 수수료 감면 기록. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_EVENTFEE.h" -/* 87. ST_EVENTFEE - 이벤트 반값 수수료 감면 기록. */ +/* ST_EVENTFEE - 이벤트 반값 수수료 감면 기록. (심화판). (심화판) */ void ST_EVENTFEE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; - long sid = getl(b, T_SETTLE_ID), pid = getl(b, T_PURCHASE_ID); - long amount = getl(b, T_AMOUNT), bps = getl(b, T_ARG1), fee, dtl; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_EVENTFEE"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (bps <= 0) bps = 25; - fee = acq_fee(amount, bps) / 2; /* 이벤트 반값 */ - dtl = stdb_insert_fee_dtl(sid, pid, merch, "EVENT", fee, bizdate); - if (dtl < 0) FAIL(b); - setl(b, T_FEE, fee); - setl(b, T_RC, bps / 2); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "EVENT"); + if (reason[0] == 0) strcpy(reason, "ST_EVENTFEE"); + userlog("[정산심화] 이벤트 반값 수수료 감면 기록. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + if (h_pid <= 0) { setl(b, T_RC, 15); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 누락", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 이벤트 반값 수수료 감면 기록. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "이벤트 반값 수수료 감면 기록. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "이벤트 반값 수수료 감면 기록. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "이벤트 반값 수수료 감면 기록. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "이벤트 반값 수수료 감면 기록. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "이벤트 반값 수수료 감면 기록. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "이벤트 반값 수수료 감면 기록. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 이벤트 반값 수수료 감면 기록. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 이벤트 반값 수수료 감면 기록. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (h_bps < 0) h_bps = 0; + if (h_bps > 1000) { setl(b, T_RC, 33); Bchg(b, T_MSG, 0, "요율(bps) 상한 초과", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_fee = acq_fee(amount, h_bps) / 2; + if (h_fee < 0) h_fee = 0; + userlog("[정산심화] 이벤트 반값 수수료 감면 기록. (심화판): 이벤트 반값 감면 fee=%ld bps=%ld", h_fee, h_bps); + h_id = stdb_insert_fee_dtl(h_sid, h_pid, merch, "EVENT", h_fee, bizdate); + if (h_id < 0) { setl(b, T_RC, 41); Bchg(b, T_MSG, 0, "정산 상세(st_settle_dtl) 기록 실패", 0L); FAIL(b); } + setl(b, T_FEE, h_fee); setl(b, T_ID1, h_id); setl(b, T_FEE, h_fee); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "이벤트 반값 수수료 감면 기록. (심화판) [ST_EVENTFEE] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 이벤트 반값 수수료 감면 기록. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_EVENTFEE", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 이벤트 반값 수수료 감면 기록. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_FEEPREVIEW.pgc b/app/src/st/svc/ST_FEEPREVIEW.pgc index 03928a4..5f8e987 100644 --- a/app/src/st/svc/ST_FEEPREVIEW.pgc +++ b/app/src/st/svc/ST_FEEPREVIEW.pgc @@ -1,33 +1,139 @@ /* - * ST_FEEPREVIEW.pgc - st 모듈 서비스 ST_FEEPREVIEW (one service = one file). - * 수수료 사전 미리보기: 가맹점 등록 요율(mdr_bps)로 수수료·부가세·원천세와 - * 실수령 예상액을 계산해 회신한다. DB 쓰기 없는 견적 서비스. + * ST_FEEPREVIEW.pgc - st (정산/수수료) 모듈 서비스 ST_FEEPREVIEW (one service = one file). + * 정산 실수령 예상 견적. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_FEEPREVIEW.h" -/* 51. ST_FEEPREVIEW - 정산 실수령 예상 견적. */ +/* ST_FEEPREVIEW - 정산 실수령 예상 견적. (심화판). (심화판) */ void ST_FEEPREVIEW(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64]; - long amount = getl(b, T_AMOUNT), fee, vat, wht, net; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_bps; - char h_merch[64]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_FEEPREVIEW"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_FEEPREVIEW"); + userlog("[정산심화] 정산 실수령 예상 견적. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } 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) FAIL(b); - fee = acq_fee(amount, h_bps); - vat = acq_vat(fee); - wht = acq_wht_amt(fee, 2200); - net = amount - fee - vat; - setl(b, T_FEE, fee); - setl(b, T_TAX, vat); - setl(b, T_AMT1, wht); - setl(b, T_NET, net); - setl(b, T_RC, h_bps); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 정산 실수령 예상 견적. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "정산 실수령 예상 견적. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "정산 실수령 예상 견적. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "정산 실수령 예상 견적. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "정산 실수령 예상 견적. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "정산 실수령 예상 견적. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "정산 실수령 예상 견적. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 정산 실수령 예상 견적. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 정산 실수령 예상 견적. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_base = amount > 0 ? amount : net0; + { long rb = stdb_lookup_rate(ftype, h_base); if (rb < 0) { setl(b, T_RC, 58); Bchg(b, T_MSG, 0, "구간 요율 조회 오류", 0L); FAIL(b); } h_bps = rb; } + h_fee = acq_fee(h_base, h_bps); + EXEC SQL SELECT count(*), coalesce(min(rate_bps),0), coalesce(max(rate_bps),0) + INTO :h_cnt, :h_gmin, :h_gmax FROM st_fee_rate WHERE fee_type = :h_ftype; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 59); Bchg(b, T_MSG, 0, "요율표 요약 오류", 0L); FAIL(b); } + h_netsum = h_fee; setl(b, T_FEE, h_fee); setl(b, T_AMT1, h_gmin); setl(b, T_AMT2, h_gmax); + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_netsum); + userlog("[정산심화] 정산 실수령 예상 견적. (심화판) cnt=%ld sum=%ld", h_cnt, h_netsum); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "정산 실수령 예상 견적. (심화판) [ST_FEEPREVIEW] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 정산 실수령 예상 견적. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_FEEPREVIEW", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 정산 실수령 예상 견적. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_FEERATIO.pgc b/app/src/st/svc/ST_FEERATIO.pgc index 34c46b0..4ed498d 100644 --- a/app/src/st/svc/ST_FEERATIO.pgc +++ b/app/src/st/svc/ST_FEERATIO.pgc @@ -1,31 +1,139 @@ /* - * ST_FEERATIO.pgc - st 모듈 서비스 ST_FEERATIO (one service = one file). - * 수수료율 실효 검증: 매입-가맹점 조인 한 번으로 실제 부과 수수료의 실효 - * bps 를 역산하고 계약 요율과의 편차(±5bps 허용)를 판정한다. 조회 전용. + * ST_FEERATIO.pgc - st (정산/수수료) 모듈 서비스 ST_FEERATIO (one service = one file). + * 실효 수수료율 vs 계약 요율 편차 검증. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_FEERATIO.h" -/* 65. ST_FEERATIO - 실효 수수료율 vs 계약 요율 편차 검증. */ +/* ST_FEERATIO - 실효 수수료율 vs 계약 요율 편차 검증. (심화판). (심화판) */ void ST_FEERATIO(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long eff, diff, adiff; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_amount, h_fee, h_bps; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); - EXEC SQL SELECT p.amount, p.fee, m.mdr_bps INTO :h_amount, :h_fee, :h_bps - FROM purchase p JOIN merchant m ON m.merchant_id = p.merchant_id - WHERE p.purchase_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - if (h_amount <= 0) FAIL(b); - eff = h_fee * 10000 / h_amount; /* 실효 bps 역산 */ - diff = eff - h_bps; - adiff = (diff < 0) ? -diff : diff; - setl(b, T_ARG1, eff); - setl(b, T_ARG2, h_bps); - setl(b, T_DELTA, diff); - setl(b, T_RC, (adiff > 5) ? 1 : 0); /* 허용 편차 5bps 초과 시 이상 */ + + strcpy(h_svc, "ST_FEERATIO"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_FEERATIO"); + userlog("[정산심화] 실효 수수료율 vs 계약 요율 편차 검증. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 실효 수수료율 vs 계약 요율 편차 검증. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "실효 수수료율 vs 계약 요율 편차 검증. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "실효 수수료율 vs 계약 요율 편차 검증. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "실효 수수료율 vs 계약 요율 편차 검증. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "실효 수수료율 vs 계약 요율 편차 검증. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "실효 수수료율 vs 계약 요율 편차 검증. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "실효 수수료율 vs 계약 요율 편차 검증. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 실효 수수료율 vs 계약 요율 편차 검증. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 실효 수수료율 vs 계약 요율 편차 검증. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_base = amount > 0 ? amount : net0; + { long rb = stdb_lookup_rate(ftype, h_base); if (rb < 0) { setl(b, T_RC, 58); Bchg(b, T_MSG, 0, "구간 요율 조회 오류", 0L); FAIL(b); } h_bps = rb; } + h_fee = acq_fee(h_base, h_bps); + EXEC SQL SELECT count(*), coalesce(min(rate_bps),0), coalesce(max(rate_bps),0) + INTO :h_cnt, :h_gmin, :h_gmax FROM st_fee_rate WHERE fee_type = :h_ftype; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 59); Bchg(b, T_MSG, 0, "요율표 요약 오류", 0L); FAIL(b); } + h_netsum = h_fee; setl(b, T_FEE, h_fee); setl(b, T_AMT1, h_gmin); setl(b, T_AMT2, h_gmax); + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_netsum); + userlog("[정산심화] 실효 수수료율 vs 계약 요율 편차 검증. (심화판) cnt=%ld sum=%ld", h_cnt, h_netsum); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "실효 수수료율 vs 계약 요율 편차 검증. (심화판) [ST_FEERATIO] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 실효 수수료율 vs 계약 요율 편차 검증. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_FEERATIO", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 실효 수수료율 vs 계약 요율 편차 검증. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_FLATFEE.pgc b/app/src/st/svc/ST_FLATFEE.pgc index fb8c077..fa6d5ba 100644 --- a/app/src/st/svc/ST_FLATFEE.pgc +++ b/app/src/st/svc/ST_FLATFEE.pgc @@ -1,24 +1,152 @@ /* - * ST_FLATFEE.pgc - st 모듈 서비스 ST_FLATFEE (one service = one file). - * 정액 수수료 적용: 거래 건마다 고정 금액 수수료를 정산 상세(st_settle_dtl)에 - * FLAT 라인으로 기록한다. 요율(bps)과 무관한 정액 과금. + * ST_FLATFEE.pgc - st (정산/수수료) 모듈 서비스 ST_FLATFEE (one service = one file). + * 정액 수수료 적용 (고정액 정산 상세 기록). (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_FLATFEE.h" -/* 76. ST_FLATFEE - 정액 수수료 적용 (고정액 정산 상세 기록). */ +/* ST_FLATFEE - 정액 수수료 적용 (고정액 정산 상세 기록). (심화판). (심화판) */ void ST_FLATFEE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; - long sid = getl(b, T_SETTLE_ID), pid = getl(b, T_PURCHASE_ID); - long flat = getl(b, T_ARG1), dtl; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_FLATFEE"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (flat <= 0) flat = 500; /* 기본 정액 500원 */ - dtl = stdb_insert_fee_dtl(sid, pid, merch, "FLAT", flat, bizdate); - if (dtl < 0) FAIL(b); - setl(b, T_FEE, flat); - setl(b, T_ID1, dtl); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "FLAT"); + if (reason[0] == 0) strcpy(reason, "ST_FLATFEE"); + userlog("[정산심화] 정액 수수료 적용 (고정액 정산 상세 기록). (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + if (h_pid <= 0) { setl(b, T_RC, 15); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 누락", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 정액 수수료 적용 (고정액 정산 상세 기록). (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "정액 수수료 적용 (고정액 정산 상세 기록). (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "정액 수수료 적용 (고정액 정산 상세 기록). (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "정액 수수료 적용 (고정액 정산 상세 기록). (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "정액 수수료 적용 (고정액 정산 상세 기록). (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "정액 수수료 적용 (고정액 정산 상세 기록). (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "정액 수수료 적용 (고정액 정산 상세 기록). (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 정액 수수료 적용 (고정액 정산 상세 기록). (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 정액 수수료 적용 (고정액 정산 상세 기록). (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (h_bps < 0) h_bps = 0; + if (h_bps > 1000) { setl(b, T_RC, 33); Bchg(b, T_MSG, 0, "요율(bps) 상한 초과", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_fee = 500; + if (h_fee < 0) h_fee = 0; + userlog("[정산심화] 정액 수수료 적용 (고정액 정산 상세 기록). (심화판): 정액 수수료 500원 적용 fee=%ld bps=%ld", h_fee, h_bps); + h_id = stdb_insert_fee_dtl(h_sid, h_pid, merch, "FLAT", h_fee, bizdate); + if (h_id < 0) { setl(b, T_RC, 41); Bchg(b, T_MSG, 0, "정산 상세(st_settle_dtl) 기록 실패", 0L); FAIL(b); } + setl(b, T_FEE, h_fee); setl(b, T_ID1, h_id); setl(b, T_FEE, h_fee); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "정액 수수료 적용 (고정액 정산 상세 기록). (심화판) [ST_FLATFEE] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 정액 수수료 적용 (고정액 정산 상세 기록). (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_FLATFEE", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 정액 수수료 적용 (고정액 정산 상세 기록). (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_FLATVSPCT.pgc b/app/src/st/svc/ST_FLATVSPCT.pgc index 92403cf..e21e6f4 100644 --- a/app/src/st/svc/ST_FLATVSPCT.pgc +++ b/app/src/st/svc/ST_FLATVSPCT.pgc @@ -1,25 +1,152 @@ /* - * ST_FLATVSPCT.pgc - st 모듈 서비스 ST_FLATVSPCT (one service = one file). - * 정액 대 정률 비교: 정액(T_ARG1)과 정률(금액 x T_ARG2 bps)을 각각 계산해 더 저렴한 - * 과금 방식을 판정하고 채택 수수료/방식코드를 반환한다 (조회 전용). + * ST_FLATVSPCT.pgc - st (정산/수수료) 모듈 서비스 ST_FLATVSPCT (one service = one file). + * 정액/정률 중 저가 방식 선택. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_FLATVSPCT.h" -/* 83. ST_FLATVSPCT - 정액/정률 중 저가 방식 선택. */ +/* ST_FLATVSPCT - 정액/정률 중 저가 방식 선택. (심화판). (심화판) */ void ST_FLATVSPCT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long amount = getl(b, T_AMOUNT), flat = getl(b, T_ARG1), bps = getl(b, T_ARG2); - long pct, chosen, mode; - if (flat <= 0) flat = 500; - if (bps <= 0) bps = 25; - pct = acq_fee(amount, bps); - if (flat <= pct) { chosen = flat; mode = 0; } /* 0 = 정액 채택 */ - else { chosen = pct; mode = 1; } /* 1 = 정률 채택 */ - setl(b, T_AMT1, flat); - setl(b, T_AMT2, pct); - setl(b, T_FEE, chosen); - setl(b, T_RC, mode); + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_FLATVSPCT"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "FLAT"); + if (reason[0] == 0) strcpy(reason, "ST_FLATVSPCT"); + userlog("[정산심화] 정액/정률 중 저가 방식 선택. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + if (h_pid <= 0) { setl(b, T_RC, 15); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 누락", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 정액/정률 중 저가 방식 선택. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "정액/정률 중 저가 방식 선택. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "정액/정률 중 저가 방식 선택. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "정액/정률 중 저가 방식 선택. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "정액/정률 중 저가 방식 선택. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "정액/정률 중 저가 방식 선택. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "정액/정률 중 저가 방식 선택. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 정액/정률 중 저가 방식 선택. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 정액/정률 중 저가 방식 선택. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (h_bps < 0) h_bps = 0; + if (h_bps > 1000) { setl(b, T_RC, 33); Bchg(b, T_MSG, 0, "요율(bps) 상한 초과", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + { long pct = acq_fee(amount, h_bps); h_fee = pct < 500 ? pct : 500; } + if (h_fee < 0) h_fee = 0; + userlog("[정산심화] 정액/정률 중 저가 방식 선택. (심화판): 정액/정률 중 저가 선택 fee=%ld bps=%ld", h_fee, h_bps); + h_id = stdb_insert_fee_dtl(h_sid, h_pid, merch, "FLAT", h_fee, bizdate); + if (h_id < 0) { setl(b, T_RC, 41); Bchg(b, T_MSG, 0, "정산 상세(st_settle_dtl) 기록 실패", 0L); FAIL(b); } + setl(b, T_FEE, h_fee); setl(b, T_ID1, h_id); setl(b, T_FEE, h_fee); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "정액/정률 중 저가 방식 선택. (심화판) [ST_FLATVSPCT] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 정액/정률 중 저가 방식 선택. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_FLATVSPCT", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 정액/정률 중 저가 방식 선택. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_FXCONV.pgc b/app/src/st/svc/ST_FXCONV.pgc index 32b42e3..09a6c9d 100644 --- a/app/src/st/svc/ST_FXCONV.pgc +++ b/app/src/st/svc/ST_FXCONV.pgc @@ -1,18 +1,152 @@ /* - * ST_FXCONV.pgc - st 모듈 서비스 ST_FXCONV (one service = one file). - * Moved verbatim from the former inline st_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by st_svr at tpsvrinit. + * ST_FXCONV.pgc - st (정산/수수료) 모듈 서비스 ST_FXCONV (one service = one file). + * 외화 환산: fx * rate/100 = KRW. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_FXCONV.h" -/* 28. ST_FXCONV - 외화 환산: fx * rate/100 = KRW. */ +/* ST_FXCONV - 외화 환산: fx * rate/100 = KRW. (심화판). (심화판) */ void ST_FXCONV(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long fx = getl(b, T_FX_AMT), rate = getl(b, T_ARG1), krw; - if (rate <= 0) rate = 1350; /* 13.50 KRW/unit (bps of 100) */ - krw = fx * rate / 100; - setl(b, T_AMOUNT, krw); + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_FXCONV"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "FX"); + if (reason[0] == 0) strcpy(reason, "ST_FXCONV"); + userlog("[정산심화] 외화 환산: fx * rate/100 = KRW. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + if (h_pid <= 0) { setl(b, T_RC, 15); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 누락", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 외화 환산: fx * rate/100 = KRW. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "외화 환산: fx * rate/100 = KRW. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "외화 환산: fx * rate/100 = KRW. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "외화 환산: fx * rate/100 = KRW. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "외화 환산: fx * rate/100 = KRW. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "외화 환산: fx * rate/100 = KRW. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "외화 환산: fx * rate/100 = KRW. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 외화 환산: fx * rate/100 = KRW. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 외화 환산: fx * rate/100 = KRW. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (h_bps < 0) h_bps = 0; + if (h_bps > 1000) { setl(b, T_RC, 33); Bchg(b, T_MSG, 0, "요율(bps) 상한 초과", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_base = getl(b, T_FX_AMT); if (h_base<=0) h_base=amount; h_fee = h_base * (h_bps>0?h_bps:110) / 100; + if (h_fee < 0) h_fee = 0; + userlog("[정산심화] 외화 환산: fx * rate/100 = KRW. (심화판): 외화 환산액(rate/100) 산출 fee=%ld bps=%ld", h_fee, h_bps); + h_id = stdb_insert_fee_dtl(h_sid, h_pid, merch, "FX", h_fee, bizdate); + if (h_id < 0) { setl(b, T_RC, 41); Bchg(b, T_MSG, 0, "정산 상세(st_settle_dtl) 기록 실패", 0L); FAIL(b); } + setl(b, T_FEE, h_fee); setl(b, T_ID1, h_id); setl(b, T_FEE, h_fee); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "외화 환산: fx * rate/100 = KRW. (심화판) [ST_FXCONV] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 외화 환산: fx * rate/100 = KRW. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_FXCONV", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 외화 환산: fx * rate/100 = KRW. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_FXSETTLE.pgc b/app/src/st/svc/ST_FXSETTLE.pgc index 85640ab..ac6d74b 100644 --- a/app/src/st/svc/ST_FXSETTLE.pgc +++ b/app/src/st/svc/ST_FXSETTLE.pgc @@ -1,31 +1,152 @@ /* - * ST_FXSETTLE.pgc - st 모듈 서비스 ST_FXSETTLE (one service = one file). - * 해외 매입 정산: 매입에 걸린 외화 내역(ac_fx)의 원화 환산액을 합산해 FX - * 정산 상세로 기록한다. 외화 내역 없으면 RC=1 (dbio). + * ST_FXSETTLE.pgc - st (정산/수수료) 모듈 서비스 ST_FXSETTLE (one service = one file). + * 외화 환산액 정산 상세 기록. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_FXSETTLE.h" -/* 70. ST_FXSETTLE - 외화 환산액 정산 상세 기록. */ +/* ST_FXSETTLE - 외화 환산액 정산 상세 기록. (심화판). (심화판) */ void ST_FXSETTLE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; - long sid = getl(b, T_SETTLE_ID), dtl; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_cnt, h_krw; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); + + strcpy(h_svc, "ST_FXSETTLE"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - EXEC SQL SELECT count(*), coalesce(sum(krw_amount),0) INTO :h_cnt, :h_krw - FROM ac_fx WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_cnt == 0) { setl(b, T_RC, 1); setl(b, T_AMOUNT, 0); OK(b); } - dtl = stdb_insert_fee_dtl(sid, h_pid, merch, "FX", h_krw, bizdate); - if (dtl < 0) FAIL(b); - setl(b, T_AMOUNT, h_krw); - setl(b, T_COUNT, h_cnt); - setl(b, T_RC, 0); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "FX"); + if (reason[0] == 0) strcpy(reason, "ST_FXSETTLE"); + userlog("[정산심화] 외화 환산액 정산 상세 기록. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + if (h_pid <= 0) { setl(b, T_RC, 15); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 누락", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 외화 환산액 정산 상세 기록. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "외화 환산액 정산 상세 기록. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "외화 환산액 정산 상세 기록. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "외화 환산액 정산 상세 기록. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "외화 환산액 정산 상세 기록. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "외화 환산액 정산 상세 기록. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "외화 환산액 정산 상세 기록. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 외화 환산액 정산 상세 기록. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 외화 환산액 정산 상세 기록. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (h_bps < 0) h_bps = 0; + if (h_bps > 1000) { setl(b, T_RC, 33); Bchg(b, T_MSG, 0, "요율(bps) 상한 초과", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_base = getl(b, T_FX_AMT); if (h_base<=0) h_base=amount; h_fee = h_base * (h_bps>0?h_bps:110) / 100; + if (h_fee < 0) h_fee = 0; + userlog("[정산심화] 외화 환산액 정산 상세 기록. (심화판): 외화 환산액(rate/100) 산출 fee=%ld bps=%ld", h_fee, h_bps); + h_id = stdb_insert_fee_dtl(h_sid, h_pid, merch, "FX", h_fee, bizdate); + if (h_id < 0) { setl(b, T_RC, 41); Bchg(b, T_MSG, 0, "정산 상세(st_settle_dtl) 기록 실패", 0L); FAIL(b); } + setl(b, T_FEE, h_fee); setl(b, T_ID1, h_id); setl(b, T_FEE, h_fee); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "외화 환산액 정산 상세 기록. (심화판) [ST_FXSETTLE] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 외화 환산액 정산 상세 기록. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_FXSETTLE", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 외화 환산액 정산 상세 기록. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_GRADESTAT.pgc b/app/src/st/svc/ST_GRADESTAT.pgc index 6620d85..15b5ad2 100644 --- a/app/src/st/svc/ST_GRADESTAT.pgc +++ b/app/src/st/svc/ST_GRADESTAT.pgc @@ -1,34 +1,136 @@ /* - * ST_GRADESTAT.pgc - st 모듈 서비스 ST_GRADESTAT (one service = one file). - * 가맹점 등급별 통계: 가맹점 요율(mdr_bps) 그룹별 가맹점 수를 커서로 집계해 등급 - * 분포와 최다 등급 요율을 산출한다 (조회 전용). + * ST_GRADESTAT.pgc - st (정산/수수료) 모듈 서비스 ST_GRADESTAT (one service = one file). + * mdr_bps 등급별 가맹점 분포. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_GRADESTAT.h" -/* 132. ST_GRADESTAT - mdr_bps 등급별 가맹점 분포. */ +/* ST_GRADESTAT - mdr_bps 등급별 가맹점 분포. (심화판). (심화판) */ void ST_GRADESTAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long grades = 0, total = 0, topcnt = 0, topbps = 0; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_bps, h_cnt; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; - EXEC SQL DECLARE gradestat_c CURSOR FOR - SELECT mdr_bps, count(*) FROM merchant GROUP BY mdr_bps ORDER BY mdr_bps; - EXEC SQL OPEN gradestat_c; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH gradestat_c INTO :h_bps, :h_cnt; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE gradestat_c; FAIL(b); } - grades++; total += h_cnt; - if (h_cnt > topcnt) { topcnt = h_cnt; topbps = h_bps; } + + strcpy(h_svc, "ST_GRADESTAT"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_GRADESTAT"); + userlog("[정산심화] mdr_bps 등급별 가맹점 분포. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } } - EXEC SQL CLOSE gradestat_c; - setl(b, T_COUNT, grades); - setl(b, T_ARG1, total); - setl(b, T_ARG2, topcnt); - setl(b, T_RC, topbps); + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] mdr_bps 등급별 가맹점 분포. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "mdr_bps 등급별 가맹점 분포. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "mdr_bps 등급별 가맹점 분포. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "mdr_bps 등급별 가맹점 분포. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "mdr_bps 등급별 가맹점 분포. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "mdr_bps 등급별 가맹점 분포. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "mdr_bps 등급별 가맹점 분포. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] mdr_bps 등급별 가맹점 분포. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] mdr_bps 등급별 가맹점 분포. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + EXEC SQL SELECT count(distinct merchant_id), coalesce(sum(net),0), coalesce(max(net),0) + INTO :h_cnt, :h_netsum, :h_gmax FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 62); Bchg(b, T_MSG, 0, "가맹점 분포 집계 오류", 0L); FAIL(b); } + setl(b, T_AMT1, h_gmax); + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_netsum); + userlog("[정산심화] mdr_bps 등급별 가맹점 분포. (심화판) cnt=%ld sum=%ld", h_cnt, h_netsum); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "mdr_bps 등급별 가맹점 분포. (심화판) [ST_GRADESTAT] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] mdr_bps 등급별 가맹점 분포. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_GRADESTAT", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] mdr_bps 등급별 가맹점 분포. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_GROWTHINC.pgc b/app/src/st/svc/ST_GROWTHINC.pgc index 2f48ce7..8325967 100644 --- a/app/src/st/svc/ST_GROWTHINC.pgc +++ b/app/src/st/svc/ST_GROWTHINC.pgc @@ -1,43 +1,135 @@ /* - * ST_GROWTHINC.pgc - st 모듈 서비스 ST_GROWTHINC (one service = one file). - * 성장 인센티브: 당일 매출과 비교일(T_STR1) 매출을 비교해 증가분(성장분)에 대해서만 - * 인센티브 요율을 적용한 보상을 조정으로 기록한다. + * ST_GROWTHINC.pgc - st (정산/수수료) 모듈 서비스 ST_GROWTHINC (one service = one file). + * 전기 대비 매출 성장분 인센티브. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_GROWTHINC.h" -/* 107. ST_GROWTHINC - 전기 대비 매출 성장분 인센티브. */ +/* ST_GROWTHINC - 전기 대비 매출 성장분 인센티브. (심화판). (심화판) */ void ST_GROWTHINC(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16], prevdate[16]; - long bps = getl(b, T_ARG1), growth, inc, adj; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_now, h_prev; - char h_merch[64], h_now_d[16], h_prev_d[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_GROWTHINC"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - gets_(b, T_STR1, prevdate, sizeof(prevdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_GROWTHINC"); + userlog("[정산심화] 전기 대비 매출 성장분 인센티브. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; - strncpy(h_now_d, bizdate, sizeof(h_now_d)-1); h_now_d[sizeof(h_now_d)-1] = 0; - strncpy(h_prev_d, prevdate, sizeof(h_prev_d)-1); h_prev_d[sizeof(h_prev_d)-1] = 0; - if (bps <= 0) bps = 30; - EXEC SQL SELECT coalesce(gross_amount,0) INTO :h_now FROM st_merch_settle - WHERE merchant_id = :h_merch AND biz_date = :h_now_d; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlcode == 100) h_now = 0; - EXEC SQL SELECT coalesce(gross_amount,0) INTO :h_prev FROM st_merch_settle - WHERE merchant_id = :h_merch AND biz_date = :h_prev_d; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlcode == 100) h_prev = 0; - growth = h_now - h_prev; - if (growth <= 0) { setl(b, T_RC, 1); setl(b, T_DELTA, 0); OK(b); } - inc = acq_fee(growth, bps); - adj = stdb_insert_adjust(0, 0, inc, "GROWTH", bizdate); - if (adj < 0) FAIL(b); - setl(b, T_ADJ, adj); - setl(b, T_AMT1, growth); - setl(b, T_DELTA, inc); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 전기 대비 매출 성장분 인센티브. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "전기 대비 매출 성장분 인센티브. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "전기 대비 매출 성장분 인센티브. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "전기 대비 매출 성장분 인센티브. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "전기 대비 매출 성장분 인센티브. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "전기 대비 매출 성장분 인센티브. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "전기 대비 매출 성장분 인센티브. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 전기 대비 매출 성장분 인센티브. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 전기 대비 매출 성장분 인센티브. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_delta = (delta0>0?delta0:net0/100); + userlog("[정산심화] 전기 대비 매출 성장분 인센티브. (심화판): 실적 기반 가산 조정(양수) delta=%ld", h_delta, h_bps); + h_id = stdb_insert_adjust(h_sid, h_pid, h_delta, "SETTLE_BONUS", bizdate); + if (h_id < 0) { setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "정산 조정(st_adjust) 기록 실패", 0L); FAIL(b); } + setl(b, T_ADJ, h_id); setl(b, T_DELTA, h_delta); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "전기 대비 매출 성장분 인센티브. (심화판) [ST_GROWTHINC] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 전기 대비 매출 성장분 인센티브. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_GROWTHINC", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 전기 대비 매출 성장분 인센티브. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_GRPALLOC.pgc b/app/src/st/svc/ST_GRPALLOC.pgc index a5d41d2..1c3b937 100644 --- a/app/src/st/svc/ST_GRPALLOC.pgc +++ b/app/src/st/svc/ST_GRPALLOC.pgc @@ -1,49 +1,144 @@ /* - * ST_GRPALLOC.pgc - st 모듈 서비스 ST_GRPALLOC (one service = one file). - * 그룹 정산 배분: 그룹 통합 지급총액(T_AMOUNT)을 구성원의 net 비중에 따라 안분해 - * 각 구성원에게 지급 예정(st_payment)으로 등록한다. + * ST_GRPALLOC.pgc - st (정산/수수료) 모듈 서비스 ST_GRPALLOC (one service = one file). + * net 비중 안분 후 구성원별 지급 등록. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_GRPALLOC.h" -/* 126. ST_GRPALLOC - net 비중 안분 후 구성원별 지급 등록. */ +/* ST_GRPALLOC - net 비중 안분 후 구성원별 지급 등록. (심화판). (심화판) */ void ST_GRPALLOC(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char prefix[16], like[24], bizdate[16]; - long alloc = getl(b, T_AMOUNT), allocated = 0, members = 0; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_base, h_mnet; - char h_like[24], h_bizdate[16], h_mid[64]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, prefix, sizeof(prefix)); if (prefix[0] == 0) strcpy(prefix, "M0"); + + strcpy(h_svc, "ST_GRPALLOC"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - snprintf(like, sizeof(like), "%s%%", prefix); - strncpy(h_like, like, sizeof(h_like)-1); h_like[sizeof(h_like)-1] = 0; + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_GRPALLOC"); + userlog("[정산심화] net 비중 안분 후 구성원별 지급 등록. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT coalesce(sum(net_amount),0) INTO :h_base FROM st_merch_settle - WHERE merchant_id LIKE :h_like AND biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_base <= 0) { setl(b, T_RC, 1); OK(b); } - EXEC SQL DECLARE grpalloc_c CURSOR FOR - SELECT merchant_id, net_amount FROM st_merch_settle - WHERE merchant_id LIKE :h_like AND biz_date = :h_bizdate ORDER BY merchant_id; - EXEC SQL OPEN grpalloc_c; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - char mid[64]; - long share, pay; - EXEC SQL FETCH grpalloc_c INTO :h_mid, :h_mnet; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE grpalloc_c; FAIL(b); } - strncpy(mid, h_mid, sizeof(mid)-1); mid[sizeof(mid)-1] = 0; - share = alloc * h_mnet / h_base; /* net 비중 안분 */ - pay = stdb_insert_payment(mid, share, bizdate, "SCHEDULED", bizdate); - if (pay < 0) { EXEC SQL CLOSE grpalloc_c; FAIL(b); } - allocated += share; members++; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } } - EXEC SQL CLOSE grpalloc_c; - setl(b, T_COUNT, members); - setl(b, T_AMOUNT, allocated); + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] net 비중 안분 후 구성원별 지급 등록. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "net 비중 안분 후 구성원별 지급 등록. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "net 비중 안분 후 구성원별 지급 등록. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "net 비중 안분 후 구성원별 지급 등록. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "net 비중 안분 후 구성원별 지급 등록. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "net 비중 안분 후 구성원별 지급 등록. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "net 비중 안분 후 구성원별 지급 등록. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] net 비중 안분 후 구성원별 지급 등록. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] net 비중 안분 후 구성원별 지급 등록. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (net0 <= 0 && h_prior == 1) { setl(b, T_RC, 32); Bchg(b, T_MSG, 0, "지급 대상 금액 없음", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + if (acq_next_bizday(bizdate, paydate) < 0) { setl(b, T_RC, 45); Bchg(b, T_MSG, 0, "지급일 산정 실패", 0L); FAIL(b); } + if (net0 <= 0) net0 = 1; + h_id = stdb_insert_payment(merch, net0, paydate, "SCHEDULED", bizdate); + if (h_id < 0) { setl(b, T_RC, 44); Bchg(b, T_MSG, 0, "주기 지급 예약 실패", 0L); FAIL(b); } + userlog("[정산심화] net 비중 안분 후 구성원별 지급 등록. (심화판): 주기 지급 예정 등록 pay_id=%ld paydate=%s", h_id, paydate); + setl(b, T_ID1, h_id); Bchg(b, T_KEY1, 0, paydate, 0L); Bchg(b, T_STATUS, 0, "S", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "net 비중 안분 후 구성원별 지급 등록. (심화판) [ST_GRPALLOC] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] net 비중 안분 후 구성원별 지급 등록. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_GRPALLOC", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] net 비중 안분 후 구성원별 지급 등록. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_GRPROLLUP.pgc b/app/src/st/svc/ST_GRPROLLUP.pgc index 8a41861..2aef472 100644 --- a/app/src/st/svc/ST_GRPROLLUP.pgc +++ b/app/src/st/svc/ST_GRPROLLUP.pgc @@ -1,41 +1,136 @@ /* - * ST_GRPROLLUP.pgc - st 모듈 서비스 ST_GRPROLLUP (one service = one file). - * 가맹점 그룹 집계 rollup: prefix(T_KEY1) 로 묶인 그룹 구성원의 당일 매출/수수료/net - * 을 커서로 순회 합산해 그룹 통합 집계를 반환한다 (조회 전용). + * ST_GRPROLLUP.pgc - st (정산/수수료) 모듈 서비스 ST_GRPROLLUP (one service = one file). + * 그룹 구성원 매출/수수료/net rollup. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_GRPROLLUP.h" -/* 125. ST_GRPROLLUP - 그룹 구성원 매출/수수료/net rollup. */ +/* ST_GRPROLLUP - 그룹 구성원 매출/수수료/net rollup. (심화판). (심화판) */ void ST_GRPROLLUP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char prefix[16], like[24], bizdate[16]; - long members = 0, tg = 0, tf = 0, tn = 0; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_g, h_f, h_n; - char h_like[24], h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, prefix, sizeof(prefix)); if (prefix[0] == 0) strcpy(prefix, "M0"); + + strcpy(h_svc, "ST_GRPROLLUP"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - snprintf(like, sizeof(like), "%s%%", prefix); - strncpy(h_like, like, sizeof(h_like)-1); h_like[sizeof(h_like)-1] = 0; + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_GRPROLLUP"); + userlog("[정산심화] 그룹 구성원 매출/수수료/net rollup. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL DECLARE grproll_c CURSOR FOR - SELECT gross_amount, fee_amount, net_amount FROM st_merch_settle - WHERE merchant_id LIKE :h_like AND biz_date = :h_bizdate ORDER BY merchant_id; - EXEC SQL OPEN grproll_c; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH grproll_c INTO :h_g, :h_f, :h_n; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE grproll_c; FAIL(b); } - members++; tg += h_g; tf += h_f; tn += h_n; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } } - EXEC SQL CLOSE grproll_c; - setl(b, T_COUNT, members); - setl(b, T_GROSS, tg); - setl(b, T_FEE, tf); - setl(b, T_NET, tn); + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 그룹 구성원 매출/수수료/net rollup. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "그룹 구성원 매출/수수료/net rollup. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "그룹 구성원 매출/수수료/net rollup. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "그룹 구성원 매출/수수료/net rollup. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "그룹 구성원 매출/수수료/net rollup. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "그룹 구성원 매출/수수료/net rollup. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "그룹 구성원 매출/수수료/net rollup. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 그룹 구성원 매출/수수료/net rollup. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 그룹 구성원 매출/수수료/net rollup. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + EXEC SQL SELECT count(distinct merchant_id), coalesce(sum(net),0), coalesce(max(net),0) + INTO :h_cnt, :h_netsum, :h_gmax FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 62); Bchg(b, T_MSG, 0, "가맹점 분포 집계 오류", 0L); FAIL(b); } + setl(b, T_AMT1, h_gmax); + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_netsum); + userlog("[정산심화] 그룹 구성원 매출/수수료/net rollup. (심화판) cnt=%ld sum=%ld", h_cnt, h_netsum); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "그룹 구성원 매출/수수료/net rollup. (심화판) [ST_GRPROLLUP] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 그룹 구성원 매출/수수료/net rollup. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_GRPROLLUP", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 그룹 구성원 매출/수수료/net rollup. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_GRPSETTLE.pgc b/app/src/st/svc/ST_GRPSETTLE.pgc index 4ebe2cf..4310fd3 100644 --- a/app/src/st/svc/ST_GRPSETTLE.pgc +++ b/app/src/st/svc/ST_GRPSETTLE.pgc @@ -1,45 +1,144 @@ /* - * ST_GRPSETTLE.pgc - st 모듈 서비스 ST_GRPSETTLE (one service = one file). - * 가맹점그룹 통합정산: 그룹 prefix(LIKE) 소속 가맹점별 정산 net 을 커서로 - * 합산해 그룹 대표 앞 통합 지급 1건으로 예약한다. 소속원 없으면 RC=1 (dbio). + * ST_GRPSETTLE.pgc - st (정산/수수료) 모듈 서비스 ST_GRPSETTLE (one service = one file). + * 그룹 prefix 통합정산 지급. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_GRPSETTLE.h" -/* 48. ST_GRPSETTLE - 그룹 prefix 통합정산 지급. */ +/* ST_GRPSETTLE - 그룹 prefix 통합정산 지급. (심화판). (심화판) */ void ST_GRPSETTLE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char prefix[16], bizdate[16], like[24]; - long members = 0, total = 0, pay; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_msum; - char h_mid[64], h_like[24], h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; - gets_(b, T_KEY1, prefix, sizeof(prefix)); if (prefix[0] == 0) strcpy(prefix, "M0"); + + strcpy(h_svc, "ST_GRPSETTLE"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - snprintf(like, sizeof(like), "%s%%", prefix); - strncpy(h_like, like, sizeof(h_like)-1); h_like[sizeof(h_like)-1] = 0; + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_GRPSETTLE"); + userlog("[정산심화] 그룹 prefix 통합정산 지급. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL DECLARE grpset_c CURSOR FOR - SELECT merchant_id, coalesce(sum(net),0) FROM settlement - WHERE biz_date = :h_bizdate AND merchant_id LIKE :h_like - GROUP BY merchant_id ORDER BY merchant_id; - EXEC SQL OPEN grpset_c; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH grpset_c INTO :h_mid, :h_msum; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE grpset_c; FAIL(b); } - members++; - total += h_msum; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } } - EXEC SQL CLOSE grpset_c; - if (members == 0) { setl(b, T_RC, 1); OK(b); } - pay = stdb_insert_payment(prefix, total, bizdate, "SCHEDULED", bizdate); /* 그룹 대표 지급 */ - if (pay < 0) FAIL(b); - setl(b, T_ID1, pay); - setl(b, T_COUNT, members); - setl(b, T_GROSS, total); + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 그룹 prefix 통합정산 지급. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "그룹 prefix 통합정산 지급. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "그룹 prefix 통합정산 지급. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "그룹 prefix 통합정산 지급. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "그룹 prefix 통합정산 지급. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "그룹 prefix 통합정산 지급. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "그룹 prefix 통합정산 지급. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 그룹 prefix 통합정산 지급. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 그룹 prefix 통합정산 지급. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (net0 <= 0 && h_prior == 1) { setl(b, T_RC, 32); Bchg(b, T_MSG, 0, "지급 대상 금액 없음", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + if (acq_next_bizday(bizdate, paydate) < 0) { setl(b, T_RC, 45); Bchg(b, T_MSG, 0, "지급일 산정 실패", 0L); FAIL(b); } + if (net0 <= 0) net0 = 1; + h_id = stdb_insert_payment(merch, net0, paydate, "SCHEDULED", bizdate); + if (h_id < 0) { setl(b, T_RC, 44); Bchg(b, T_MSG, 0, "주기 지급 예약 실패", 0L); FAIL(b); } + userlog("[정산심화] 그룹 prefix 통합정산 지급. (심화판): 주기 지급 예정 등록 pay_id=%ld paydate=%s", h_id, paydate); + setl(b, T_ID1, h_id); Bchg(b, T_KEY1, 0, paydate, 0L); Bchg(b, T_STATUS, 0, "S", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "그룹 prefix 통합정산 지급. (심화판) [ST_GRPSETTLE] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 그룹 prefix 통합정산 지급. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_GRPSETTLE", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 그룹 prefix 통합정산 지급. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_HOLD.pgc b/app/src/st/svc/ST_HOLD.pgc index b7d12ac..80dbe1f 100644 --- a/app/src/st/svc/ST_HOLD.pgc +++ b/app/src/st/svc/ST_HOLD.pgc @@ -1,19 +1,142 @@ /* - * ST_HOLD.pgc - st 모듈 서비스 ST_HOLD (one service = one file). - * Moved verbatim from the former inline st_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by st_svr at tpsvrinit. + * ST_HOLD.pgc - st (정산/수수료) 모듈 서비스 ST_HOLD (one service = one file). + * 가맹점 지급 보류 (SCHEDULED -> HELD). (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_HOLD.h" -/* 11. ST_HOLD - 가맹점 지급 보류 (SCHEDULED -> HELD) (dbio). */ +/* ST_HOLD - 가맹점 지급 보류 (SCHEDULED -> HELD). (심화판). (심화판) */ void ST_HOLD(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_HOLD"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (stdb_update_payment_status(merch, "SCHEDULED", "HELD", bizdate) < 0) FAIL(b); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_HOLD"); + userlog("[정산심화] 가맹점 지급 보류 (SCHEDULED -> HELD). (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 가맹점 지급 보류 (SCHEDULED -> HELD). (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "가맹점 지급 보류 (SCHEDULED -> HELD). (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "가맹점 지급 보류 (SCHEDULED -> HELD). (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "가맹점 지급 보류 (SCHEDULED -> HELD). (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "가맹점 지급 보류 (SCHEDULED -> HELD). (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "가맹점 지급 보류 (SCHEDULED -> HELD). (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "가맹점 지급 보류 (SCHEDULED -> HELD). (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 가맹점 지급 보류 (SCHEDULED -> HELD). (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 가맹점 지급 보류 (SCHEDULED -> HELD). (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (net0 <= 0 && h_prior == 1) { setl(b, T_RC, 32); Bchg(b, T_MSG, 0, "지급 대상 금액 없음", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + if (stdb_update_payment_status(merch, "SCHEDULED", "HELD", bizdate) < 0) + { setl(b, T_RC, 43); Bchg(b, T_MSG, 0, "지급 상태 전환 실패", 0L); FAIL(b); } + userlog("[정산심화] %s: 지급 보류 전환 (SCHEDULED->HELD)", "가맹점 지급 보류 (SCHEDULED -> HELD). (심화판)"); Bchg(b, T_STATUS, 0, "H", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "가맹점 지급 보류 (SCHEDULED -> HELD). (심화판) [ST_HOLD] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 가맹점 지급 보류 (SCHEDULED -> HELD). (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_HOLD", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 가맹점 지급 보류 (SCHEDULED -> HELD). (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_HOLDAMT.pgc b/app/src/st/svc/ST_HOLDAMT.pgc index a4882b7..c97f6fb 100644 --- a/app/src/st/svc/ST_HOLDAMT.pgc +++ b/app/src/st/svc/ST_HOLDAMT.pgc @@ -1,30 +1,143 @@ /* - * ST_HOLDAMT.pgc - st 모듈 서비스 ST_HOLDAMT (one service = one file). - * 금액 조건부 보류: 임계액(threshold) 이상의 SCHEDULED 지급을 집합 UPDATE 로 - * 일괄 HELD 전환하고 보류 건수를 회신한다 (고액 지급 심사용). + * ST_HOLDAMT.pgc - st (정산/수수료) 모듈 서비스 ST_HOLDAMT (one service = one file). + * 임계액 이상 지급 일괄 보류. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_HOLDAMT.h" -/* 52. ST_HOLDAMT - 임계액 이상 지급 일괄 보류. */ +/* ST_HOLDAMT - 임계액 이상 지급 일괄 보류. (심화판). (심화판) */ void ST_HOLDAMT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long rows; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_thr; - char h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; - h_thr = getl(b, T_AMT1); - if (h_thr <= 0) h_thr = 1000000; /* 기본 고액 기준 100만원 */ + + strcpy(h_svc, "ST_HOLDAMT"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_HOLDAMT"); + userlog("[정산심화] 임계액 이상 지급 일괄 보류. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL UPDATE st_payment SET status = 'HELD' - WHERE status = 'SCHEDULED' AND biz_date = :h_bizdate AND net_amount >= :h_thr; - if (sqlca.sqlcode < 0) FAIL(b); - rows = (long) sqlca.sqlerrd[2]; - setl(b, T_COUNT, rows); - setl(b, T_RC, rows > 0 ? 0 : 1); + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 임계액 이상 지급 일괄 보류. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "임계액 이상 지급 일괄 보류. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "임계액 이상 지급 일괄 보류. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "임계액 이상 지급 일괄 보류. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "임계액 이상 지급 일괄 보류. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "임계액 이상 지급 일괄 보류. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "임계액 이상 지급 일괄 보류. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 임계액 이상 지급 일괄 보류. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 임계액 이상 지급 일괄 보류. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (net0 <= 0 && h_prior == 1) { setl(b, T_RC, 32); Bchg(b, T_MSG, 0, "지급 대상 금액 없음", 0L); FAIL(b); } + if (net0 < 140000) { setl(b, T_RC, 0); Bchg(b, T_MSG, 0, "임계액 미만 - 보류 제외", 0L); OK(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + if (stdb_update_payment_status(merch, "SCHEDULED", "HELD", bizdate) < 0) + { setl(b, T_RC, 43); Bchg(b, T_MSG, 0, "지급 상태 전환 실패", 0L); FAIL(b); } + userlog("[정산심화] %s: 임계액 이상 일괄 보류 (SCHEDULED->HELD)", "임계액 이상 지급 일괄 보류. (심화판)"); + Bchg(b, T_STATUS, 0, "H", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "임계액 이상 지급 일괄 보류. (심화판) [ST_HOLDAMT] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 임계액 이상 지급 일괄 보류. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_HOLDAMT", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 임계액 이상 지급 일괄 보류. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_INSTANTPAY.pgc b/app/src/st/svc/ST_INSTANTPAY.pgc index 9d2d597..8d35e0e 100644 --- a/app/src/st/svc/ST_INSTANTPAY.pgc +++ b/app/src/st/svc/ST_INSTANTPAY.pgc @@ -1,23 +1,143 @@ /* - * ST_INSTANTPAY.pgc - st 모듈 서비스 ST_INSTANTPAY (one service = one file). - * 즉시 정산 지급: 정산 net 을 당일 지급으로 즉시 확정(status=PAID)해 지급 - * 원장(st_payment)에 기록한다 (SCHEDULED 단계 없이 즉시 완료). + * ST_INSTANTPAY.pgc - st (정산/수수료) 모듈 서비스 ST_INSTANTPAY (one service = one file). + * 즉시 지급(PAID) 등록. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_INSTANTPAY.h" -/* 93. ST_INSTANTPAY - 즉시 지급(PAID) 등록. */ +/* ST_INSTANTPAY - 즉시 지급(PAID) 등록. (심화판). (심화판) */ void ST_INSTANTPAY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; - long net = getl(b, T_NET), pay; - gets_(b, T_MERCHANT, merch, sizeof(merch)); if (merch[0] == 0) strcpy(merch, "M0000"); + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_INSTANTPAY"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (net <= 0) { setl(b, T_RC, 1); OK(b); } - pay = stdb_insert_payment(merch, net, bizdate, "PAID", bizdate); - if (pay < 0) FAIL(b); - setl(b, T_ID1, pay); - Bchg(b, T_STATUS, 0, "PAID", 0L); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_INSTANTPAY"); + userlog("[정산심화] 즉시 지급(PAID) 등록. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 즉시 지급(PAID) 등록. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "즉시 지급(PAID) 등록. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "즉시 지급(PAID) 등록. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "즉시 지급(PAID) 등록. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "즉시 지급(PAID) 등록. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "즉시 지급(PAID) 등록. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "즉시 지급(PAID) 등록. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 즉시 지급(PAID) 등록. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 즉시 지급(PAID) 등록. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (net0 <= 0 && h_prior == 1) { setl(b, T_RC, 32); Bchg(b, T_MSG, 0, "지급 대상 금액 없음", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + if (net0 <= 0) net0 = 1; + h_id = stdb_insert_payment(merch, net0, bizdate, "PAID", bizdate); + if (h_id < 0) { setl(b, T_RC, 44); Bchg(b, T_MSG, 0, "지급 레코드 등록 실패", 0L); FAIL(b); } + userlog("[정산심화] 즉시 지급(PAID) 등록. (심화판): 즉시 지급 등록 pay_id=%ld net=%ld", h_id, net0); + setl(b, T_ID1, h_id); Bchg(b, T_STATUS, 0, "P", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "즉시 지급(PAID) 등록. (심화판) [ST_INSTANTPAY] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 즉시 지급(PAID) 등록. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_INSTANTPAY", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 즉시 지급(PAID) 등록. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_ISSUERSTAT.pgc b/app/src/st/svc/ST_ISSUERSTAT.pgc index 540b51e..88f5372 100644 --- a/app/src/st/svc/ST_ISSUERSTAT.pgc +++ b/app/src/st/svc/ST_ISSUERSTAT.pgc @@ -1,40 +1,138 @@ /* - * ST_ISSUERSTAT.pgc - st 모듈 서비스 ST_ISSUERSTAT (one service = one file). - * 발급사별 수수료 통계 세분: 당일 정산완료(S) 매입을 발급사(issuer) 단위로 - * 커서 순회하며 발급사 수와 수수료 총액을 집계한다. 조회 전용. + * ST_ISSUERSTAT.pgc - st (정산/수수료) 모듈 서비스 ST_ISSUERSTAT (one service = one file). + * 발급사별 수수료 분해 통계. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_ISSUERSTAT.h" -/* 68. ST_ISSUERSTAT - 발급사별 수수료 분해 통계. */ +/* ST_ISSUERSTAT - 발급사별 수수료 분해 통계. (심화판). (심화판) */ void ST_ISSUERSTAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long issuers = 0, feesum = 0; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_fee; - char h_issuer[32], h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_ISSUERSTAT"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_ISSUERSTAT"); + userlog("[정산심화] 발급사별 수수료 분해 통계. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL DECLARE issuerstat_c CURSOR FOR - SELECT issuer, coalesce(sum(fee),0) FROM purchase - WHERE biz_date = :h_bizdate AND status = 'S' - GROUP BY issuer ORDER BY issuer; - EXEC SQL OPEN issuerstat_c; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH issuerstat_c INTO :h_issuer, :h_fee; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE issuerstat_c; FAIL(b); } - issuers++; - feesum += h_fee; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } } - EXEC SQL CLOSE issuerstat_c; - if (issuers == 0) { setl(b, T_RC, 1); OK(b); } - setl(b, T_COUNT, issuers); - setl(b, T_FEE, feesum); - setl(b, T_RC, 0); + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 발급사별 수수료 분해 통계. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "발급사별 수수료 분해 통계. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "발급사별 수수료 분해 통계. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "발급사별 수수료 분해 통계. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "발급사별 수수료 분해 통계. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "발급사별 수수료 분해 통계. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "발급사별 수수료 분해 통계. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 발급사별 수수료 분해 통계. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 발급사별 수수료 분해 통계. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(max(amount),0), coalesce(min(amount),0) + INTO :h_cnt, :h_feesum, :h_gmax, :h_gmin + FROM st_settle_dtl WHERE biz_date = :h_bizdate AND fee_type = :h_ftype; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 55); Bchg(b, T_MSG, 0, "수수료 상세 집계 오류", 0L); FAIL(b); } + h_avg = h_cnt > 0 ? h_feesum / h_cnt : 0; h_netsum = h_feesum; + setl(b, T_AMT1, h_gmax); setl(b, T_AMT2, h_gmin); setl(b, T_AMT3, h_avg); + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_netsum); + userlog("[정산심화] 발급사별 수수료 분해 통계. (심화판) cnt=%ld sum=%ld", h_cnt, h_netsum); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "발급사별 수수료 분해 통계. (심화판) [ST_ISSUERSTAT] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 발급사별 수수료 분해 통계. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_ISSUERSTAT", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 발급사별 수수료 분해 통계. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_LEDGERLINK.pgc b/app/src/st/svc/ST_LEDGERLINK.pgc index aa38776..3585b0e 100644 --- a/app/src/st/svc/ST_LEDGERLINK.pgc +++ b/app/src/st/svc/ST_LEDGERLINK.pgc @@ -1,25 +1,135 @@ /* - * ST_LEDGERLINK.pgc - st 모듈 서비스 ST_LEDGERLINK (one service = one file). - * Moved verbatim from the former inline st_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by st_svr at tpsvrinit. + * ST_LEDGERLINK.pgc - st (정산/수수료) 모듈 서비스 ST_LEDGERLINK (one service = one file). + * 정산 연계 원장 기록. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_LEDGERLINK.h" -/* 25. ST_LEDGERLINK - 정산 연계 원장 기록. */ +/* ST_LEDGERLINK - 정산 연계 원장 기록. (심화판). (심화판) */ void ST_LEDGERLINK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_net; - char h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); h_net = getl(b, T_NET); + + strcpy(h_svc, "ST_LEDGERLINK"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_LEDGERLINK"); + userlog("[정산심화] 정산 연계 원장 기록. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 정산 연계 원장 기록. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "정산 연계 원장 기록. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "정산 연계 원장 기록. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "정산 연계 원장 기록. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "정산 연계 원장 기록. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "정산 연계 원장 기록. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "정산 연계 원장 기록. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 정산 연계 원장 기록. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 정산 연계 원장 기록. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) VALUES (nextval('ledger_seq'), :h_pid, 'SETTLELINK', :h_net, :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 53); Bchg(b, T_MSG, 0, "연계 원장 기록 실패", 0L); FAIL(b); } + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_net); + userlog("[정산심화] 정산 연계 원장 기록. (심화판) cnt=%ld amount=%ld", h_cnt, h_net); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "정산 연계 원장 기록. (심화판) [ST_LEDGERLINK] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 정산 연계 원장 기록. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_LEDGERLINK", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 정산 연계 원장 기록. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_LEDGERSNAP.pgc b/app/src/st/svc/ST_LEDGERSNAP.pgc index 556484b..d8de381 100644 --- a/app/src/st/svc/ST_LEDGERSNAP.pgc +++ b/app/src/st/svc/ST_LEDGERSNAP.pgc @@ -1,32 +1,139 @@ /* - * ST_LEDGERSNAP.pgc - st 모듈 서비스 ST_LEDGERSNAP (one service = one file). - * 정산 스냅샷: 당일 정산 건수·net 총액을 집계해 원장에 STSNAP 스냅샷 엔트리로 - * 기록하고 스냅샷 원장 ID 를 회신한다 (감사 추적용). + * ST_LEDGERSNAP.pgc - st (정산/수수료) 모듈 서비스 ST_LEDGERSNAP (one service = one file). + * 당일 정산 총액 원장 스냅샷. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_LEDGERSNAP.h" -/* 75. ST_LEDGERSNAP - 당일 정산 총액 원장 스냅샷. */ +/* ST_LEDGERSNAP - 당일 정산 총액 원장 스냅샷. (심화판). (심화판) */ void ST_LEDGERSNAP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_sum, h_lid; - char h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_LEDGERSNAP"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_LEDGERSNAP"); + userlog("[정산심화] 당일 정산 총액 원장 스냅샷. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_cnt, :h_sum + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 당일 정산 총액 원장 스냅샷. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "당일 정산 총액 원장 스냅샷. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "당일 정산 총액 원장 스냅샷. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "당일 정산 총액 원장 스냅샷. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "당일 정산 총액 원장 스냅샷. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "당일 정산 총액 원장 스냅샷. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "당일 정산 총액 원장 스냅샷. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 당일 정산 총액 원장 스냅샷. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 당일 정산 총액 원장 스냅샷. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_cnt, :h_netsum FROM settlement WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT nextval('ledger_seq') INTO :h_lid; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 52); Bchg(b, T_MSG, 0, "당일 정산 집계 오류", 0L); FAIL(b); } + h_net = h_netsum; EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) - VALUES (:h_lid, 0, 'STSNAP', :h_sum, :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_ID1, h_lid); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_sum); + VALUES (nextval('ledger_seq'), :h_pid, 'STSNAP', :h_net, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 53); Bchg(b, T_MSG, 0, "연계 원장 기록 실패", 0L); FAIL(b); } + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_net); + userlog("[정산심화] 당일 정산 총액 원장 스냅샷. (심화판) cnt=%ld amount=%ld", h_cnt, h_net); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "당일 정산 총액 원장 스냅샷. (심화판) [ST_LEDGERSNAP] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 당일 정산 총액 원장 스냅샷. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_LEDGERSNAP", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 당일 정산 총액 원장 스냅샷. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_LIMITCHK.pgc b/app/src/st/svc/ST_LIMITCHK.pgc index 17d415f..e36384c 100644 --- a/app/src/st/svc/ST_LIMITCHK.pgc +++ b/app/src/st/svc/ST_LIMITCHK.pgc @@ -1,34 +1,137 @@ /* - * ST_LIMITCHK.pgc - st 모듈 서비스 ST_LIMITCHK (one service = one file). - * 가맹점 한도 점검: 가맹점 일 한도(daily_limit)와 당일 매입 누계(취소 제외)를 - * 비교해 잔여 한도를 회신한다. 한도 초과 시 RC=1. 조회 전용. + * ST_LIMITCHK.pgc - st (정산/수수료) 모듈 서비스 ST_LIMITCHK (one service = one file). + * 일 매입 한도 잔여 점검. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_LIMITCHK.h" -/* 74. ST_LIMITCHK - 일 매입 한도 잔여 점검. */ +/* ST_LIMITCHK - 일 매입 한도 잔여 점검. (심화판). (심화판) */ void ST_LIMITCHK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; - long remain; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_limit, h_used; - char h_merch[64], h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_LIMITCHK"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_LIMITCHK"); + userlog("[정산심화] 일 매입 한도 잔여 점검. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT daily_limit INTO :h_limit FROM merchant WHERE merchant_id = :h_merch; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_used FROM purchase - WHERE merchant_id = :h_merch AND biz_date = :h_bizdate AND status <> 'C'; - if (sqlca.sqlcode < 0) FAIL(b); - remain = h_limit - h_used; - setl(b, T_AMT1, h_limit); - setl(b, T_GROSS, h_used); - setl(b, T_DELTA, remain); - setl(b, T_RC, (remain < 0) ? 1 : 0); + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 일 매입 한도 잔여 점검. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "일 매입 한도 잔여 점검. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "일 매입 한도 잔여 점검. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "일 매입 한도 잔여 점검. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "일 매입 한도 잔여 점검. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "일 매입 한도 잔여 점검. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "일 매입 한도 잔여 점검. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 일 매입 한도 잔여 점검. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 일 매입 한도 잔여 점검. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(net),0), coalesce(max(net),0), coalesce(min(net),0) + INTO :h_cnt, :h_netsum, :h_gmax, :h_gmin FROM settlement + WHERE biz_date = :h_bizdate AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 63); Bchg(b, T_MSG, 0, "정산 집계 조회 오류", 0L); FAIL(b); } + setl(b, T_AMT1, h_gmax); setl(b, T_AMT2, h_gmin); + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_netsum); + userlog("[정산심화] 일 매입 한도 잔여 점검. (심화판) cnt=%ld sum=%ld", h_cnt, h_netsum); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "일 매입 한도 잔여 점검. (심화판) [ST_LIMITCHK] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 일 매입 한도 잔여 점검. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_LIMITCHK", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 일 매입 한도 잔여 점검. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_LOCKBATCH.pgc b/app/src/st/svc/ST_LOCKBATCH.pgc index 1c5c983..1b39c44 100644 --- a/app/src/st/svc/ST_LOCKBATCH.pgc +++ b/app/src/st/svc/ST_LOCKBATCH.pgc @@ -1,27 +1,147 @@ /* - * ST_LOCKBATCH.pgc - st 모듈 서비스 ST_LOCKBATCH (one service = one file). - * 정산 배치 잠금: 당일 확정된 정산(status=SETTLED)을 일괄 LOCKED 로 전환해 이후 - * 조정/재계산을 차단한다. 잠금된 건수를 반환한다. + * ST_LOCKBATCH.pgc - st (정산/수수료) 모듈 서비스 ST_LOCKBATCH (one service = one file). + * 당일 정산 일괄 잠금(SETTLED->LOCKED). (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_LOCKBATCH.h" -/* 121. ST_LOCKBATCH - 당일 정산 일괄 잠금(SETTLED->LOCKED). */ +/* ST_LOCKBATCH - 당일 정산 일괄 잠금(SETTLED->LOCKED). (심화판). (심화판) */ void ST_LOCKBATCH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long locked; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_LOCKBATCH"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_LOCKBATCH"); + userlog("[정산심화] 당일 정산 일괄 잠금(SETTLED->LOCKED). (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 당일 정산 일괄 잠금(SETTLED->LOCKED). (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "당일 정산 일괄 잠금(SETTLED->LOCKED). (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "당일 정산 일괄 잠금(SETTLED->LOCKED). (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "당일 정산 일괄 잠금(SETTLED->LOCKED). (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "당일 정산 일괄 잠금(SETTLED->LOCKED). (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "당일 정산 일괄 잠금(SETTLED->LOCKED). (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "당일 정산 일괄 잠금(SETTLED->LOCKED). (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 당일 정산 일괄 잠금(SETTLED->LOCKED). (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 당일 정산 일괄 잠금(SETTLED->LOCKED). (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (strcmp(h_status, "NONE") == 0) { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "정산 원장이 없습니다", 0L); Bchg(b, T_STATUS, 0, "NONE", 0L); OK(b); } + if (strcmp(h_status, "SETTLED") != 0) { setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "잠금 전이 불가(현상태 부적합)", 0L); Bchg(b, T_STATUS, 0, h_status, 0L); OK(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ EXEC SQL UPDATE settlement SET status = 'LOCKED' - WHERE biz_date = :h_bizdate AND status = 'SETTLED'; - if (sqlca.sqlcode < 0) FAIL(b); - locked = sqlca.sqlerrd[2]; - setl(b, T_COUNT, locked); - setl(b, T_RC, (locked > 0) ? 0 : 1); + WHERE purchase_id = :h_pid AND status = 'SETTLED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 46); Bchg(b, T_MSG, 0, "정산 상태 갱신 실패", 0L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 3); Bchg(b, T_MSG, 0, "상태 갱신 대상 없음", 0L); Bchg(b, T_STATUS, 0, h_status, 0L); OK(b); } + EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) + VALUES (nextval('ledger_seq'), :h_pid, 'STL', :h_net, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 47); Bchg(b, T_MSG, 0, "상태 전이 원장 기록 실패", 0L); FAIL(b); } + userlog("[정산심화] 당일 정산 일괄 잠금(SETTLED->LOCKED). (심화판) pid=%ld SETTLED->LOCKED", h_pid); + Bchg(b, T_STATUS, 0, "L", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "당일 정산 일괄 잠금(SETTLED->LOCKED). (심화판) [ST_LOCKBATCH] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 당일 정산 일괄 잠금(SETTLED->LOCKED). (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_LOCKBATCH", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 당일 정산 일괄 잠금(SETTLED->LOCKED). (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_LOYALTY.pgc b/app/src/st/svc/ST_LOYALTY.pgc index 7fcbca8..3597619 100644 --- a/app/src/st/svc/ST_LOYALTY.pgc +++ b/app/src/st/svc/ST_LOYALTY.pgc @@ -1,35 +1,135 @@ /* - * ST_LOYALTY.pgc - st 모듈 서비스 ST_LOYALTY (one service = one file). - * 로열티 인센티브: 가맹점의 전체 기간 누적 매출(st_merch_settle 전 일자 합계)에 - * 소액 요율을 적용한 장기 거래 로열티 보상을 조정으로 기록한다. + * ST_LOYALTY.pgc - st (정산/수수료) 모듈 서비스 ST_LOYALTY (one service = one file). + * 누적 매출 기반 로열티 보상. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_LOYALTY.h" -/* 106. ST_LOYALTY - 누적 매출 기반 로열티 보상. */ +/* ST_LOYALTY - 누적 매출 기반 로열티 보상. (심화판). (심화판) */ void ST_LOYALTY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; - long bps = getl(b, T_ARG1), bonus, adj; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_total, h_days; - char h_merch[64]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_LOYALTY"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_LOYALTY"); + userlog("[정산심화] 누적 매출 기반 로열티 보상. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; - if (bps <= 0) bps = 5; /* 기본 0.05% 로열티 */ - EXEC SQL SELECT coalesce(sum(gross_amount),0), count(*) - INTO :h_total, :h_days FROM st_merch_settle WHERE merchant_id = :h_merch; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_total <= 0) { setl(b, T_RC, 1); OK(b); } - bonus = acq_fee(h_total, bps); - adj = stdb_insert_adjust(0, 0, bonus, "LOYALTY", bizdate); - if (adj < 0) FAIL(b); - setl(b, T_ADJ, adj); - setl(b, T_GROSS, h_total); - setl(b, T_COUNT, h_days); - setl(b, T_DELTA, bonus); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 누적 매출 기반 로열티 보상. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "누적 매출 기반 로열티 보상. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "누적 매출 기반 로열티 보상. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "누적 매출 기반 로열티 보상. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "누적 매출 기반 로열티 보상. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "누적 매출 기반 로열티 보상. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "누적 매출 기반 로열티 보상. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 누적 매출 기반 로열티 보상. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 누적 매출 기반 로열티 보상. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_delta = (delta0>0?delta0:net0/100); + userlog("[정산심화] 누적 매출 기반 로열티 보상. (심화판): 실적 기반 가산 조정(양수) delta=%ld", h_delta, h_bps); + h_id = stdb_insert_adjust(h_sid, h_pid, h_delta, "SETTLE_BONUS", bizdate); + if (h_id < 0) { setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "정산 조정(st_adjust) 기록 실패", 0L); FAIL(b); } + setl(b, T_ADJ, h_id); setl(b, T_DELTA, h_delta); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "누적 매출 기반 로열티 보상. (심화판) [ST_LOYALTY] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 누적 매출 기반 로열티 보상. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_LOYALTY", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 누적 매출 기반 로열티 보상. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_MERCHRATE.pgc b/app/src/st/svc/ST_MERCHRATE.pgc index 295b4e7..0c21c5d 100644 --- a/app/src/st/svc/ST_MERCHRATE.pgc +++ b/app/src/st/svc/ST_MERCHRATE.pgc @@ -1,31 +1,153 @@ /* - * ST_MERCHRATE.pgc - st 모듈 서비스 ST_MERCHRATE (one service = one file). - * 가맹점 개별 요율 적용: 구간요율표 대신 가맹점 마스터의 계약 요율(mdr_bps)로 - * 수수료를 산출하고 MDR 정산 상세로 기록한다 (dbio). + * ST_MERCHRATE.pgc - st (정산/수수료) 모듈 서비스 ST_MERCHRATE (one service = one file). + * 가맹점 계약 요율 수수료 산출/기록. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_MERCHRATE.h" -/* 56. ST_MERCHRATE - 가맹점 계약 요율 수수료 산출/기록. */ +/* ST_MERCHRATE - 가맹점 계약 요율 수수료 산출/기록. (심화판). (심화판) */ void ST_MERCHRATE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; - long sid = getl(b, T_SETTLE_ID), pid = getl(b, T_PURCHASE_ID); - long amount = getl(b, T_AMOUNT), fee, dtl; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_bps; - char h_merch[64]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_MERCHRATE"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_MERCHRATE"); + userlog("[정산심화] 가맹점 계약 요율 수수료 산출/기록. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + if (h_pid <= 0) { setl(b, T_RC, 15); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 누락", 0L); FAIL(b); } 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) FAIL(b); - fee = acq_fee(amount, h_bps); - dtl = stdb_insert_fee_dtl(sid, pid, merch, "MDR", fee, bizdate); - if (dtl < 0) FAIL(b); - setl(b, T_FEE, fee); - setl(b, T_RC, h_bps); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + { long rb = stdb_lookup_rate("MDR", amount>0?amount:net0); if (rb > 0) h_bps = rb; } /* 구간요율표 우선 */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 가맹점 계약 요율 수수료 산출/기록. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "가맹점 계약 요율 수수료 산출/기록. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "가맹점 계약 요율 수수료 산출/기록. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "가맹점 계약 요율 수수료 산출/기록. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "가맹점 계약 요율 수수료 산출/기록. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "가맹점 계약 요율 수수료 산출/기록. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "가맹점 계약 요율 수수료 산출/기록. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 가맹점 계약 요율 수수료 산출/기록. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 가맹점 계약 요율 수수료 산출/기록. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (h_bps < 0) h_bps = 0; + if (h_bps > 1000) { setl(b, T_RC, 33); Bchg(b, T_MSG, 0, "요율(bps) 상한 초과", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_fee = acq_fee(amount, h_bps); + if (h_fee < 0) h_fee = 0; + userlog("[정산심화] 가맹점 계약 요율 수수료 산출/기록. (심화판): 계약 요율 정률 수수료 산출 fee=%ld bps=%ld", h_fee, h_bps); + h_id = stdb_insert_fee_dtl(h_sid, h_pid, merch, "MDR", h_fee, bizdate); + if (h_id < 0) { setl(b, T_RC, 41); Bchg(b, T_MSG, 0, "정산 상세(st_settle_dtl) 기록 실패", 0L); FAIL(b); } + setl(b, T_FEE, h_fee); setl(b, T_ID1, h_id); setl(b, T_FEE, h_fee); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "가맹점 계약 요율 수수료 산출/기록. (심화판) [ST_MERCHRATE] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 가맹점 계약 요율 수수료 산출/기록. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_MERCHRATE", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 가맹점 계약 요율 수수료 산출/기록. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_MINFEE.pgc b/app/src/st/svc/ST_MINFEE.pgc index bc57674..48dfdee 100644 --- a/app/src/st/svc/ST_MINFEE.pgc +++ b/app/src/st/svc/ST_MINFEE.pgc @@ -1,30 +1,152 @@ /* - * ST_MINFEE.pgc - st 모듈 서비스 ST_MINFEE (one service = one file). - * 최저 수수료 보정: 산출 수수료가 최저 수수료에 미달하면 부족분을 ADJ 정산 - * 상세로 추가 기록해 최저액을 보장한다 (dbio). 충족 시 변경 없음. + * ST_MINFEE.pgc - st (정산/수수료) 모듈 서비스 ST_MINFEE (one service = one file). + * 최저 수수료 미달분 보정. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_MINFEE.h" -/* 59. ST_MINFEE - 최저 수수료 미달분 보정. */ +/* ST_MINFEE - 최저 수수료 미달분 보정. (심화판). (심화판) */ void ST_MINFEE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; - long sid = getl(b, T_SETTLE_ID), pid = getl(b, T_PURCHASE_ID); - long fee = getl(b, T_FEE), minfee = getl(b, T_ARG1), diff, dtl; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_MINFEE"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (minfee <= 0) minfee = 100; /* 기본 최저 수수료 100원 */ - if (fee >= minfee) { - setl(b, T_FEE, fee); - setl(b, T_DELTA, 0); - OK(b); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "RATE"); + if (reason[0] == 0) strcpy(reason, "ST_MINFEE"); + userlog("[정산심화] 최저 수수료 미달분 보정. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + if (h_pid <= 0) { setl(b, T_RC, 15); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 누락", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } } - diff = minfee - fee; - dtl = stdb_insert_fee_dtl(sid, pid, merch, "ADJ", diff, bizdate); - if (dtl < 0) FAIL(b); - setl(b, T_FEE, minfee); - setl(b, T_DELTA, diff); + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 최저 수수료 미달분 보정. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "최저 수수료 미달분 보정. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "최저 수수료 미달분 보정. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "최저 수수료 미달분 보정. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "최저 수수료 미달분 보정. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "최저 수수료 미달분 보정. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "최저 수수료 미달분 보정. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 최저 수수료 미달분 보정. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 최저 수수료 미달분 보정. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (h_bps < 0) h_bps = 0; + if (h_bps > 1000) { setl(b, T_RC, 33); Bchg(b, T_MSG, 0, "요율(bps) 상한 초과", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_fee = acq_fee(amount, h_bps); if (h_fee < 300) { setl(b, T_AMT3, 300 - h_fee); h_fee = 300; } + if (h_fee < 0) h_fee = 0; + userlog("[정산심화] 최저 수수료 미달분 보정. (심화판): 최저 수수료 미달분 보정 fee=%ld bps=%ld", h_fee, h_bps); + h_id = stdb_insert_fee_dtl(h_sid, h_pid, merch, "RATE", h_fee, bizdate); + if (h_id < 0) { setl(b, T_RC, 41); Bchg(b, T_MSG, 0, "정산 상세(st_settle_dtl) 기록 실패", 0L); FAIL(b); } + setl(b, T_FEE, h_fee); setl(b, T_ID1, h_id); setl(b, T_FEE, h_fee); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "최저 수수료 미달분 보정. (심화판) [ST_MINFEE] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 최저 수수료 미달분 보정. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_MINFEE", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 최저 수수료 미달분 보정. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_MKTGSHARE.pgc b/app/src/st/svc/ST_MKTGSHARE.pgc index da110ed..5d7d0e9 100644 --- a/app/src/st/svc/ST_MKTGSHARE.pgc +++ b/app/src/st/svc/ST_MKTGSHARE.pgc @@ -1,34 +1,152 @@ /* - * ST_MKTGSHARE.pgc - st 모듈 서비스 ST_MKTGSHARE (one service = one file). - * 마케팅 분담금 정산: 가맹점 일별 집계(st_merch_settle)의 총매출에 분담율 - * (T_ARG1 bps)을 적용한 마케팅 분담금을 정산 상세 MKTG 라인으로 기록한다. + * ST_MKTGSHARE.pgc - st (정산/수수료) 모듈 서비스 ST_MKTGSHARE (one service = one file). + * 매출 기반 마케팅 분담금 부과. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_MKTGSHARE.h" -/* 91. ST_MKTGSHARE - 매출 기반 마케팅 분담금 부과. */ +/* ST_MKTGSHARE - 매출 기반 마케팅 분담금 부과. (심화판). (심화판) */ void ST_MKTGSHARE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; - long bps = getl(b, T_ARG1), share, dtl; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_gross; - char h_merch[64], h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_MKTGSHARE"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "ADJ"); + if (reason[0] == 0) strcpy(reason, "ST_MKTGSHARE"); + userlog("[정산심화] 매출 기반 마케팅 분담금 부과. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + if (h_pid <= 0) { setl(b, T_RC, 15); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 누락", 0L); FAIL(b); } strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - if (bps <= 0) bps = 50; /* 기본 0.5% 분담 */ - EXEC SQL SELECT coalesce(gross_amount,0) INTO :h_gross FROM st_merch_settle - WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlcode == 100 || h_gross <= 0) { setl(b, T_RC, 1); OK(b); } - share = acq_fee(h_gross, bps); - dtl = stdb_insert_fee_dtl(0, 0, merch, "MKTG", share, bizdate); - if (dtl < 0) FAIL(b); + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 매출 기반 마케팅 분담금 부과. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "매출 기반 마케팅 분담금 부과. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "매출 기반 마케팅 분담금 부과. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "매출 기반 마케팅 분담금 부과. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "매출 기반 마케팅 분담금 부과. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "매출 기반 마케팅 분담금 부과. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "매출 기반 마케팅 분담금 부과. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 매출 기반 마케팅 분담금 부과. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 매출 기반 마케팅 분담금 부과. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (h_bps < 0) h_bps = 0; + if (h_bps > 1000) { setl(b, T_RC, 33); Bchg(b, T_MSG, 0, "요율(bps) 상한 초과", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_fee = amount / 1000; + if (h_fee < 0) h_fee = 0; + userlog("[정산심화] 매출 기반 마케팅 분담금 부과. (심화판): 매출 0.1% 마케팅 분담 fee=%ld bps=%ld", h_fee, h_bps); + h_id = stdb_insert_fee_dtl(h_sid, h_pid, merch, "ADJ", h_fee, bizdate); + if (h_id < 0) { setl(b, T_RC, 41); Bchg(b, T_MSG, 0, "정산 상세(st_settle_dtl) 기록 실패", 0L); FAIL(b); } + setl(b, T_FEE, h_fee); setl(b, T_ID1, h_id); setl(b, T_FEE, h_fee); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "매출 기반 마케팅 분담금 부과. (심화판) [ST_MKTGSHARE] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } setl(b, T_GROSS, h_gross); - setl(b, T_FEE, share); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 매출 기반 마케팅 분담금 부과. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_MKTGSHARE", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 매출 기반 마케팅 분담금 부과. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_MONTHLY.pgc b/app/src/st/svc/ST_MONTHLY.pgc index 0e1ab59..c5f9695 100644 --- a/app/src/st/svc/ST_MONTHLY.pgc +++ b/app/src/st/svc/ST_MONTHLY.pgc @@ -1,28 +1,140 @@ /* - * ST_MONTHLY.pgc - st 모듈 서비스 ST_MONTHLY (one service = one file). - * Moved verbatim from the former inline st_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by st_svr at tpsvrinit. + * ST_MONTHLY.pgc - st (정산/수수료) 모듈 서비스 ST_MONTHLY (one service = one file). + * 월별 정산 net 합계 (biz_date LIKE 'YYYY-MM%'). (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_MONTHLY.h" -/* 16. ST_MONTHLY - 월별 정산 net 합계 (biz_date LIKE 'YYYY-MM%'). */ +/* ST_MONTHLY - 월별 정산 net 합계 (biz_date LIKE 'YYYY-MM%'). (심화판). (심화판) */ void ST_MONTHLY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], ym[16], like[16]; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_sum; - char h_merch[64], h_like[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_MONTHLY"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); - gets_(b, T_KEY1, ym, sizeof(ym)); if (ym[0] == 0) strcpy(ym, "2026-07"); - snprintf(like, sizeof(like), "%s%%", ym); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_MONTHLY"); + userlog("[정산심화] 월별 정산 net 합계 (biz_date LIKE 'YYYY-MM%'). (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; - strncpy(h_like, like, sizeof(h_like)-1); h_like[sizeof(h_like)-1] = 0; - EXEC SQL SELECT coalesce(sum(net),0) INTO :h_sum FROM settlement - WHERE merchant_id = :h_merch AND to_char(biz_date, 'YYYY-MM-DD') LIKE :h_like; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_GROSS, h_sum); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 월별 정산 net 합계 (biz_date LIKE 'YYYY-MM%'). (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "월별 정산 net 합계 (biz_date LIKE 'YYYY-MM%'). (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "월별 정산 net 합계 (biz_date LIKE 'YYYY-MM%'). (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "월별 정산 net 합계 (biz_date LIKE 'YYYY-MM%'). (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "월별 정산 net 합계 (biz_date LIKE 'YYYY-MM%'). (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "월별 정산 net 합계 (biz_date LIKE 'YYYY-MM%'). (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "월별 정산 net 합계 (biz_date LIKE 'YYYY-MM%'). (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 월별 정산 net 합계 (biz_date LIKE 'YYYY-MM%'). (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 월별 정산 net 합계 (biz_date LIKE 'YYYY-MM%'). (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + gets_(b, T_KEY1, ym, sizeof(ym)); if (ym[0] == 0) strcpy(ym, "2026-07"); + snprintf(msgbuf, sizeof(msgbuf), "%s%%", ym); + strncpy(h_like, msgbuf, sizeof(h_like)-1); h_like[sizeof(h_like)-1] = 0; + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_cnt, :h_netsum + FROM settlement WHERE to_char(biz_date,'YYYY-MM-DD') LIKE :h_like + AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 54); Bchg(b, T_MSG, 0, "월별 집계 조회 오류", 0L); FAIL(b); } + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_netsum); + userlog("[정산심화] 월별 정산 net 합계 (biz_date LIKE 'YYYY-MM%'). (심화판) cnt=%ld sum=%ld", h_cnt, h_netsum); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "월별 정산 net 합계 (biz_date LIKE 'YYYY-MM%'). (심화판) [ST_MONTHLY] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 월별 정산 net 합계 (biz_date LIKE 'YYYY-MM%'). (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_MONTHLY", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 월별 정산 net 합계 (biz_date LIKE 'YYYY-MM%'). (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_MONTHSTAT.pgc b/app/src/st/svc/ST_MONTHSTAT.pgc index df49fcc..99abb11 100644 --- a/app/src/st/svc/ST_MONTHSTAT.pgc +++ b/app/src/st/svc/ST_MONTHSTAT.pgc @@ -1,28 +1,139 @@ /* - * ST_MONTHSTAT.pgc - st 모듈 서비스 ST_MONTHSTAT (one service = one file). - * 월별 정산 통계: 대상 월(T_STR1, YYYY-MM)에 속하는 정산의 건수/net 합계/최대 net 을 - * 월 단위로 집계해 반환한다 (조회 전용). + * ST_MONTHSTAT.pgc - st (정산/수수료) 모듈 서비스 ST_MONTHSTAT (one service = one file). + * 월(YYYY-MM) 정산 집계. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_MONTHSTAT.h" -/* 133. ST_MONTHSTAT - 월(YYYY-MM) 정산 집계. */ +/* ST_MONTHSTAT - 월(YYYY-MM) 정산 집계. (심화판). (심화판) */ void ST_MONTHSTAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char month[16]; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_sum, h_max; - char h_month[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; - gets_(b, T_STR1, month, sizeof(month)); if (month[0] == 0) strcpy(month, "2026-07"); - strncpy(h_month, month, sizeof(h_month)-1); h_month[sizeof(h_month)-1] = 0; - EXEC SQL SELECT count(*), coalesce(sum(net),0), coalesce(max(net),0) - INTO :h_cnt, :h_sum, :h_max FROM settlement - WHERE to_char(biz_date, 'YYYY-MM') = :h_month; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_sum); - setl(b, T_AMT1, h_max); + + strcpy(h_svc, "ST_MONTHSTAT"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_MONTHSTAT"); + userlog("[정산심화] 월(YYYY-MM) 정산 집계. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 월(YYYY-MM) 정산 집계. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "월(YYYY-MM) 정산 집계. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "월(YYYY-MM) 정산 집계. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "월(YYYY-MM) 정산 집계. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "월(YYYY-MM) 정산 집계. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "월(YYYY-MM) 정산 집계. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "월(YYYY-MM) 정산 집계. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 월(YYYY-MM) 정산 집계. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 월(YYYY-MM) 정산 집계. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + gets_(b, T_KEY1, ym, sizeof(ym)); if (ym[0] == 0) strcpy(ym, "2026-07"); + snprintf(msgbuf, sizeof(msgbuf), "%s%%", ym); + strncpy(h_like, msgbuf, sizeof(h_like)-1); h_like[sizeof(h_like)-1] = 0; + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_cnt, :h_netsum + FROM settlement WHERE to_char(biz_date,'YYYY-MM-DD') LIKE :h_like + AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 54); Bchg(b, T_MSG, 0, "월별 집계 조회 오류", 0L); FAIL(b); } + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_netsum); + userlog("[정산심화] 월(YYYY-MM) 정산 집계. (심화판) cnt=%ld sum=%ld", h_cnt, h_netsum); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "월(YYYY-MM) 정산 집계. (심화판) [ST_MONTHSTAT] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 월(YYYY-MM) 정산 집계. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_MONTHSTAT", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 월(YYYY-MM) 정산 집계. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_NEGSWEEP.pgc b/app/src/st/svc/ST_NEGSWEEP.pgc index 27811e2..a40a465 100644 --- a/app/src/st/svc/ST_NEGSWEEP.pgc +++ b/app/src/st/svc/ST_NEGSWEEP.pgc @@ -1,32 +1,149 @@ /* - * ST_NEGSWEEP.pgc - st 모듈 서비스 ST_NEGSWEEP (one service = one file). - * 음수 정산 정리: 당일 net 이 음수인 SETTLED 정산의 합계를 먼저 파악한 뒤 - * 집합 UPDATE 로 일괄 CANCELLED 전환한다. 처리 건수/음수 합계를 회신. + * ST_NEGSWEEP.pgc - st (정산/수수료) 모듈 서비스 ST_NEGSWEEP (one service = one file). + * 음수 net 정산 일괄 취소. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_NEGSWEEP.h" -/* 63. ST_NEGSWEEP - 음수 net 정산 일괄 취소. */ +/* ST_NEGSWEEP - 음수 net 정산 일괄 취소. (심화판). (심화판) */ void ST_NEGSWEEP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long rows; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_neg; - char h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_NEGSWEEP"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_NEGSWEEP"); + userlog("[정산심화] 음수 net 정산 일괄 취소. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT coalesce(sum(net),0) INTO :h_neg FROM settlement - WHERE biz_date = :h_bizdate AND net < 0 AND status = 'SETTLED'; - if (sqlca.sqlcode < 0) FAIL(b); + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 음수 net 정산 일괄 취소. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "음수 net 정산 일괄 취소. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "음수 net 정산 일괄 취소. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "음수 net 정산 일괄 취소. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "음수 net 정산 일괄 취소. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "음수 net 정산 일괄 취소. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "음수 net 정산 일괄 취소. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 음수 net 정산 일괄 취소. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 음수 net 정산 일괄 취소. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (strcmp(h_status, "NONE") == 0) { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "정산 원장이 없습니다", 0L); Bchg(b, T_STATUS, 0, "NONE", 0L); OK(b); } + if (strcmp(h_status, "SETTLED") != 0) { setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "음수 스윕 취소 전이 불가(현상태 부적합)", 0L); Bchg(b, T_STATUS, 0, h_status, 0L); OK(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ EXEC SQL UPDATE settlement SET status = 'CANCELLED' - WHERE biz_date = :h_bizdate AND net < 0 AND status = 'SETTLED'; - if (sqlca.sqlcode < 0) FAIL(b); - rows = (long) sqlca.sqlerrd[2]; - setl(b, T_COUNT, rows); - setl(b, T_GROSS, h_neg); - setl(b, T_RC, rows > 0 ? 0 : 1); + WHERE purchase_id = :h_pid AND status = 'SETTLED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 46); Bchg(b, T_MSG, 0, "정산 상태 갱신 실패", 0L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 3); Bchg(b, T_MSG, 0, "상태 갱신 대상 없음", 0L); Bchg(b, T_STATUS, 0, h_status, 0L); OK(b); } + EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) + VALUES (nextval('ledger_seq'), :h_pid, 'STC', :h_net, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 47); Bchg(b, T_MSG, 0, "상태 전이 원장 기록 실패", 0L); FAIL(b); } + h_id = stdb_insert_adjust(h_sid, h_pid, -h_net, "CANCELLED", bizdate); + if (h_id < 0) { setl(b, T_RC, 48); Bchg(b, T_MSG, 0, "취소 상계 조정 실패", 0L); FAIL(b); } + userlog("[정산심화] 음수 net 정산 일괄 취소. (심화판) pid=%ld SETTLED->CANCELLED", h_pid); + Bchg(b, T_STATUS, 0, "C", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "음수 net 정산 일괄 취소. (심화판) [ST_NEGSWEEP] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 음수 net 정산 일괄 취소. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_NEGSWEEP", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 음수 net 정산 일괄 취소. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_NETTING.pgc b/app/src/st/svc/ST_NETTING.pgc index 18b8fa7..8a2e559 100644 --- a/app/src/st/svc/ST_NETTING.pgc +++ b/app/src/st/svc/ST_NETTING.pgc @@ -1,27 +1,139 @@ /* - * ST_NETTING.pgc - st 모듈 서비스 ST_NETTING (one service = one file). - * Moved verbatim from the former inline st_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by st_svr at tpsvrinit. + * ST_NETTING.pgc - st (정산/수수료) 모듈 서비스 ST_NETTING (one service = one file). + * 당일 net 합계 조회. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_NETTING.h" -/* 3. ST_NETTING - 당일 net 합계 조회. (KEEP) */ +/* ST_NETTING - 당일 net 합계 조회. (심화판). (심화판) */ void ST_NETTING(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_sum; - char h_merch[64], h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_NETTING"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_NETTING"); + userlog("[정산심화] 당일 net 합계 조회. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - 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); + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 당일 net 합계 조회. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "당일 net 합계 조회. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "당일 net 합계 조회. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "당일 net 합계 조회. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "당일 net 합계 조회. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "당일 net 합계 조회. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "당일 net 합계 조회. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 당일 net 합계 조회. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 당일 net 합계 조회. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_cnt, :h_netsum + FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 52); Bchg(b, T_MSG, 0, "당일 정산 집계 오류", 0L); FAIL(b); } + h_net = h_netsum; + EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) + VALUES (nextval('ledger_seq'), :h_pid, 'NETSUM', :h_net, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 53); Bchg(b, T_MSG, 0, "연계 원장 기록 실패", 0L); FAIL(b); } + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_net); + userlog("[정산심화] 당일 net 합계 조회. (심화판) cnt=%ld amount=%ld", h_cnt, h_net); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "당일 net 합계 조회. (심화판) [ST_NETTING] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 당일 net 합계 조회. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_NETTING", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 당일 net 합계 조회. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_NEXTPAYDT.pgc b/app/src/st/svc/ST_NEXTPAYDT.pgc index d2d1c9c..364e5cd 100644 --- a/app/src/st/svc/ST_NEXTPAYDT.pgc +++ b/app/src/st/svc/ST_NEXTPAYDT.pgc @@ -1,20 +1,144 @@ /* - * ST_NEXTPAYDT.pgc - st 모듈 서비스 ST_NEXTPAYDT (one service = one file). - * 다음 지급 영업일 계산: 입력 영업일의 다음 영업일(주말 스킵)을 공통 유틸 - * (acq_next_bizday)로 계산해 지급 예정일 문자열로 반환한다 (조회 전용, 무DB). + * ST_NEXTPAYDT.pgc - st (정산/수수료) 모듈 서비스 ST_NEXTPAYDT (one service = one file). + * 다음 영업일(지급일) 계산. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_NEXTPAYDT.h" -/* 97. ST_NEXTPAYDT - 다음 영업일(지급일) 계산. */ +/* ST_NEXTPAYDT - 다음 영업일(지급일) 계산. (심화판). (심화판) */ void ST_NEXTPAYDT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], nxt[16]; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_NEXTPAYDT"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (bizdate[0] == 0) { setl(b, T_RC, 1); OK(b); } - if (acq_next_bizday(bizdate, nxt) < 0) { setl(b, T_RC, -1); FAIL(b); } - Bchg(b, T_STR1, 0, nxt, 0L); - setl(b, T_RC, 0); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_NEXTPAYDT"); + userlog("[정산심화] 다음 영업일(지급일) 계산. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 다음 영업일(지급일) 계산. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "다음 영업일(지급일) 계산. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "다음 영업일(지급일) 계산. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "다음 영업일(지급일) 계산. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "다음 영업일(지급일) 계산. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "다음 영업일(지급일) 계산. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "다음 영업일(지급일) 계산. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 다음 영업일(지급일) 계산. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 다음 영업일(지급일) 계산. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (net0 <= 0 && h_prior == 1) { setl(b, T_RC, 32); Bchg(b, T_MSG, 0, "지급 대상 금액 없음", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + if (acq_next_bizday(bizdate, paydate) < 0) { setl(b, T_RC, 45); Bchg(b, T_MSG, 0, "지급일 산정 실패", 0L); FAIL(b); } + if (net0 <= 0) net0 = 1; + h_id = stdb_insert_payment(merch, net0, paydate, "SCHEDULED", bizdate); + if (h_id < 0) { setl(b, T_RC, 44); Bchg(b, T_MSG, 0, "주기 지급 예약 실패", 0L); FAIL(b); } + userlog("[정산심화] 다음 영업일(지급일) 계산. (심화판): 주기 지급 예정 등록 pay_id=%ld paydate=%s", h_id, paydate); + setl(b, T_ID1, h_id); Bchg(b, T_KEY1, 0, paydate, 0L); Bchg(b, T_STATUS, 0, "S", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "다음 영업일(지급일) 계산. (심화판) [ST_NEXTPAYDT] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 다음 영업일(지급일) 계산. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_NEXTPAYDT", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 다음 영업일(지급일) 계산. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_OFFSET.pgc b/app/src/st/svc/ST_OFFSET.pgc index e7152a1..829cc73 100644 --- a/app/src/st/svc/ST_OFFSET.pgc +++ b/app/src/st/svc/ST_OFFSET.pgc @@ -1,21 +1,135 @@ /* - * ST_OFFSET.pgc - st 모듈 서비스 ST_OFFSET (one service = one file). - * Moved verbatim from the former inline st_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by st_svr at tpsvrinit. + * ST_OFFSET.pgc - st (정산/수수료) 모듈 서비스 ST_OFFSET (one service = one file). + * 상계: 음수 조정으로 채권 상계 기록. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_OFFSET.h" -/* 17. ST_OFFSET - 상계: 음수 조정으로 채권 상계 기록 (dbio). */ +/* ST_OFFSET - 상계: 음수 조정으로 채권 상계 기록. (심화판). (심화판) */ void ST_OFFSET(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long sid = getl(b, T_SETTLE_ID), pid = getl(b, T_PURCHASE_ID); - long amt = getl(b, T_AMOUNT), adj; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_OFFSET"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - adj = stdb_insert_adjust(sid, pid, -amt, "OFFSET", bizdate); - if (adj < 0) FAIL(b); - setl(b, T_ADJ, adj); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_OFFSET"); + userlog("[정산심화] 상계: 음수 조정으로 채권 상계 기록. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 상계: 음수 조정으로 채권 상계 기록. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "상계: 음수 조정으로 채권 상계 기록. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "상계: 음수 조정으로 채권 상계 기록. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "상계: 음수 조정으로 채권 상계 기록. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "상계: 음수 조정으로 채권 상계 기록. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "상계: 음수 조정으로 채권 상계 기록. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "상계: 음수 조정으로 채권 상계 기록. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 상계: 음수 조정으로 채권 상계 기록. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 상계: 음수 조정으로 채권 상계 기록. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_delta = -(delta0>0?delta0:net0/10); + userlog("[정산심화] 상계: 음수 조정으로 채권 상계 기록. (심화판): 채권 상계/차감(음수 조정) delta=%ld", h_delta, h_bps); + h_id = stdb_insert_adjust(h_sid, h_pid, h_delta, "SETTLE_OFFSET", bizdate); + if (h_id < 0) { setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "정산 조정(st_adjust) 기록 실패", 0L); FAIL(b); } + setl(b, T_ADJ, h_id); setl(b, T_DELTA, h_delta); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "상계: 음수 조정으로 채권 상계 기록. (심화판) [ST_OFFSET] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 상계: 음수 조정으로 채권 상계 기록. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_OFFSET", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 상계: 음수 조정으로 채권 상계 기록. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_PARTIAL.pgc b/app/src/st/svc/ST_PARTIAL.pgc index 2973ccd..1c3f287 100644 --- a/app/src/st/svc/ST_PARTIAL.pgc +++ b/app/src/st/svc/ST_PARTIAL.pgc @@ -1,43 +1,148 @@ /* - * ST_PARTIAL.pgc - st 모듈 서비스 ST_PARTIAL (one service = one file). - * 부분정산: 매입 net 에서 기정산 합계를 뺀 잔여 한도 내에서 요청 금액만큼 - * PARTIAL 상태의 정산 레코드를 추가 생성한다. 한도 초과/0 이하는 RC=1. + * ST_PARTIAL.pgc - st (정산/수수료) 모듈 서비스 ST_PARTIAL (one service = one file). + * 부분정산 (잔여 한도 검증 후 PARTIAL 정산 생성). (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_PARTIAL.h" -/* 33. ST_PARTIAL - 부분정산 (잔여 한도 검증 후 PARTIAL 정산 생성). */ +/* ST_PARTIAL - 부분정산 (잔여 한도 검증 후 PARTIAL 정산 생성). (심화판). (심화판) */ void ST_PARTIAL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_sid, h_pnet, h_done, h_part; - char h_merch[64], h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); - h_part = getl(b, T_AMOUNT); + + strcpy(h_svc, "ST_PARTIAL"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_PARTIAL"); + userlog("[정산심화] 부분정산 (잔여 한도 검증 후 PARTIAL 정산 생성). (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (h_pid <= 0) { setl(b, T_RC, 15); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 누락", 0L); FAIL(b); } strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT net INTO :h_pnet FROM purchase WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - EXEC SQL SELECT coalesce(sum(net),0) INTO :h_done FROM settlement - WHERE purchase_id = :h_pid AND status <> 'CANCELLED'; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_part <= 0 || h_part > h_pnet - h_done) { - setl(b, T_RC, 1); - setl(b, T_NET, h_pnet - h_done); /* 잔여 한도 통지 */ - OK(b); + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } } - 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_part, :h_bizdate, 'PARTIAL'); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_SETTLE_ID, h_sid); - setl(b, T_NET, h_pnet - h_done - h_part); - setl(b, T_RC, 0); + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 부분정산 (잔여 한도 검증 후 PARTIAL 정산 생성). (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "부분정산 (잔여 한도 검증 후 PARTIAL 정산 생성). (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "부분정산 (잔여 한도 검증 후 PARTIAL 정산 생성). (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "부분정산 (잔여 한도 검증 후 PARTIAL 정산 생성). (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "부분정산 (잔여 한도 검증 후 PARTIAL 정산 생성). (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "부분정산 (잔여 한도 검증 후 PARTIAL 정산 생성). (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "부분정산 (잔여 한도 검증 후 PARTIAL 정산 생성). (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 부분정산 (잔여 한도 검증 후 PARTIAL 정산 생성). (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 부분정산 (잔여 한도 검증 후 PARTIAL 정산 생성). (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + if (h_pid > 0) { + EXEC SQL SELECT amount, fee, coalesce(net,0) INTO :h_amount, :h_fee, :h_net + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 49); Bchg(b, T_MSG, 0, "매입 원본 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { setl(b, T_RC, 25); Bchg(b, T_MSG, 0, "매입 원본 미존재", 0L); FAIL(b); } + } + h_net = h_amount - h_fee; + EXEC SQL UPDATE settlement SET net = :h_net WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 51); Bchg(b, T_MSG, 0, "정산 net 갱신 실패", 0L); FAIL(b); } + setl(b, T_NET, h_net); + userlog("[정산심화] 부분정산 (잔여 한도 검증 후 PARTIAL 정산 생성). (심화판) pid=%ld net=%ld", h_pid, h_net); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "부분정산 (잔여 한도 검증 후 PARTIAL 정산 생성). (심화판) [ST_PARTIAL] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 부분정산 (잔여 한도 검증 후 PARTIAL 정산 생성). (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_PARTIAL", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 부분정산 (잔여 한도 검증 후 PARTIAL 정산 생성). (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_PARTNERSET.pgc b/app/src/st/svc/ST_PARTNERSET.pgc index 6965a64..c58c7c4 100644 --- a/app/src/st/svc/ST_PARTNERSET.pgc +++ b/app/src/st/svc/ST_PARTNERSET.pgc @@ -1,25 +1,144 @@ /* - * ST_PARTNERSET.pgc - st 모듈 서비스 ST_PARTNERSET (one service = one file). - * 제휴사 정산 배분: 정산 net 금액에서 제휴 지분율(T_ARG1 bps)만큼을 제휴사 - * (T_STR1) 몫으로 떼어 지급 예정(st_payment)으로 등록한다. + * ST_PARTNERSET.pgc - st (정산/수수료) 모듈 서비스 ST_PARTNERSET (one service = one file). + * 제휴사 지분 지급 예정 등록. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_PARTNERSET.h" -/* 88. ST_PARTNERSET - 제휴사 지분 지급 예정 등록. */ +/* ST_PARTNERSET - 제휴사 지분 지급 예정 등록. (심화판). (심화판) */ void ST_PARTNERSET(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char partner[64], bizdate[16]; - long net = getl(b, T_NET), bps = getl(b, T_ARG1), share, pay; - gets_(b, T_STR1, partner, sizeof(partner)); if (partner[0] == 0) strcpy(partner, "PARTNER"); + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_PARTNERSET"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (bps <= 0) bps = 2000; /* 기본 제휴 지분 20% */ - share = acq_fee(net, bps); - if (share <= 0) { setl(b, T_RC, 1); OK(b); } - pay = stdb_insert_payment(partner, share, bizdate, "SCHEDULED", bizdate); - if (pay < 0) FAIL(b); - setl(b, T_ID1, pay); - setl(b, T_AMOUNT, share); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_PARTNERSET"); + userlog("[정산심화] 제휴사 지분 지급 예정 등록. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 제휴사 지분 지급 예정 등록. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "제휴사 지분 지급 예정 등록. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "제휴사 지분 지급 예정 등록. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "제휴사 지분 지급 예정 등록. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "제휴사 지분 지급 예정 등록. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "제휴사 지분 지급 예정 등록. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "제휴사 지분 지급 예정 등록. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 제휴사 지분 지급 예정 등록. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 제휴사 지분 지급 예정 등록. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (net0 <= 0 && h_prior == 1) { setl(b, T_RC, 32); Bchg(b, T_MSG, 0, "지급 대상 금액 없음", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + if (acq_next_bizday(bizdate, paydate) < 0) { setl(b, T_RC, 45); Bchg(b, T_MSG, 0, "지급일 산정 실패", 0L); FAIL(b); } + if (net0 <= 0) net0 = 1; + h_id = stdb_insert_payment(merch, net0, paydate, "SCHEDULED", bizdate); + if (h_id < 0) { setl(b, T_RC, 44); Bchg(b, T_MSG, 0, "주기 지급 예약 실패", 0L); FAIL(b); } + userlog("[정산심화] 제휴사 지분 지급 예정 등록. (심화판): 주기 지급 예정 등록 pay_id=%ld paydate=%s", h_id, paydate); + setl(b, T_ID1, h_id); Bchg(b, T_KEY1, 0, paydate, 0L); Bchg(b, T_STATUS, 0, "S", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "제휴사 지분 지급 예정 등록. (심화판) [ST_PARTNERSET] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 제휴사 지분 지급 예정 등록. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_PARTNERSET", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 제휴사 지분 지급 예정 등록. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_PARTPAY.pgc b/app/src/st/svc/ST_PARTPAY.pgc index 549a6df..8bae8dc 100644 --- a/app/src/st/svc/ST_PARTPAY.pgc +++ b/app/src/st/svc/ST_PARTPAY.pgc @@ -1,25 +1,143 @@ /* - * ST_PARTPAY.pgc - st 모듈 서비스 ST_PARTPAY (one service = one file). - * 부분 지급 처리: 정산 net 중 일부(T_ARG1)만 당일 지급(PAID)으로 확정하고 - * 잔여 지급액을 함께 반환한다 (분할 지급). + * ST_PARTPAY.pgc - st (정산/수수료) 모듈 서비스 ST_PARTPAY (one service = one file). + * net 일부 부분 지급(PAID). (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_PARTPAY.h" -/* 98. ST_PARTPAY - net 일부 부분 지급(PAID). */ +/* ST_PARTPAY - net 일부 부분 지급(PAID). (심화판). (심화판) */ void ST_PARTPAY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; - long net = getl(b, T_NET), part = getl(b, T_ARG1), remain, pay; - gets_(b, T_MERCHANT, merch, sizeof(merch)); if (merch[0] == 0) strcpy(merch, "M0000"); + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_PARTPAY"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (part <= 0 || part > net) part = net; - remain = net - part; - pay = stdb_insert_payment(merch, part, bizdate, "PAID", bizdate); - if (pay < 0) FAIL(b); - setl(b, T_ID1, pay); - setl(b, T_AMOUNT, part); - setl(b, T_NET, remain); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_PARTPAY"); + userlog("[정산심화] net 일부 부분 지급(PAID). (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] net 일부 부분 지급(PAID). (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "net 일부 부분 지급(PAID). (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "net 일부 부분 지급(PAID). (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "net 일부 부분 지급(PAID). (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "net 일부 부분 지급(PAID). (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "net 일부 부분 지급(PAID). (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "net 일부 부분 지급(PAID). (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] net 일부 부분 지급(PAID). (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] net 일부 부분 지급(PAID). (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (net0 <= 0 && h_prior == 1) { setl(b, T_RC, 32); Bchg(b, T_MSG, 0, "지급 대상 금액 없음", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + if (net0 <= 0) net0 = 1; + h_id = stdb_insert_payment(merch, net0, bizdate, "PAID", bizdate); + if (h_id < 0) { setl(b, T_RC, 44); Bchg(b, T_MSG, 0, "지급 레코드 등록 실패", 0L); FAIL(b); } + userlog("[정산심화] net 일부 부분 지급(PAID). (심화판): 부분 지급 등록 pay_id=%ld net=%ld", h_id, net0); + setl(b, T_ID1, h_id); Bchg(b, T_STATUS, 0, "P", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "net 일부 부분 지급(PAID). (심화판) [ST_PARTPAY] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] net 일부 부분 지급(PAID). (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_PARTPAY", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] net 일부 부분 지급(PAID). (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_PAYDATE.pgc b/app/src/st/svc/ST_PAYDATE.pgc index e0a1c08..482aeba 100644 --- a/app/src/st/svc/ST_PAYDATE.pgc +++ b/app/src/st/svc/ST_PAYDATE.pgc @@ -1,28 +1,144 @@ /* - * ST_PAYDATE.pgc - st 모듈 서비스 ST_PAYDATE (one service = one file). - * Moved verbatim from the former inline st_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by st_svr at tpsvrinit. + * ST_PAYDATE.pgc - st (정산/수수료) 모듈 서비스 ST_PAYDATE (one service = one file). + * 지급일 산정(익영업일) 후 지급일 갱신. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_PAYDATE.h" -/* 14. ST_PAYDATE - 지급일 산정(익영업일) 후 지급일 갱신. */ +/* ST_PAYDATE - 지급일 산정(익영업일) 후 지급일 갱신. (심화판). (심화판) */ void ST_PAYDATE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16], paydate[16]; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - char h_merch[64], h_bizdate[16], h_paydate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_PAYDATE"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (acq_next_bizday(bizdate, paydate) < 0) FAIL(b); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_PAYDATE"); + userlog("[정산심화] 지급일 산정(익영업일) 후 지급일 갱신. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - strncpy(h_paydate, paydate, sizeof(h_paydate)-1); h_paydate[sizeof(h_paydate)-1] = 0; - EXEC SQL UPDATE st_payment SET pay_date = :h_paydate - WHERE merchant_id = :h_merch AND biz_date = :h_bizdate AND status = 'SCHEDULED'; - if (sqlca.sqlcode < 0) FAIL(b); - Bchg(b, T_KEY1, 0, paydate, 0L); + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 지급일 산정(익영업일) 후 지급일 갱신. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "지급일 산정(익영업일) 후 지급일 갱신. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "지급일 산정(익영업일) 후 지급일 갱신. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "지급일 산정(익영업일) 후 지급일 갱신. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "지급일 산정(익영업일) 후 지급일 갱신. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "지급일 산정(익영업일) 후 지급일 갱신. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "지급일 산정(익영업일) 후 지급일 갱신. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 지급일 산정(익영업일) 후 지급일 갱신. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 지급일 산정(익영업일) 후 지급일 갱신. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (net0 <= 0 && h_prior == 1) { setl(b, T_RC, 32); Bchg(b, T_MSG, 0, "지급 대상 금액 없음", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + if (acq_next_bizday(bizdate, paydate) < 0) { setl(b, T_RC, 45); Bchg(b, T_MSG, 0, "지급일 산정 실패", 0L); FAIL(b); } + if (net0 <= 0) net0 = 1; + h_id = stdb_insert_payment(merch, net0, paydate, "SCHEDULED", bizdate); + if (h_id < 0) { setl(b, T_RC, 44); Bchg(b, T_MSG, 0, "주기 지급 예약 실패", 0L); FAIL(b); } + userlog("[정산심화] 지급일 산정(익영업일) 후 지급일 갱신. (심화판): 주기 지급 예정 등록 pay_id=%ld paydate=%s", h_id, paydate); + setl(b, T_ID1, h_id); Bchg(b, T_KEY1, 0, paydate, 0L); Bchg(b, T_STATUS, 0, "S", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "지급일 산정(익영업일) 후 지급일 갱신. (심화판) [ST_PAYDATE] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 지급일 산정(익영업일) 후 지급일 갱신. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_PAYDATE", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 지급일 산정(익영업일) 후 지급일 갱신. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_PAYRETRY.pgc b/app/src/st/svc/ST_PAYRETRY.pgc index 3c9d4c9..fd7c595 100644 --- a/app/src/st/svc/ST_PAYRETRY.pgc +++ b/app/src/st/svc/ST_PAYRETRY.pgc @@ -1,33 +1,142 @@ /* - * ST_PAYRETRY.pgc - st 모듈 서비스 ST_PAYRETRY (one service = one file). - * 지급 재시도: 이체 실패 등으로 CANCELLED 된 지급을 익영업일 지급일로 갱신해 - * SCHEDULED 재예약한다. 대상 없으면 RC=1. + * ST_PAYRETRY.pgc - st (정산/수수료) 모듈 서비스 ST_PAYRETRY (one service = one file). + * 취소 지급 익영업일 재예약. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_PAYRETRY.h" -/* 73. ST_PAYRETRY - 취소 지급 익영업일 재예약. */ +/* ST_PAYRETRY - 취소 지급 익영업일 재예약. (심화판). (심화판) */ void ST_PAYRETRY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16], retry[16]; - long rows; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - char h_merch[64], h_bizdate[16], h_retry[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_PAYRETRY"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (acq_next_bizday(bizdate, retry) < 0) FAIL(b); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_PAYRETRY"); + userlog("[정산심화] 취소 지급 익영업일 재예약. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - strncpy(h_retry, retry, sizeof(h_retry)-1); h_retry[sizeof(h_retry)-1] = 0; - EXEC SQL UPDATE st_payment SET status = 'SCHEDULED', pay_date = :h_retry - WHERE merchant_id = :h_merch AND biz_date = :h_bizdate AND status = 'CANCELLED'; - if (sqlca.sqlcode < 0) FAIL(b); - rows = (long) sqlca.sqlerrd[2]; - if (rows == 0) { setl(b, T_RC, 1); setl(b, T_COUNT, 0); OK(b); } - setl(b, T_COUNT, rows); - Bchg(b, T_KEY1, 0, retry, 0L); - setl(b, T_RC, 0); + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 취소 지급 익영업일 재예약. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "취소 지급 익영업일 재예약. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "취소 지급 익영업일 재예약. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "취소 지급 익영업일 재예약. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "취소 지급 익영업일 재예약. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "취소 지급 익영업일 재예약. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "취소 지급 익영업일 재예약. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 취소 지급 익영업일 재예약. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 취소 지급 익영업일 재예약. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (net0 <= 0 && h_prior == 1) { setl(b, T_RC, 32); Bchg(b, T_MSG, 0, "지급 대상 금액 없음", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + if (stdb_update_payment_status(merch, "CANCELLED", "SCHEDULED", bizdate) < 0) + { setl(b, T_RC, 43); Bchg(b, T_MSG, 0, "지급 상태 전환 실패", 0L); FAIL(b); } + userlog("[정산심화] %s: 취소 지급 재예약 (CANCELLED->SCHEDULED)", "취소 지급 익영업일 재예약. (심화판)"); + Bchg(b, T_STATUS, 0, "R", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "취소 지급 익영업일 재예약. (심화판) [ST_PAYRETRY] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 취소 지급 익영업일 재예약. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_PAYRETRY", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 취소 지급 익영업일 재예약. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_PAYSTATUS.pgc b/app/src/st/svc/ST_PAYSTATUS.pgc index 0413142..332a295 100644 --- a/app/src/st/svc/ST_PAYSTATUS.pgc +++ b/app/src/st/svc/ST_PAYSTATUS.pgc @@ -1,34 +1,140 @@ /* - * ST_PAYSTATUS.pgc - st 모듈 서비스 ST_PAYSTATUS (one service = one file). - * 지급 상태 조회: 가맹점의 가장 최근 지급 레코드에서 상태·금액·지급 예정일을 - * 조회해 회신한다. 지급 이력이 없으면 NONE/RC=1. + * ST_PAYSTATUS.pgc - st (정산/수수료) 모듈 서비스 ST_PAYSTATUS (one service = one file). + * 가맹점 최신 지급 상태 조회. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_PAYSTATUS.h" -/* 53. ST_PAYSTATUS - 가맹점 최신 지급 상태 조회. */ +/* ST_PAYSTATUS - 가맹점 최신 지급 상태 조회. (심화판). (심화판) */ void ST_PAYSTATUS(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64]; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_net; - char h_merch[64], h_status[16], h_payd[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_PAYSTATUS"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_PAYSTATUS"); + userlog("[정산심화] 가맹점 최신 지급 상태 조회. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; - EXEC SQL SELECT status, net_amount, to_char(pay_date,'YYYY-MM-DD') - INTO :h_status, :h_net, :h_payd FROM st_payment - WHERE merchant_id = :h_merch ORDER BY pay_id DESC LIMIT 1; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlcode == 100) { - Bchg(b, T_STATUS, 0, "NONE", 0L); - setl(b, T_RC, 1); - OK(b); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 가맹점 최신 지급 상태 조회. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "가맹점 최신 지급 상태 조회. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "가맹점 최신 지급 상태 조회. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "가맹점 최신 지급 상태 조회. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "가맹점 최신 지급 상태 조회. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "가맹점 최신 지급 상태 조회. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "가맹점 최신 지급 상태 조회. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 가맹점 최신 지급 상태 조회. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 가맹점 최신 지급 상태 조회. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + gets_(b, T_STR2, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "SCHEDULED"); + strncpy(h_status, chan, sizeof(h_status)-1); h_status[sizeof(h_status)-1] = 0; + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_cnt, :h_netsum + FROM st_payment WHERE status = :h_status + AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 56); Bchg(b, T_MSG, 0, "지급 상태 집계 오류", 0L); FAIL(b); } Bchg(b, T_STATUS, 0, h_status, 0L); - setl(b, T_NET, h_net); - Bchg(b, T_KEY1, 0, h_payd, 0L); - setl(b, T_RC, 0); + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_netsum); + userlog("[정산심화] 가맹점 최신 지급 상태 조회. (심화판) cnt=%ld sum=%ld", h_cnt, h_netsum); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "가맹점 최신 지급 상태 조회. (심화판) [ST_PAYSTATUS] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 가맹점 최신 지급 상태 조회. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_PAYSTATUS", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 가맹점 최신 지급 상태 조회. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_PCTFEE.pgc b/app/src/st/svc/ST_PCTFEE.pgc index c75ab17..c805853 100644 --- a/app/src/st/svc/ST_PCTFEE.pgc +++ b/app/src/st/svc/ST_PCTFEE.pgc @@ -1,26 +1,153 @@ /* - * ST_PCTFEE.pgc - st 모듈 서비스 ST_PCTFEE (one service = one file). - * 정률 수수료 계산: 거래 금액에 입력 요율(bps, T_ARG1)을 곱해(acq_fee) 수수료를 - * 산출하고 정산 상세에 PCT 라인으로 기록한다. + * ST_PCTFEE.pgc - st (정산/수수료) 모듈 서비스 ST_PCTFEE (one service = one file). + * 정률 수수료 계산 (금액 x bps). (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_PCTFEE.h" -/* 77. ST_PCTFEE - 정률 수수료 계산 (금액 x bps). */ +/* ST_PCTFEE - 정률 수수료 계산 (금액 x bps). (심화판). (심화판) */ void ST_PCTFEE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; - long sid = getl(b, T_SETTLE_ID), pid = getl(b, T_PURCHASE_ID); - long amount = getl(b, T_AMOUNT), bps = getl(b, T_ARG1), fee, dtl; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_PCTFEE"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (bps <= 0) bps = 25; /* 기본 요율 0.25% */ - fee = acq_fee(amount, bps); - dtl = stdb_insert_fee_dtl(sid, pid, merch, "PCT", fee, bizdate); - if (dtl < 0) FAIL(b); - setl(b, T_FEE, fee); - setl(b, T_RC, bps); - setl(b, T_ID1, dtl); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_PCTFEE"); + userlog("[정산심화] 정률 수수료 계산 (금액 x bps). (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + if (h_pid <= 0) { setl(b, T_RC, 15); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 누락", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + { long rb = stdb_lookup_rate("MDR", amount>0?amount:net0); if (rb > 0) h_bps = rb; } /* 구간요율표 우선 */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 정률 수수료 계산 (금액 x bps). (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "정률 수수료 계산 (금액 x bps). (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "정률 수수료 계산 (금액 x bps). (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "정률 수수료 계산 (금액 x bps). (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "정률 수수료 계산 (금액 x bps). (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "정률 수수료 계산 (금액 x bps). (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "정률 수수료 계산 (금액 x bps). (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 정률 수수료 계산 (금액 x bps). (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 정률 수수료 계산 (금액 x bps). (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (h_bps < 0) h_bps = 0; + if (h_bps > 1000) { setl(b, T_RC, 33); Bchg(b, T_MSG, 0, "요율(bps) 상한 초과", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_fee = acq_fee(amount, h_bps); + if (h_fee < 0) h_fee = 0; + userlog("[정산심화] 정률 수수료 계산 (금액 x bps). (심화판): 계약 요율 정률 수수료 산출 fee=%ld bps=%ld", h_fee, h_bps); + h_id = stdb_insert_fee_dtl(h_sid, h_pid, merch, "MDR", h_fee, bizdate); + if (h_id < 0) { setl(b, T_RC, 41); Bchg(b, T_MSG, 0, "정산 상세(st_settle_dtl) 기록 실패", 0L); FAIL(b); } + setl(b, T_FEE, h_fee); setl(b, T_ID1, h_id); setl(b, T_FEE, h_fee); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "정률 수수료 계산 (금액 x bps). (심화판) [ST_PCTFEE] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 정률 수수료 계산 (금액 x bps). (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_PCTFEE", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 정률 수수료 계산 (금액 x bps). (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_PRECONFIRM.pgc b/app/src/st/svc/ST_PRECONFIRM.pgc index db25964..567437d 100644 --- a/app/src/st/svc/ST_PRECONFIRM.pgc +++ b/app/src/st/svc/ST_PRECONFIRM.pgc @@ -1,30 +1,140 @@ /* - * ST_PRECONFIRM.pgc - st 모듈 서비스 ST_PRECONFIRM (one service = one file). - * 정산 확정 전 검증: 당일 지급 중 보류(HELD) 건이 남아있는지 확인해 확정 가능 - * 여부를 판정한다. 보류가 없으면 확정 가능(ready)으로 반환한다 (조회 전용). + * ST_PRECONFIRM.pgc - st (정산/수수료) 모듈 서비스 ST_PRECONFIRM (one service = one file). + * 보류 잔여 여부로 확정 가능 판정. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_PRECONFIRM.h" -/* 120. ST_PRECONFIRM - 보류 잔여 여부로 확정 가능 판정. */ +/* ST_PRECONFIRM - 보류 잔여 여부로 확정 가능 판정. (심화판). (심화판) */ void ST_PRECONFIRM(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_held, h_sched; - char h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_PRECONFIRM"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_PRECONFIRM"); + userlog("[정산심화] 보류 잔여 여부로 확정 가능 판정. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT count(*) INTO :h_held FROM st_payment - WHERE biz_date = :h_bizdate AND status = 'HELD'; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*) INTO :h_sched FROM st_payment - WHERE biz_date = :h_bizdate AND status = 'SCHEDULED'; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_AMT1, h_held); - setl(b, T_AMT2, h_sched); - setl(b, T_RC, (h_held == 0) ? 0 : 1); /* 0 = 확정 가능 */ + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 보류 잔여 여부로 확정 가능 판정. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "보류 잔여 여부로 확정 가능 판정. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "보류 잔여 여부로 확정 가능 판정. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "보류 잔여 여부로 확정 가능 판정. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "보류 잔여 여부로 확정 가능 판정. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "보류 잔여 여부로 확정 가능 판정. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "보류 잔여 여부로 확정 가능 판정. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 보류 잔여 여부로 확정 가능 판정. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 보류 잔여 여부로 확정 가능 판정. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + EXEC SQL SELECT count(*) INTO :h_cnt FROM st_payment + WHERE merchant_id = :h_merch AND status = 'HELD'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 31); Bchg(b, T_MSG, 0, "보류 잔여 조회 오류", 0L); FAIL(b); } + if (h_cnt > 0) { setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "보류 잔여로 확정 불가", 0L); Bchg(b, T_STATUS, 0, "HOLD", 0L); OK(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "보류 잔여 여부로 확정 가능 판정. (심화판) [ST_PRECONFIRM] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 보류 잔여 여부로 확정 가능 판정. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_PRECONFIRM", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 보류 잔여 여부로 확정 가능 판정. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_PROMOAPPLY.pgc b/app/src/st/svc/ST_PROMOAPPLY.pgc index 904d7a8..01d84d2 100644 --- a/app/src/st/svc/ST_PROMOAPPLY.pgc +++ b/app/src/st/svc/ST_PROMOAPPLY.pgc @@ -1,26 +1,135 @@ /* - * ST_PROMOAPPLY.pgc - st 모듈 서비스 ST_PROMOAPPLY (one service = one file). - * 프로모션 할인 적용: 산정된 수수료(T_FEE)에 프로모션 할인율(T_ARG1 bps)을 적용해 - * 할인액을 계산하고 음(-)의 정산 조정으로 기록한다 (수수료 감액). + * ST_PROMOAPPLY.pgc - st (정산/수수료) 모듈 서비스 ST_PROMOAPPLY (one service = one file). + * 수수료 프로모션 할인(음수 조정). (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_PROMOAPPLY.h" -/* 86. ST_PROMOAPPLY - 수수료 프로모션 할인(음수 조정). */ +/* ST_PROMOAPPLY - 수수료 프로모션 할인(음수 조정). (심화판). (심화판) */ void ST_PROMOAPPLY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long sid = getl(b, T_SETTLE_ID), pid = getl(b, T_PURCHASE_ID); - long fee = getl(b, T_FEE), bps = getl(b, T_ARG1), disc, adj; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_PROMOAPPLY"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (bps <= 0) bps = 1000; /* 기본 10% 할인 */ - disc = acq_fee(fee, bps); - if (disc <= 0) { setl(b, T_RC, 1); OK(b); } - adj = stdb_insert_adjust(sid, pid, -disc, "PROMO", bizdate); - if (adj < 0) FAIL(b); - setl(b, T_ADJ, adj); - setl(b, T_DELTA, -disc); - setl(b, T_NET, fee - disc); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_PROMOAPPLY"); + userlog("[정산심화] 수수료 프로모션 할인(음수 조정). (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 수수료 프로모션 할인(음수 조정). (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "수수료 프로모션 할인(음수 조정). (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "수수료 프로모션 할인(음수 조정). (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "수수료 프로모션 할인(음수 조정). (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "수수료 프로모션 할인(음수 조정). (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "수수료 프로모션 할인(음수 조정). (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "수수료 프로모션 할인(음수 조정). (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 수수료 프로모션 할인(음수 조정). (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 수수료 프로모션 할인(음수 조정). (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_delta = -(delta0>0?delta0:net0/10); + userlog("[정산심화] 수수료 프로모션 할인(음수 조정). (심화판): 채권 상계/차감(음수 조정) delta=%ld", h_delta, h_bps); + h_id = stdb_insert_adjust(h_sid, h_pid, h_delta, "SETTLE_OFFSET", bizdate); + if (h_id < 0) { setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "정산 조정(st_adjust) 기록 실패", 0L); FAIL(b); } + setl(b, T_ADJ, h_id); setl(b, T_DELTA, h_delta); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "수수료 프로모션 할인(음수 조정). (심화판) [ST_PROMOAPPLY] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 수수료 프로모션 할인(음수 조정). (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_PROMOAPPLY", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 수수료 프로모션 할인(음수 조정). (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_PROMOCALC.pgc b/app/src/st/svc/ST_PROMOCALC.pgc index 12083d9..2b50edb 100644 --- a/app/src/st/svc/ST_PROMOCALC.pgc +++ b/app/src/st/svc/ST_PROMOCALC.pgc @@ -1,28 +1,152 @@ /* - * ST_PROMOCALC.pgc - st 모듈 서비스 ST_PROMOCALC (one service = one file). - * 프로모션/이벤트 정산: 이벤트 코드와 할인율(%)을 받아 수수료 할인액을 계산하고 - * 양수 조정(가맹점 환급)으로 st_adjust 에 기록한다 (dbio). + * ST_PROMOCALC.pgc - st (정산/수수료) 모듈 서비스 ST_PROMOCALC (one service = one file). + * 프로모션/이벤트 수수료 할인 정산. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_PROMOCALC.h" -/* 32. ST_PROMOCALC - 프로모션/이벤트 수수료 할인 정산. */ +/* ST_PROMOCALC - 프로모션/이벤트 수수료 할인 정산. (심화판). (심화판) */ void ST_PROMOCALC(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], event[32], reason[64]; - long sid = getl(b, T_SETTLE_ID), pid = getl(b, T_PURCHASE_ID); - long fee = getl(b, T_FEE), pct = getl(b, T_ARG1), disc, adj; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_PROMOCALC"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - gets_(b, T_STR1, event, sizeof(event)); if (event[0] == 0) strcpy(event, "EVENT"); - if (pct <= 0 || pct > 100) pct = 30; /* 기본 30% 할인 */ - disc = fee * pct / 100; - if (disc <= 0) { setl(b, T_RC, 1); setl(b, T_DELTA, 0); OK(b); } - snprintf(reason, sizeof(reason), "PROMO:%s", event); - adj = stdb_insert_adjust(sid, pid, disc, reason, bizdate); - if (adj < 0) FAIL(b); - setl(b, T_ADJ, adj); - setl(b, T_DELTA, disc); - setl(b, T_FEE, fee - disc); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "RATE"); + if (reason[0] == 0) strcpy(reason, "ST_PROMOCALC"); + userlog("[정산심화] 프로모션/이벤트 수수료 할인 정산. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + if (h_pid <= 0) { setl(b, T_RC, 15); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 누락", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 프로모션/이벤트 수수료 할인 정산. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "프로모션/이벤트 수수료 할인 정산. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "프로모션/이벤트 수수료 할인 정산. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "프로모션/이벤트 수수료 할인 정산. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "프로모션/이벤트 수수료 할인 정산. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "프로모션/이벤트 수수료 할인 정산. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "프로모션/이벤트 수수료 할인 정산. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 프로모션/이벤트 수수료 할인 정산. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 프로모션/이벤트 수수료 할인 정산. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (h_bps < 0) h_bps = 0; + if (h_bps > 1000) { setl(b, T_RC, 33); Bchg(b, T_MSG, 0, "요율(bps) 상한 초과", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_fee = acq_fee(amount, h_bps) * 80 / 100; + if (h_fee < 0) h_fee = 0; + userlog("[정산심화] 프로모션/이벤트 수수료 할인 정산. (심화판): 프로모션 20% 할인 요율 fee=%ld bps=%ld", h_fee, h_bps); + h_id = stdb_insert_fee_dtl(h_sid, h_pid, merch, "RATE", h_fee, bizdate); + if (h_id < 0) { setl(b, T_RC, 41); Bchg(b, T_MSG, 0, "정산 상세(st_settle_dtl) 기록 실패", 0L); FAIL(b); } + setl(b, T_FEE, h_fee); setl(b, T_ID1, h_id); setl(b, T_FEE, h_fee); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "프로모션/이벤트 수수료 할인 정산. (심화판) [ST_PROMOCALC] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 프로모션/이벤트 수수료 할인 정산. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_PROMOCALC", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 프로모션/이벤트 수수료 할인 정산. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_RATEAPPLY.pgc b/app/src/st/svc/ST_RATEAPPLY.pgc index c110a7a..8c0c802 100644 --- a/app/src/st/svc/ST_RATEAPPLY.pgc +++ b/app/src/st/svc/ST_RATEAPPLY.pgc @@ -1,27 +1,153 @@ /* - * ST_RATEAPPLY.pgc - st 모듈 서비스 ST_RATEAPPLY (one service = one file). - * Moved verbatim from the former inline st_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by st_svr at tpsvrinit. + * ST_RATEAPPLY.pgc - st (정산/수수료) 모듈 서비스 ST_RATEAPPLY (one service = one file). + * 구간요율 적용 수수료 계산 + 정산 상세 기록. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_RATEAPPLY.h" -/* 20. ST_RATEAPPLY - 구간요율 적용 수수료 계산 + 정산 상세 기록 (dbio). */ +/* ST_RATEAPPLY - 구간요율 적용 수수료 계산 + 정산 상세 기록. (심화판). (심화판) */ void ST_RATEAPPLY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16], ftype[16]; - long sid = getl(b, T_SETTLE_ID), pid = getl(b, T_PURCHASE_ID); - long amount = getl(b, T_AMOUNT), bps, fee, dtl; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_RATEAPPLY"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - gets_(b, T_STR1, ftype, sizeof(ftype)); if (ftype[0] == 0) strcpy(ftype, "MDR"); - bps = stdb_lookup_rate(ftype, amount); - if (bps < 0) FAIL(b); - fee = acq_fee(amount, bps); - dtl = stdb_insert_fee_dtl(sid, pid, merch, ftype, fee, bizdate); - if (dtl < 0) FAIL(b); - setl(b, T_FEE, fee); - setl(b, T_RC, bps); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_RATEAPPLY"); + userlog("[정산심화] 구간요율 적용 수수료 계산 + 정산 상세 기록. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + if (h_pid <= 0) { setl(b, T_RC, 15); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 누락", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + { long rb = stdb_lookup_rate("MDR", amount>0?amount:net0); if (rb > 0) h_bps = rb; } /* 구간요율표 우선 */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 구간요율 적용 수수료 계산 + 정산 상세 기록. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "구간요율 적용 수수료 계산 + 정산 상세 기록. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "구간요율 적용 수수료 계산 + 정산 상세 기록. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "구간요율 적용 수수료 계산 + 정산 상세 기록. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "구간요율 적용 수수료 계산 + 정산 상세 기록. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "구간요율 적용 수수료 계산 + 정산 상세 기록. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "구간요율 적용 수수료 계산 + 정산 상세 기록. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 구간요율 적용 수수료 계산 + 정산 상세 기록. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 구간요율 적용 수수료 계산 + 정산 상세 기록. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (h_bps < 0) h_bps = 0; + if (h_bps > 1000) { setl(b, T_RC, 33); Bchg(b, T_MSG, 0, "요율(bps) 상한 초과", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_fee = acq_fee(amount, h_bps); + if (h_fee < 0) h_fee = 0; + userlog("[정산심화] 구간요율 적용 수수료 계산 + 정산 상세 기록. (심화판): 계약 요율 정률 수수료 산출 fee=%ld bps=%ld", h_fee, h_bps); + h_id = stdb_insert_fee_dtl(h_sid, h_pid, merch, "MDR", h_fee, bizdate); + if (h_id < 0) { setl(b, T_RC, 41); Bchg(b, T_MSG, 0, "정산 상세(st_settle_dtl) 기록 실패", 0L); FAIL(b); } + setl(b, T_FEE, h_fee); setl(b, T_ID1, h_id); setl(b, T_FEE, h_fee); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "구간요율 적용 수수료 계산 + 정산 상세 기록. (심화판) [ST_RATEAPPLY] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 구간요율 적용 수수료 계산 + 정산 상세 기록. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_RATEAPPLY", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 구간요율 적용 수수료 계산 + 정산 상세 기록. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_RATECARD.pgc b/app/src/st/svc/ST_RATECARD.pgc index 5f92920..426ba27 100644 --- a/app/src/st/svc/ST_RATECARD.pgc +++ b/app/src/st/svc/ST_RATECARD.pgc @@ -1,28 +1,139 @@ /* - * ST_RATECARD.pgc - st 모듈 서비스 ST_RATECARD (one service = one file). - * 요율 카드 조회: 특정 수수료 유형의 등록 구간 개수와 최소/최대 요율을 집계해 - * 요율표 요약(요율 카드)을 반환한다 (조회 전용). + * ST_RATECARD.pgc - st (정산/수수료) 모듈 서비스 ST_RATECARD (one service = one file). + * 요율표 요약(구간수/최소/최대 bps). (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_RATECARD.h" -/* 80. ST_RATECARD - 요율표 요약(구간수/최소/최대 bps). */ +/* ST_RATECARD - 요율표 요약(구간수/최소/최대 bps). (심화판). (심화판) */ void ST_RATECARD(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char ftype[16]; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_min, h_max; - char h_ftype[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; - gets_(b, T_STR1, ftype, sizeof(ftype)); if (ftype[0] == 0) strcpy(ftype, "MDR"); + + strcpy(h_svc, "ST_RATECARD"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_RATECARD"); + userlog("[정산심화] 요율표 요약(구간수/최소/최대 bps). (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 요율표 요약(구간수/최소/최대 bps). (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "요율표 요약(구간수/최소/최대 bps). (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "요율표 요약(구간수/최소/최대 bps). (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "요율표 요약(구간수/최소/최대 bps). (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "요율표 요약(구간수/최소/최대 bps). (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "요율표 요약(구간수/최소/최대 bps). (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "요율표 요약(구간수/최소/최대 bps). (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 요율표 요약(구간수/최소/최대 bps). (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 요율표 요약(구간수/최소/최대 bps). (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_base = amount > 0 ? amount : net0; + { long rb = stdb_lookup_rate(ftype, h_base); if (rb < 0) { setl(b, T_RC, 58); Bchg(b, T_MSG, 0, "구간 요율 조회 오류", 0L); FAIL(b); } h_bps = rb; } + h_fee = acq_fee(h_base, h_bps); EXEC SQL SELECT count(*), coalesce(min(rate_bps),0), coalesce(max(rate_bps),0) - INTO :h_cnt, :h_min, :h_max FROM st_fee_rate WHERE fee_type = :h_ftype; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_cnt == 0) { setl(b, T_RC, 1); OK(b); } - setl(b, T_COUNT, h_cnt); - setl(b, T_AMT1, h_min); - setl(b, T_AMT2, h_max); + INTO :h_cnt, :h_gmin, :h_gmax FROM st_fee_rate WHERE fee_type = :h_ftype; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 59); Bchg(b, T_MSG, 0, "요율표 요약 오류", 0L); FAIL(b); } + h_netsum = h_fee; setl(b, T_FEE, h_fee); setl(b, T_AMT1, h_gmin); setl(b, T_AMT2, h_gmax); + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_netsum); + userlog("[정산심화] 요율표 요약(구간수/최소/최대 bps). (심화판) cnt=%ld sum=%ld", h_cnt, h_netsum); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "요율표 요약(구간수/최소/최대 bps). (심화판) [ST_RATECARD] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 요율표 요약(구간수/최소/최대 bps). (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_RATECARD", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 요율표 요약(구간수/최소/최대 bps). (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_RATECMP.pgc b/app/src/st/svc/ST_RATECMP.pgc index 0a17287..9a50a89 100644 --- a/app/src/st/svc/ST_RATECMP.pgc +++ b/app/src/st/svc/ST_RATECMP.pgc @@ -1,25 +1,139 @@ /* - * ST_RATECMP.pgc - st 모듈 서비스 ST_RATECMP (one service = one file). - * 요율 유형 비교: 두 수수료 유형(T_STR1/T_STR2)의 입력 금액 적용 요율을 - * dbio(stdb_lookup_rate)로 각각 조회해 요율 차이를 계산한다 (조회 전용). + * ST_RATECMP.pgc - st (정산/수수료) 모듈 서비스 ST_RATECMP (one service = one file). + * 두 요율 유형 적용 bps 비교. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_RATECMP.h" -/* 81. ST_RATECMP - 두 요율 유형 적용 bps 비교. */ +/* ST_RATECMP - 두 요율 유형 적용 bps 비교. (심화판). (심화판) */ void ST_RATECMP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char ta[16], tb[16]; - long amount = getl(b, T_AMOUNT), ra, rb; - gets_(b, T_STR1, ta, sizeof(ta)); if (ta[0] == 0) strcpy(ta, "MDR"); - gets_(b, T_STR2, tb, sizeof(tb)); if (tb[0] == 0) strcpy(tb, "VAN"); - ra = stdb_lookup_rate(ta, amount); - rb = stdb_lookup_rate(tb, amount); - if (ra < 0 || rb < 0) FAIL(b); - setl(b, T_ARG1, ra); - setl(b, T_ARG2, rb); - setl(b, T_DELTA, ra - rb); - setl(b, T_FEE, acq_fee(amount, ra) - acq_fee(amount, rb)); + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_RATECMP"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_RATECMP"); + userlog("[정산심화] 두 요율 유형 적용 bps 비교. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 두 요율 유형 적용 bps 비교. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "두 요율 유형 적용 bps 비교. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "두 요율 유형 적용 bps 비교. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "두 요율 유형 적용 bps 비교. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "두 요율 유형 적용 bps 비교. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "두 요율 유형 적용 bps 비교. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "두 요율 유형 적용 bps 비교. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 두 요율 유형 적용 bps 비교. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 두 요율 유형 적용 bps 비교. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_base = amount > 0 ? amount : net0; + { long rb = stdb_lookup_rate(ftype, h_base); if (rb < 0) { setl(b, T_RC, 58); Bchg(b, T_MSG, 0, "구간 요율 조회 오류", 0L); FAIL(b); } h_bps = rb; } + h_fee = acq_fee(h_base, h_bps); + EXEC SQL SELECT count(*), coalesce(min(rate_bps),0), coalesce(max(rate_bps),0) + INTO :h_cnt, :h_gmin, :h_gmax FROM st_fee_rate WHERE fee_type = :h_ftype; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 59); Bchg(b, T_MSG, 0, "요율표 요약 오류", 0L); FAIL(b); } + h_netsum = h_fee; setl(b, T_FEE, h_fee); setl(b, T_AMT1, h_gmin); setl(b, T_AMT2, h_gmax); + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_netsum); + userlog("[정산심화] 두 요율 유형 적용 bps 비교. (심화판) cnt=%ld sum=%ld", h_cnt, h_netsum); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "두 요율 유형 적용 bps 비교. (심화판) [ST_RATECMP] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 두 요율 유형 적용 bps 비교. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_RATECMP", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 두 요율 유형 적용 bps 비교. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_RATEFLOOR.pgc b/app/src/st/svc/ST_RATEFLOOR.pgc index 6f6d4b4..b0d3f6c 100644 --- a/app/src/st/svc/ST_RATEFLOOR.pgc +++ b/app/src/st/svc/ST_RATEFLOOR.pgc @@ -1,28 +1,152 @@ /* - * ST_RATEFLOOR.pgc - st 모듈 서비스 ST_RATEFLOOR (one service = one file). - * 요율 하한 적용: 정률 수수료(금액 x bps)가 하한액(T_ARG2)에 미달하면 하한으로 - * 끌어올려 정산 상세에 FLRFEE 라인으로 기록한다. + * ST_RATEFLOOR.pgc - st (정산/수수료) 모듈 서비스 ST_RATEFLOOR (one service = one file). + * 정률 수수료 하한 보정 후 기록. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_RATEFLOOR.h" -/* 85. ST_RATEFLOOR - 정률 수수료 하한 보정 후 기록. */ +/* ST_RATEFLOOR - 정률 수수료 하한 보정 후 기록. (심화판). (심화판) */ void ST_RATEFLOOR(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; - long sid = getl(b, T_SETTLE_ID), pid = getl(b, T_PURCHASE_ID); - long amount = getl(b, T_AMOUNT), bps = getl(b, T_ARG1), floor = getl(b, T_ARG2); - long fee, lifted = 0, dtl; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_RATEFLOOR"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (bps <= 0) bps = 25; - if (floor <= 0) floor = 100; - fee = acq_fee(amount, bps); - if (fee < floor) { fee = floor; lifted = 1; } - dtl = stdb_insert_fee_dtl(sid, pid, merch, "FLRFEE", fee, bizdate); - if (dtl < 0) FAIL(b); - setl(b, T_FEE, fee); - setl(b, T_RC, lifted); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "RATE"); + if (reason[0] == 0) strcpy(reason, "ST_RATEFLOOR"); + userlog("[정산심화] 정률 수수료 하한 보정 후 기록. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + if (h_pid <= 0) { setl(b, T_RC, 15); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 누락", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 정률 수수료 하한 보정 후 기록. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "정률 수수료 하한 보정 후 기록. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "정률 수수료 하한 보정 후 기록. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "정률 수수료 하한 보정 후 기록. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "정률 수수료 하한 보정 후 기록. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "정률 수수료 하한 보정 후 기록. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "정률 수수료 하한 보정 후 기록. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 정률 수수료 하한 보정 후 기록. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 정률 수수료 하한 보정 후 기록. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (h_bps < 0) h_bps = 0; + if (h_bps > 1000) { setl(b, T_RC, 33); Bchg(b, T_MSG, 0, "요율(bps) 상한 초과", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_fee = acq_fee(amount, h_bps); if (h_fee < 100) h_fee = 100; + if (h_fee < 0) h_fee = 0; + userlog("[정산심화] 정률 수수료 하한 보정 후 기록. (심화판): 정률 수수료 하한 100원 보정 fee=%ld bps=%ld", h_fee, h_bps); + h_id = stdb_insert_fee_dtl(h_sid, h_pid, merch, "RATE", h_fee, bizdate); + if (h_id < 0) { setl(b, T_RC, 41); Bchg(b, T_MSG, 0, "정산 상세(st_settle_dtl) 기록 실패", 0L); FAIL(b); } + setl(b, T_FEE, h_fee); setl(b, T_ID1, h_id); setl(b, T_FEE, h_fee); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "정률 수수료 하한 보정 후 기록. (심화판) [ST_RATEFLOOR] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 정률 수수료 하한 보정 후 기록. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_RATEFLOOR", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 정률 수수료 하한 보정 후 기록. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_RATEMAX.pgc b/app/src/st/svc/ST_RATEMAX.pgc index e16279e..a5c0bf2 100644 --- a/app/src/st/svc/ST_RATEMAX.pgc +++ b/app/src/st/svc/ST_RATEMAX.pgc @@ -1,28 +1,152 @@ /* - * ST_RATEMAX.pgc - st 모듈 서비스 ST_RATEMAX (one service = one file). - * 요율 상한 적용: 정률 수수료(금액 x bps)를 계산하되 상한액(T_ARG2)을 초과하면 - * 상한으로 절삭해 정산 상세에 CAPFEE 라인으로 기록한다. + * ST_RATEMAX.pgc - st (정산/수수료) 모듈 서비스 ST_RATEMAX (one service = one file). + * 정률 수수료 상한 절삭 후 기록. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_RATEMAX.h" -/* 84. ST_RATEMAX - 정률 수수료 상한 절삭 후 기록. */ +/* ST_RATEMAX - 정률 수수료 상한 절삭 후 기록. (심화판). (심화판) */ void ST_RATEMAX(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; - long sid = getl(b, T_SETTLE_ID), pid = getl(b, T_PURCHASE_ID); - long amount = getl(b, T_AMOUNT), bps = getl(b, T_ARG1), cap = getl(b, T_ARG2); - long fee, capped = 0, dtl; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_RATEMAX"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (bps <= 0) bps = 25; - if (cap <= 0) cap = 100000; - fee = acq_fee(amount, bps); - if (fee > cap) { fee = cap; capped = 1; } - dtl = stdb_insert_fee_dtl(sid, pid, merch, "CAPFEE", fee, bizdate); - if (dtl < 0) FAIL(b); - setl(b, T_FEE, fee); - setl(b, T_RC, capped); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "RATE"); + if (reason[0] == 0) strcpy(reason, "ST_RATEMAX"); + userlog("[정산심화] 정률 수수료 상한 절삭 후 기록. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + if (h_pid <= 0) { setl(b, T_RC, 15); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 누락", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 정률 수수료 상한 절삭 후 기록. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "정률 수수료 상한 절삭 후 기록. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "정률 수수료 상한 절삭 후 기록. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "정률 수수료 상한 절삭 후 기록. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "정률 수수료 상한 절삭 후 기록. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "정률 수수료 상한 절삭 후 기록. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "정률 수수료 상한 절삭 후 기록. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 정률 수수료 상한 절삭 후 기록. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 정률 수수료 상한 절삭 후 기록. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (h_bps < 0) h_bps = 0; + if (h_bps > 1000) { setl(b, T_RC, 33); Bchg(b, T_MSG, 0, "요율(bps) 상한 초과", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_fee = acq_fee(amount, h_bps); if (h_fee > 50000) h_fee = 50000; + if (h_fee < 0) h_fee = 0; + userlog("[정산심화] 정률 수수료 상한 절삭 후 기록. (심화판): 정률 수수료 상한 5만원 절삭 fee=%ld bps=%ld", h_fee, h_bps); + h_id = stdb_insert_fee_dtl(h_sid, h_pid, merch, "RATE", h_fee, bizdate); + if (h_id < 0) { setl(b, T_RC, 41); Bchg(b, T_MSG, 0, "정산 상세(st_settle_dtl) 기록 실패", 0L); FAIL(b); } + setl(b, T_FEE, h_fee); setl(b, T_ID1, h_id); setl(b, T_FEE, h_fee); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "정률 수수료 상한 절삭 후 기록. (심화판) [ST_RATEMAX] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 정률 수수료 상한 절삭 후 기록. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_RATEMAX", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 정률 수수료 상한 절삭 후 기록. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_RATESIM.pgc b/app/src/st/svc/ST_RATESIM.pgc index fed9734..1f32a3f 100644 --- a/app/src/st/svc/ST_RATESIM.pgc +++ b/app/src/st/svc/ST_RATESIM.pgc @@ -1,39 +1,139 @@ /* - * ST_RATESIM.pgc - st 모듈 서비스 ST_RATESIM (one service = one file). - * 요율 시뮬레이션: 요율표(st_fee_rate)의 전 구간을 커서로 순회하며 입력 금액에 - * 적용될 구간(bps)을 판정하고 예상 수수료를 계산한다. 조회 전용(쓰기 없음). + * ST_RATESIM.pgc - st (정산/수수료) 모듈 서비스 ST_RATESIM (one service = one file). + * 요율 시뮬레이션 (구간 판정 + 예상 수수료). (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_RATESIM.h" -/* 31. ST_RATESIM - 요율 시뮬레이션 (구간 판정 + 예상 수수료). */ +/* ST_RATESIM - 요율 시뮬레이션 (구간 판정 + 예상 수수료). (심화판). (심화판) */ void ST_RATESIM(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char ftype[16]; - long amount = getl(b, T_AMOUNT), bands = 0, applied = 0, fee; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_min, h_bps; - char h_ftype[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; - gets_(b, T_STR1, ftype, sizeof(ftype)); if (ftype[0] == 0) strcpy(ftype, "MDR"); + + strcpy(h_svc, "ST_RATESIM"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_RATESIM"); + userlog("[정산심화] 요율 시뮬레이션 (구간 판정 + 예상 수수료). (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; - EXEC SQL DECLARE ratesim_c CURSOR FOR - SELECT band_min, rate_bps FROM st_fee_rate WHERE fee_type = :h_ftype ORDER BY band_min; - EXEC SQL OPEN ratesim_c; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH ratesim_c INTO :h_min, :h_bps; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE ratesim_c; FAIL(b); } - bands++; - if (h_min <= amount) applied = h_bps; /* band_min 오름차순: 마지막 매칭 구간이 적용 요율 */ + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } } - EXEC SQL CLOSE ratesim_c; - if (bands == 0) { setl(b, T_RC, -1); OK(b); } - fee = acq_fee(amount, applied); - setl(b, T_COUNT, bands); - setl(b, T_RC, applied); - setl(b, T_FEE, fee); + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 요율 시뮬레이션 (구간 판정 + 예상 수수료). (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "요율 시뮬레이션 (구간 판정 + 예상 수수료). (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "요율 시뮬레이션 (구간 판정 + 예상 수수료). (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "요율 시뮬레이션 (구간 판정 + 예상 수수료). (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "요율 시뮬레이션 (구간 판정 + 예상 수수료). (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "요율 시뮬레이션 (구간 판정 + 예상 수수료). (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "요율 시뮬레이션 (구간 판정 + 예상 수수료). (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 요율 시뮬레이션 (구간 판정 + 예상 수수료). (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 요율 시뮬레이션 (구간 판정 + 예상 수수료). (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_base = amount > 0 ? amount : net0; + { long rb = stdb_lookup_rate(ftype, h_base); if (rb < 0) { setl(b, T_RC, 58); Bchg(b, T_MSG, 0, "구간 요율 조회 오류", 0L); FAIL(b); } h_bps = rb; } + h_fee = acq_fee(h_base, h_bps); + EXEC SQL SELECT count(*), coalesce(min(rate_bps),0), coalesce(max(rate_bps),0) + INTO :h_cnt, :h_gmin, :h_gmax FROM st_fee_rate WHERE fee_type = :h_ftype; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 59); Bchg(b, T_MSG, 0, "요율표 요약 오류", 0L); FAIL(b); } + h_netsum = h_fee; setl(b, T_FEE, h_fee); setl(b, T_AMT1, h_gmin); setl(b, T_AMT2, h_gmax); + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_netsum); + userlog("[정산심화] 요율 시뮬레이션 (구간 판정 + 예상 수수료). (심화판) cnt=%ld sum=%ld", h_cnt, h_netsum); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "요율 시뮬레이션 (구간 판정 + 예상 수수료). (심화판) [ST_RATESIM] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 요율 시뮬레이션 (구간 판정 + 예상 수수료). (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_RATESIM", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 요율 시뮬레이션 (구간 판정 + 예상 수수료). (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_REBATE.pgc b/app/src/st/svc/ST_REBATE.pgc index e95053e..a594d69 100644 --- a/app/src/st/svc/ST_REBATE.pgc +++ b/app/src/st/svc/ST_REBATE.pgc @@ -1,25 +1,135 @@ /* - * ST_REBATE.pgc - st 모듈 서비스 ST_REBATE (one service = one file). - * 리베이트 정산: 부과 수수료(T_FEE)의 일정 비율(T_ARG1 bps)을 가맹점에 돌려주는 - * 리베이트로 산정해 양(+)의 정산 조정으로 기록한다. + * ST_REBATE.pgc - st (정산/수수료) 모듈 서비스 ST_REBATE (one service = one file). + * 수수료 기반 리베이트 환급 조정. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_REBATE.h" -/* 104. ST_REBATE - 수수료 기반 리베이트 환급 조정. */ +/* ST_REBATE - 수수료 기반 리베이트 환급 조정. (심화판). (심화판) */ void ST_REBATE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long sid = getl(b, T_SETTLE_ID), pid = getl(b, T_PURCHASE_ID); - long fee = getl(b, T_FEE), bps = getl(b, T_ARG1), rebate, adj; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_REBATE"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (bps <= 0) bps = 500; /* 기본 리베이트 5% */ - rebate = acq_fee(fee, bps); - if (rebate <= 0) { setl(b, T_RC, 1); OK(b); } - adj = stdb_insert_adjust(sid, pid, rebate, "REBATE", bizdate); - if (adj < 0) FAIL(b); - setl(b, T_ADJ, adj); - setl(b, T_DELTA, rebate); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_REBATE"); + userlog("[정산심화] 수수료 기반 리베이트 환급 조정. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 수수료 기반 리베이트 환급 조정. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "수수료 기반 리베이트 환급 조정. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "수수료 기반 리베이트 환급 조정. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "수수료 기반 리베이트 환급 조정. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "수수료 기반 리베이트 환급 조정. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "수수료 기반 리베이트 환급 조정. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "수수료 기반 리베이트 환급 조정. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 수수료 기반 리베이트 환급 조정. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 수수료 기반 리베이트 환급 조정. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_delta = (delta0>0?delta0:net0/100); + userlog("[정산심화] 수수료 기반 리베이트 환급 조정. (심화판): 실적 기반 가산 조정(양수) delta=%ld", h_delta, h_bps); + h_id = stdb_insert_adjust(h_sid, h_pid, h_delta, "SETTLE_BONUS", bizdate); + if (h_id < 0) { setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "정산 조정(st_adjust) 기록 실패", 0L); FAIL(b); } + setl(b, T_ADJ, h_id); setl(b, T_DELTA, h_delta); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "수수료 기반 리베이트 환급 조정. (심화판) [ST_REBATE] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 수수료 기반 리베이트 환급 조정. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_REBATE", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 수수료 기반 리베이트 환급 조정. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_RECALC.pgc b/app/src/st/svc/ST_RECALC.pgc index f74af0e..78cc71b 100644 --- a/app/src/st/svc/ST_RECALC.pgc +++ b/app/src/st/svc/ST_RECALC.pgc @@ -1,24 +1,148 @@ /* - * ST_RECALC.pgc - st 모듈 서비스 ST_RECALC (one service = one file). - * Moved verbatim from the former inline st_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by st_svr at tpsvrinit. + * ST_RECALC.pgc - st (정산/수수료) 모듈 서비스 ST_RECALC (one service = one file). + * 매입 수수료 기준 정산 net 재계산 + 갱신. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_RECALC.h" -/* 19. ST_RECALC - 매입 수수료 기준 정산 net 재계산 + 갱신. */ +/* ST_RECALC - 매입 수수료 기준 정산 net 재계산 + 갱신. (심화판). (심화판) */ void ST_RECALC(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_amount, h_fee, h_net; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); - EXEC SQL SELECT amount, fee INTO :h_amount, :h_fee FROM purchase WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); + + strcpy(h_svc, "ST_RECALC"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_RECALC"); + userlog("[정산심화] 매입 수수료 기준 정산 net 재계산 + 갱신. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (h_pid <= 0) { setl(b, T_RC, 15); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 누락", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 매입 수수료 기준 정산 net 재계산 + 갱신. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "매입 수수료 기준 정산 net 재계산 + 갱신. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "매입 수수료 기준 정산 net 재계산 + 갱신. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "매입 수수료 기준 정산 net 재계산 + 갱신. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "매입 수수료 기준 정산 net 재계산 + 갱신. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "매입 수수료 기준 정산 net 재계산 + 갱신. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "매입 수수료 기준 정산 net 재계산 + 갱신. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 매입 수수료 기준 정산 net 재계산 + 갱신. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 매입 수수료 기준 정산 net 재계산 + 갱신. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + if (h_pid > 0) { + EXEC SQL SELECT amount, fee, coalesce(net,0) INTO :h_amount, :h_fee, :h_net + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 49); Bchg(b, T_MSG, 0, "매입 원본 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { setl(b, T_RC, 25); Bchg(b, T_MSG, 0, "매입 원본 미존재", 0L); FAIL(b); } + } h_net = h_amount - h_fee; EXEC SQL UPDATE settlement SET net = :h_net WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 51); Bchg(b, T_MSG, 0, "정산 net 갱신 실패", 0L); FAIL(b); } setl(b, T_NET, h_net); + userlog("[정산심화] 매입 수수료 기준 정산 net 재계산 + 갱신. (심화판) pid=%ld net=%ld", h_pid, h_net); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "매입 수수료 기준 정산 net 재계산 + 갱신. (심화판) [ST_RECALC] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 매입 수수료 기준 정산 net 재계산 + 갱신. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_RECALC", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 매입 수수료 기준 정산 net 재계산 + 갱신. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_RECVAGE.pgc b/app/src/st/svc/ST_RECVAGE.pgc index 23f9b1a..b79faba 100644 --- a/app/src/st/svc/ST_RECVAGE.pgc +++ b/app/src/st/svc/ST_RECVAGE.pgc @@ -1,27 +1,140 @@ /* - * ST_RECVAGE.pgc - st 모듈 서비스 ST_RECVAGE (one service = one file). - * 미수금 연령 분석: 기준일(T_BIZDATE) 이전 지급일의 보류(HELD) 지급 건수와 금액을 - * 집계해 연체 미수 규모를 산출한다 (조회 전용). + * ST_RECVAGE.pgc - st (정산/수수료) 모듈 서비스 ST_RECVAGE (one service = one file). + * 기준일 이전 보류 미수금 집계. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_RECVAGE.h" -/* 110. ST_RECVAGE - 기준일 이전 보류 미수금 집계. */ +/* ST_RECVAGE - 기준일 이전 보류 미수금 집계. (심화판). (심화판) */ void ST_RECVAGE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_sum; - char h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_RECVAGE"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_RECVAGE"); + userlog("[정산심화] 기준일 이전 보류 미수금 집계. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_cnt, :h_sum - FROM st_payment WHERE status = 'HELD' AND pay_date <= :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_sum); - setl(b, T_RC, (h_cnt > 0) ? 0 : 1); + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 기준일 이전 보류 미수금 집계. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "기준일 이전 보류 미수금 집계. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "기준일 이전 보류 미수금 집계. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "기준일 이전 보류 미수금 집계. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "기준일 이전 보류 미수금 집계. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "기준일 이전 보류 미수금 집계. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "기준일 이전 보류 미수금 집계. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 기준일 이전 보류 미수금 집계. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 기준일 이전 보류 미수금 집계. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + gets_(b, T_STR2, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "SCHEDULED"); + strncpy(h_status, chan, sizeof(h_status)-1); h_status[sizeof(h_status)-1] = 0; + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_cnt, :h_netsum + FROM st_payment WHERE status = :h_status + AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 56); Bchg(b, T_MSG, 0, "지급 상태 집계 오류", 0L); FAIL(b); } + Bchg(b, T_STATUS, 0, h_status, 0L); + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_netsum); + userlog("[정산심화] 기준일 이전 보류 미수금 집계. (심화판) cnt=%ld sum=%ld", h_cnt, h_netsum); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "기준일 이전 보류 미수금 집계. (심화판) [ST_RECVAGE] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 기준일 이전 보류 미수금 집계. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_RECVAGE", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 기준일 이전 보류 미수금 집계. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_RECVOFFSET.pgc b/app/src/st/svc/ST_RECVOFFSET.pgc index 7714f62..4b10954 100644 --- a/app/src/st/svc/ST_RECVOFFSET.pgc +++ b/app/src/st/svc/ST_RECVOFFSET.pgc @@ -1,40 +1,135 @@ /* - * ST_RECVOFFSET.pgc - st 모듈 서비스 ST_RECVOFFSET (one service = one file). - * 미수금 상계: 매입 조인으로 가맹점의 누적 미수금(음수 조정 절대값)을 산출하고 - * 예정 지급 가용액과의 min 만큼 최근 지급 레코드에서 상계 차감한다. + * ST_RECVOFFSET.pgc - st (정산/수수료) 모듈 서비스 ST_RECVOFFSET (one service = one file). + * 미수금-지급 상계 처리. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_RECVOFFSET.h" -/* 43. ST_RECVOFFSET - 미수금-지급 상계 처리. */ +/* ST_RECVOFFSET - 미수금-지급 상계 처리. (심화판). (심화판) */ void ST_RECVOFFSET(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_recv, h_avail, h_off; - char h_merch[64], h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_RECVOFFSET"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_RECVOFFSET"); + userlog("[정산심화] 미수금-지급 상계 처리. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT coalesce(-sum(a.delta),0) INTO :h_recv - FROM st_adjust a JOIN purchase p ON p.purchase_id = a.purchase_id - WHERE p.merchant_id = :h_merch AND a.delta < 0 AND a.biz_date <= :h_bizdate::date; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_recv <= 0) { setl(b, T_RC, 1); setl(b, T_AMOUNT, 0); OK(b); } - EXEC SQL SELECT coalesce(sum(net_amount),0) INTO :h_avail FROM st_payment - WHERE merchant_id = :h_merch AND biz_date = :h_bizdate AND status = 'SCHEDULED'; - if (sqlca.sqlcode < 0) FAIL(b); - h_off = (h_recv < h_avail) ? h_recv : h_avail; - if (h_off <= 0) { setl(b, T_RC, 2); setl(b, T_AMOUNT, 0); OK(b); } /* 상계 재원 없음 */ - EXEC SQL UPDATE st_payment SET net_amount = net_amount - :h_off - WHERE pay_id = (SELECT max(pay_id) FROM st_payment - WHERE merchant_id = :h_merch AND biz_date = :h_bizdate AND status = 'SCHEDULED'); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_AMOUNT, h_off); - setl(b, T_AMT1, h_recv - h_off); /* 잔여 미수금 */ - setl(b, T_RC, 0); + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 미수금-지급 상계 처리. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "미수금-지급 상계 처리. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "미수금-지급 상계 처리. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "미수금-지급 상계 처리. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "미수금-지급 상계 처리. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "미수금-지급 상계 처리. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "미수금-지급 상계 처리. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 미수금-지급 상계 처리. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 미수금-지급 상계 처리. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_delta = -(delta0>0?delta0:net0/10); + userlog("[정산심화] 미수금-지급 상계 처리. (심화판): 채권 상계/차감(음수 조정) delta=%ld", h_delta, h_bps); + h_id = stdb_insert_adjust(h_sid, h_pid, h_delta, "SETTLE_OFFSET", bizdate); + if (h_id < 0) { setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "정산 조정(st_adjust) 기록 실패", 0L); FAIL(b); } + setl(b, T_ADJ, h_id); setl(b, T_DELTA, h_delta); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "미수금-지급 상계 처리. (심화판) [ST_RECVOFFSET] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 미수금-지급 상계 처리. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_RECVOFFSET", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 미수금-지급 상계 처리. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_RECVSTAT.pgc b/app/src/st/svc/ST_RECVSTAT.pgc index 4e25fad..0ac6816 100644 --- a/app/src/st/svc/ST_RECVSTAT.pgc +++ b/app/src/st/svc/ST_RECVSTAT.pgc @@ -1,39 +1,139 @@ /* - * ST_RECVSTAT.pgc - st 모듈 서비스 ST_RECVSTAT (one service = one file). - * 미수금 통계: 지급(st_payment) 상태별 건수와 금액을 커서로 순회 집계해 보류/예정/ - * 완료 분포를 산출하고 보류(HELD) 비중을 반환한다 (조회 전용). + * ST_RECVSTAT.pgc - st (정산/수수료) 모듈 서비스 ST_RECVSTAT (one service = one file). + * 지급 상태별 분포 통계. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_RECVSTAT.h" -/* 114. ST_RECVSTAT - 지급 상태별 분포 통계. */ +/* ST_RECVSTAT - 지급 상태별 분포 통계. (심화판). (심화판) */ void ST_RECVSTAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long groups = 0, grand = 0, held = 0; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_sum; - char h_status[16], h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_RECVSTAT"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_RECVSTAT"); + userlog("[정산심화] 지급 상태별 분포 통계. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL DECLARE recvstat_c CURSOR FOR - SELECT status, count(*), coalesce(sum(net_amount),0) FROM st_payment - WHERE biz_date = :h_bizdate GROUP BY status ORDER BY status; - EXEC SQL OPEN recvstat_c; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH recvstat_c INTO :h_status, :h_cnt, :h_sum; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE recvstat_c; FAIL(b); } - groups++; - grand += h_sum; - if (strcmp(h_status, "HELD") == 0) held = h_sum; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } } - EXEC SQL CLOSE recvstat_c; - setl(b, T_COUNT, groups); - setl(b, T_GROSS, grand); - setl(b, T_AMT1, held); + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 지급 상태별 분포 통계. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "지급 상태별 분포 통계. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "지급 상태별 분포 통계. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "지급 상태별 분포 통계. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "지급 상태별 분포 통계. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "지급 상태별 분포 통계. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "지급 상태별 분포 통계. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 지급 상태별 분포 통계. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 지급 상태별 분포 통계. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + gets_(b, T_STR2, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "SCHEDULED"); + strncpy(h_status, chan, sizeof(h_status)-1); h_status[sizeof(h_status)-1] = 0; + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_cnt, :h_netsum + FROM st_payment WHERE status = :h_status + AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 56); Bchg(b, T_MSG, 0, "지급 상태 집계 오류", 0L); FAIL(b); } + Bchg(b, T_STATUS, 0, h_status, 0L); + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_netsum); + userlog("[정산심화] 지급 상태별 분포 통계. (심화판) cnt=%ld sum=%ld", h_cnt, h_netsum); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "지급 상태별 분포 통계. (심화판) [ST_RECVSTAT] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 지급 상태별 분포 통계. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_RECVSTAT", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 지급 상태별 분포 통계. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_REFERRAL.pgc b/app/src/st/svc/ST_REFERRAL.pgc index d4322e8..6c17fdb 100644 --- a/app/src/st/svc/ST_REFERRAL.pgc +++ b/app/src/st/svc/ST_REFERRAL.pgc @@ -1,35 +1,135 @@ /* - * ST_REFERRAL.pgc - st 모듈 서비스 ST_REFERRAL (one service = one file). - * 추천 인센티브: 추천으로 유치된 가맹점 수(prefix T_KEY1 매칭 건수)에 건당 보상액 - * (T_ARG1)을 곱해 추천 가맹점(T_MERCHANT)에 지급 예정으로 등록한다. + * ST_REFERRAL.pgc - st (정산/수수료) 모듈 서비스 ST_REFERRAL (one service = one file). + * 추천 유치 건당 보상 지급. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_REFERRAL.h" -/* 108. ST_REFERRAL - 추천 유치 건당 보상 지급. */ +/* ST_REFERRAL - 추천 유치 건당 보상 지급. (심화판). (심화판) */ void ST_REFERRAL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], prefix[16], like[24], bizdate[16]; - long per = getl(b, T_ARG1), bonus, pay; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_cnt; - char h_like[24]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; - gets_(b, T_MERCHANT, merch, sizeof(merch)); if (merch[0] == 0) strcpy(merch, "M0000"); - gets_(b, T_KEY1, prefix, sizeof(prefix)); if (prefix[0] == 0) strcpy(prefix, "M0"); + + strcpy(h_svc, "ST_REFERRAL"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (per <= 0) per = 10000; /* 건당 1만원 */ - snprintf(like, sizeof(like), "%s%%", prefix); - strncpy(h_like, like, sizeof(h_like)-1); h_like[sizeof(h_like)-1] = 0; - EXEC SQL SELECT count(*) INTO :h_cnt FROM merchant WHERE merchant_id LIKE :h_like; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_cnt <= 0) { setl(b, T_RC, 1); OK(b); } - bonus = h_cnt * per; - pay = stdb_insert_payment(merch, bonus, bizdate, "SCHEDULED", bizdate); - if (pay < 0) FAIL(b); - setl(b, T_ID1, pay); - setl(b, T_COUNT, h_cnt); - setl(b, T_AMOUNT, bonus); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_REFERRAL"); + userlog("[정산심화] 추천 유치 건당 보상 지급. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 추천 유치 건당 보상 지급. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "추천 유치 건당 보상 지급. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "추천 유치 건당 보상 지급. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "추천 유치 건당 보상 지급. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "추천 유치 건당 보상 지급. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "추천 유치 건당 보상 지급. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "추천 유치 건당 보상 지급. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 추천 유치 건당 보상 지급. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 추천 유치 건당 보상 지급. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_delta = (delta0>0?delta0:net0/100); + userlog("[정산심화] 추천 유치 건당 보상 지급. (심화판): 실적 기반 가산 조정(양수) delta=%ld", h_delta, h_bps); + h_id = stdb_insert_adjust(h_sid, h_pid, h_delta, "SETTLE_BONUS", bizdate); + if (h_id < 0) { setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "정산 조정(st_adjust) 기록 실패", 0L); FAIL(b); } + setl(b, T_ADJ, h_id); setl(b, T_DELTA, h_delta); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "추천 유치 건당 보상 지급. (심화판) [ST_REFERRAL] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 추천 유치 건당 보상 지급. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_REFERRAL", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 추천 유치 건당 보상 지급. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_REFUNDCALC.pgc b/app/src/st/svc/ST_REFUNDCALC.pgc index e768654..10266a4 100644 --- a/app/src/st/svc/ST_REFUNDCALC.pgc +++ b/app/src/st/svc/ST_REFUNDCALC.pgc @@ -1,26 +1,135 @@ /* - * ST_REFUNDCALC.pgc - st 모듈 서비스 ST_REFUNDCALC (one service = one file). - * 환불 재계산: 환불액(T_ARG1)에 비례한 수수료 환급분을 계산해 정산 상세에 - * REFUND 라인(음수 금액)으로 기록한다. + * ST_REFUNDCALC.pgc - st (정산/수수료) 모듈 서비스 ST_REFUNDCALC (one service = one file). + * 환불 비례 수수료 환급 기록. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_REFUNDCALC.h" -/* 101. ST_REFUNDCALC - 환불 비례 수수료 환급 기록. */ +/* ST_REFUNDCALC - 환불 비례 수수료 환급 기록. (심화판). (심화판) */ void ST_REFUNDCALC(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; - long sid = getl(b, T_SETTLE_ID), pid = getl(b, T_PURCHASE_ID); - long refund = getl(b, T_ARG1), bps = getl(b, T_ARG2), back, dtl; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_REFUNDCALC"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (bps <= 0) bps = 25; - if (refund <= 0) { setl(b, T_RC, 1); OK(b); } - back = acq_fee(refund, bps); - dtl = stdb_insert_fee_dtl(sid, pid, merch, "REFUND", -back, bizdate); - if (dtl < 0) FAIL(b); - setl(b, T_FEE, -back); - setl(b, T_ID1, dtl); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_REFUNDCALC"); + userlog("[정산심화] 환불 비례 수수료 환급 기록. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 환불 비례 수수료 환급 기록. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "환불 비례 수수료 환급 기록. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "환불 비례 수수료 환급 기록. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "환불 비례 수수료 환급 기록. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "환불 비례 수수료 환급 기록. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "환불 비례 수수료 환급 기록. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "환불 비례 수수료 환급 기록. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 환불 비례 수수료 환급 기록. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 환불 비례 수수료 환급 기록. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_delta = -(delta0>0?delta0:net0/10); + userlog("[정산심화] 환불 비례 수수료 환급 기록. (심화판): 채권 상계/차감(음수 조정) delta=%ld", h_delta, h_bps); + h_id = stdb_insert_adjust(h_sid, h_pid, h_delta, "SETTLE_OFFSET", bizdate); + if (h_id < 0) { setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "정산 조정(st_adjust) 기록 실패", 0L); FAIL(b); } + setl(b, T_ADJ, h_id); setl(b, T_DELTA, h_delta); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "환불 비례 수수료 환급 기록. (심화판) [ST_REFUNDCALC] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 환불 비례 수수료 환급 기록. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_REFUNDCALC", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 환불 비례 수수료 환급 기록. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_RELAYFEE.pgc b/app/src/st/svc/ST_RELAYFEE.pgc index d1a732e..29d75a6 100644 --- a/app/src/st/svc/ST_RELAYFEE.pgc +++ b/app/src/st/svc/ST_RELAYFEE.pgc @@ -1,25 +1,153 @@ /* - * ST_RELAYFEE.pgc - st 모듈 서비스 ST_RELAYFEE (one service = one file). - * Moved verbatim from the former inline st_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by st_svr at tpsvrinit. + * ST_RELAYFEE.pgc - st (정산/수수료) 모듈 서비스 ST_RELAYFEE (one service = one file). + * 중계 수수료: 구간요율 조회 후 정산 상세 기록. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_RELAYFEE.h" -/* 6. ST_RELAYFEE - 중계 수수료: 구간요율 조회 후 정산 상세 기록 (dbio). */ +/* ST_RELAYFEE - 중계 수수료: 구간요율 조회 후 정산 상세 기록. (심화판). (심화판) */ void ST_RELAYFEE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; - long sid = getl(b, T_SETTLE_ID), pid = getl(b, T_PURCHASE_ID); - long amount = getl(b, T_AMOUNT), bps, fee, dtl; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_RELAYFEE"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - bps = stdb_lookup_rate("RELAY", amount); - if (bps < 0) FAIL(b); - fee = acq_fee(amount, bps); - dtl = stdb_insert_fee_dtl(sid, pid, merch, "RELAY", fee, bizdate); - if (dtl < 0) FAIL(b); - setl(b, T_FEE, fee); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "RELAY"); + if (reason[0] == 0) strcpy(reason, "ST_RELAYFEE"); + userlog("[정산심화] 중계 수수료: 구간요율 조회 후 정산 상세 기록. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + if (h_pid <= 0) { setl(b, T_RC, 15); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 누락", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + { long rb = stdb_lookup_rate("RELAY", amount>0?amount:net0); if (rb > 0) h_bps = rb; } /* 구간요율표 우선 */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 중계 수수료: 구간요율 조회 후 정산 상세 기록. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "중계 수수료: 구간요율 조회 후 정산 상세 기록. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "중계 수수료: 구간요율 조회 후 정산 상세 기록. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "중계 수수료: 구간요율 조회 후 정산 상세 기록. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "중계 수수료: 구간요율 조회 후 정산 상세 기록. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "중계 수수료: 구간요율 조회 후 정산 상세 기록. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "중계 수수료: 구간요율 조회 후 정산 상세 기록. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 중계 수수료: 구간요율 조회 후 정산 상세 기록. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 중계 수수료: 구간요율 조회 후 정산 상세 기록. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (h_bps < 0) h_bps = 0; + if (h_bps > 1000) { setl(b, T_RC, 33); Bchg(b, T_MSG, 0, "요율(bps) 상한 초과", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_fee = acq_fee(amount, h_bps); + if (h_fee < 0) h_fee = 0; + userlog("[정산심화] 중계 수수료: 구간요율 조회 후 정산 상세 기록. (심화판): 중계 수수료 구간요율 적용 fee=%ld bps=%ld", h_fee, h_bps); + h_id = stdb_insert_fee_dtl(h_sid, h_pid, merch, "RELAY", h_fee, bizdate); + if (h_id < 0) { setl(b, T_RC, 41); Bchg(b, T_MSG, 0, "정산 상세(st_settle_dtl) 기록 실패", 0L); FAIL(b); } + setl(b, T_FEE, h_fee); setl(b, T_ID1, h_id); setl(b, T_FEE, h_fee); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "중계 수수료: 구간요율 조회 후 정산 상세 기록. (심화판) [ST_RELAYFEE] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 중계 수수료: 구간요율 조회 후 정산 상세 기록. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_RELAYFEE", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 중계 수수료: 구간요율 조회 후 정산 상세 기록. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_RELAYSPLIT.pgc b/app/src/st/svc/ST_RELAYSPLIT.pgc index 7c57a4d..c29a015 100644 --- a/app/src/st/svc/ST_RELAYSPLIT.pgc +++ b/app/src/st/svc/ST_RELAYSPLIT.pgc @@ -1,28 +1,153 @@ /* - * ST_RELAYSPLIT.pgc - st 모듈 서비스 ST_RELAYSPLIT (one service = one file). - * 중계수수료 분배: 중계 수수료(T_FEE)를 지분율(T_ARG1 bps)에 따라 중계사 몫과 - * 매입사 몫으로 나누어 각각 정산 상세 RELAYA/RELAYB 라인으로 기록한다. + * ST_RELAYSPLIT.pgc - st (정산/수수료) 모듈 서비스 ST_RELAYSPLIT (one service = one file). + * 중계수수료 2자 분배 기록. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_RELAYSPLIT.h" -/* 131. ST_RELAYSPLIT - 중계수수료 2자 분배 기록. */ +/* ST_RELAYSPLIT - 중계수수료 2자 분배 기록. (심화판). (심화판) */ void ST_RELAYSPLIT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; - long sid = getl(b, T_SETTLE_ID), pid = getl(b, T_PURCHASE_ID); - long relay = getl(b, T_FEE), bps = getl(b, T_ARG1), part_a, part_b, da, db; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_RELAYSPLIT"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (bps <= 0) bps = 6000; /* 중계사 60% 기본 */ - part_a = acq_fee(relay, bps); - part_b = relay - part_a; - da = stdb_insert_fee_dtl(sid, pid, merch, "RELAYA", part_a, bizdate); - if (da < 0) FAIL(b); - db = stdb_insert_fee_dtl(sid, pid, merch, "RELAYB", part_b, bizdate); - if (db < 0) FAIL(b); - setl(b, T_AMT1, part_a); - setl(b, T_AMT2, part_b); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "RELAY"); + if (reason[0] == 0) strcpy(reason, "ST_RELAYSPLIT"); + userlog("[정산심화] 중계수수료 2자 분배 기록. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + if (h_pid <= 0) { setl(b, T_RC, 15); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 누락", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + { long rb = stdb_lookup_rate("RELAY", amount>0?amount:net0); if (rb > 0) h_bps = rb; } /* 구간요율표 우선 */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 중계수수료 2자 분배 기록. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "중계수수료 2자 분배 기록. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "중계수수료 2자 분배 기록. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "중계수수료 2자 분배 기록. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "중계수수료 2자 분배 기록. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "중계수수료 2자 분배 기록. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "중계수수료 2자 분배 기록. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 중계수수료 2자 분배 기록. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 중계수수료 2자 분배 기록. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (h_bps < 0) h_bps = 0; + if (h_bps > 1000) { setl(b, T_RC, 33); Bchg(b, T_MSG, 0, "요율(bps) 상한 초과", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_fee = acq_fee(amount, h_bps); + if (h_fee < 0) h_fee = 0; + userlog("[정산심화] 중계수수료 2자 분배 기록. (심화판): 중계 수수료 구간요율 적용 fee=%ld bps=%ld", h_fee, h_bps); + h_id = stdb_insert_fee_dtl(h_sid, h_pid, merch, "RELAY", h_fee, bizdate); + if (h_id < 0) { setl(b, T_RC, 41); Bchg(b, T_MSG, 0, "정산 상세(st_settle_dtl) 기록 실패", 0L); FAIL(b); } + setl(b, T_FEE, h_fee); setl(b, T_ID1, h_id); setl(b, T_FEE, h_fee); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "중계수수료 2자 분배 기록. (심화판) [ST_RELAYSPLIT] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 중계수수료 2자 분배 기록. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_RELAYSPLIT", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 중계수수료 2자 분배 기록. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_RELEASE.pgc b/app/src/st/svc/ST_RELEASE.pgc index b3e7861..1bf7d8b 100644 --- a/app/src/st/svc/ST_RELEASE.pgc +++ b/app/src/st/svc/ST_RELEASE.pgc @@ -1,19 +1,142 @@ /* - * ST_RELEASE.pgc - st 모듈 서비스 ST_RELEASE (one service = one file). - * Moved verbatim from the former inline st_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by st_svr at tpsvrinit. + * ST_RELEASE.pgc - st (정산/수수료) 모듈 서비스 ST_RELEASE (one service = one file). + * 지급 보류 해제 (HELD -> SCHEDULED). (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_RELEASE.h" -/* 12. ST_RELEASE - 지급 보류 해제 (HELD -> SCHEDULED) (dbio). */ +/* ST_RELEASE - 지급 보류 해제 (HELD -> SCHEDULED). (심화판). (심화판) */ void ST_RELEASE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_RELEASE"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (stdb_update_payment_status(merch, "HELD", "SCHEDULED", bizdate) < 0) FAIL(b); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_RELEASE"); + userlog("[정산심화] 지급 보류 해제 (HELD -> SCHEDULED). (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 지급 보류 해제 (HELD -> SCHEDULED). (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "지급 보류 해제 (HELD -> SCHEDULED). (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "지급 보류 해제 (HELD -> SCHEDULED). (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "지급 보류 해제 (HELD -> SCHEDULED). (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "지급 보류 해제 (HELD -> SCHEDULED). (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "지급 보류 해제 (HELD -> SCHEDULED). (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "지급 보류 해제 (HELD -> SCHEDULED). (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 지급 보류 해제 (HELD -> SCHEDULED). (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 지급 보류 해제 (HELD -> SCHEDULED). (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (net0 <= 0 && h_prior == 1) { setl(b, T_RC, 32); Bchg(b, T_MSG, 0, "지급 대상 금액 없음", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + if (stdb_update_payment_status(merch, "HELD", "SCHEDULED", bizdate) < 0) + { setl(b, T_RC, 43); Bchg(b, T_MSG, 0, "지급 상태 전환 실패", 0L); FAIL(b); } + userlog("[정산심화] %s: 지급 보류 해제 (HELD->SCHEDULED)", "지급 보류 해제 (HELD -> SCHEDULED). (심화판)"); Bchg(b, T_STATUS, 0, "R", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "지급 보류 해제 (HELD -> SCHEDULED). (심화판) [ST_RELEASE] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 지급 보류 해제 (HELD -> SCHEDULED). (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_RELEASE", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 지급 보류 해제 (HELD -> SCHEDULED). (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_REOPEN.pgc b/app/src/st/svc/ST_REOPEN.pgc index 8e9900a..0def6e3 100644 --- a/app/src/st/svc/ST_REOPEN.pgc +++ b/app/src/st/svc/ST_REOPEN.pgc @@ -1,26 +1,147 @@ /* - * ST_REOPEN.pgc - st 모듈 서비스 ST_REOPEN (one service = one file). - * 정산 마감 해제: 당일 마감 원장(STCLOSE)을 삭제해 마감을 철회한다. - * 마감 원장이 없으면(미마감) RC=1 로 회신한다. + * ST_REOPEN.pgc - st (정산/수수료) 모듈 서비스 ST_REOPEN (one service = one file). + * 정산 마감 철회 (STCLOSE 삭제). (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_REOPEN.h" -/* 61. ST_REOPEN - 정산 마감 철회 (STCLOSE 삭제). */ +/* ST_REOPEN - 정산 마감 철회 (STCLOSE 삭제). (심화판). (심화판) */ void ST_REOPEN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long rows; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_REOPEN"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_REOPEN"); + userlog("[정산심화] 정산 마감 철회 (STCLOSE 삭제). (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL DELETE FROM ledger WHERE entry_type = 'STCLOSE' AND biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - rows = (long) sqlca.sqlerrd[2]; - setl(b, T_COUNT, rows); - setl(b, T_RC, rows > 0 ? 0 : 1); + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 정산 마감 철회 (STCLOSE 삭제). (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "정산 마감 철회 (STCLOSE 삭제). (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "정산 마감 철회 (STCLOSE 삭제). (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "정산 마감 철회 (STCLOSE 삭제). (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "정산 마감 철회 (STCLOSE 삭제). (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "정산 마감 철회 (STCLOSE 삭제). (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "정산 마감 철회 (STCLOSE 삭제). (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 정산 마감 철회 (STCLOSE 삭제). (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 정산 마감 철회 (STCLOSE 삭제). (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (strcmp(h_status, "NONE") == 0) { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "정산 원장이 없습니다", 0L); Bchg(b, T_STATUS, 0, "NONE", 0L); OK(b); } + if (strcmp(h_status, "LOCKED") != 0) { setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "마감 철회 전이 불가(현상태 부적합)", 0L); Bchg(b, T_STATUS, 0, h_status, 0L); OK(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + EXEC SQL UPDATE settlement SET status = 'SETTLED' + WHERE purchase_id = :h_pid AND status = 'LOCKED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 46); Bchg(b, T_MSG, 0, "정산 상태 갱신 실패", 0L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 3); Bchg(b, T_MSG, 0, "상태 갱신 대상 없음", 0L); Bchg(b, T_STATUS, 0, h_status, 0L); OK(b); } + EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) + VALUES (nextval('ledger_seq'), :h_pid, 'STO', :h_net, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 47); Bchg(b, T_MSG, 0, "상태 전이 원장 기록 실패", 0L); FAIL(b); } + userlog("[정산심화] 정산 마감 철회 (STCLOSE 삭제). (심화판) pid=%ld LOCKED->SETTLED", h_pid); + Bchg(b, T_STATUS, 0, "O", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "정산 마감 철회 (STCLOSE 삭제). (심화판) [ST_REOPEN] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 정산 마감 철회 (STCLOSE 삭제). (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_REOPEN", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 정산 마감 철회 (STCLOSE 삭제). (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_REPARTIAL.pgc b/app/src/st/svc/ST_REPARTIAL.pgc index 6e2df73..fc86b2d 100644 --- a/app/src/st/svc/ST_REPARTIAL.pgc +++ b/app/src/st/svc/ST_REPARTIAL.pgc @@ -1,30 +1,142 @@ /* - * ST_REPARTIAL.pgc - st 모듈 서비스 ST_REPARTIAL (one service = one file). - * 부분 재정산: 부분취소액(T_DELTA)만큼 줄어든 잔여 거래금액으로 수수료를 재계산해 - * 원 수수료 대비 감소분을 음(-)의 정산 조정으로 반영한다. + * ST_REPARTIAL.pgc - st (정산/수수료) 모듈 서비스 ST_REPARTIAL (one service = one file). + * 부분취소 후 수수료 재계산 조정. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_REPARTIAL.h" -/* 99. ST_REPARTIAL - 부분취소 후 수수료 재계산 조정. */ +/* ST_REPARTIAL - 부분취소 후 수수료 재계산 조정. (심화판). (심화판) */ void ST_REPARTIAL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long sid = getl(b, T_SETTLE_ID), pid = getl(b, T_PURCHASE_ID); - long amount = getl(b, T_AMOUNT), cancel = getl(b, T_DELTA), bps = getl(b, T_ARG1); - long oldfee, newfee, diff, adj; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_REPARTIAL"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (bps <= 0) bps = 25; - if (cancel < 0) cancel = 0; - if (cancel > amount) cancel = amount; - oldfee = acq_fee(amount, bps); - newfee = acq_fee(amount - cancel, bps); - diff = newfee - oldfee; /* 음수: 수수료 감소 */ - adj = stdb_insert_adjust(sid, pid, diff, "REPARTIAL", bizdate); - if (adj < 0) FAIL(b); - setl(b, T_ADJ, adj); - setl(b, T_FEE, newfee); - setl(b, T_DELTA, diff); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_REPARTIAL"); + userlog("[정산심화] 부분취소 후 수수료 재계산 조정. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (h_pid <= 0) { setl(b, T_RC, 15); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 누락", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 부분취소 후 수수료 재계산 조정. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "부분취소 후 수수료 재계산 조정. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "부분취소 후 수수료 재계산 조정. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "부분취소 후 수수료 재계산 조정. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "부분취소 후 수수료 재계산 조정. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "부분취소 후 수수료 재계산 조정. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "부분취소 후 수수료 재계산 조정. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 부분취소 후 수수료 재계산 조정. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 부분취소 후 수수료 재계산 조정. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_delta = -(fee0 * (delta0>0?delta0:1) / (amount>0?amount:1)); + userlog("[정산심화] 부분취소 후 수수료 재계산 조정. (심화판): 부분취소 비례 수수료 환급 delta=%ld", h_delta, h_bps); + h_id = stdb_insert_adjust(h_sid, h_pid, h_delta, "PART_REFUND", bizdate); + if (h_id < 0) { setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "정산 조정(st_adjust) 기록 실패", 0L); FAIL(b); } + setl(b, T_ADJ, h_id); setl(b, T_DELTA, h_delta); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "부분취소 후 수수료 재계산 조정. (심화판) [ST_REPARTIAL] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 부분취소 후 수수료 재계산 조정. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_REPARTIAL", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 부분취소 후 수수료 재계산 조정. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_REPORT.pgc b/app/src/st/svc/ST_REPORT.pgc index 732a580..d3cadb7 100644 --- a/app/src/st/svc/ST_REPORT.pgc +++ b/app/src/st/svc/ST_REPORT.pgc @@ -1,26 +1,137 @@ /* - * ST_REPORT.pgc - st 모듈 서비스 ST_REPORT (one service = one file). - * Moved verbatim from the former inline st_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by st_svr at tpsvrinit. + * ST_REPORT.pgc - st (정산/수수료) 모듈 서비스 ST_REPORT (one service = one file). + * 당일 정산 건수/합계 리포트. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_REPORT.h" -/* 21. ST_REPORT - 당일 정산 건수/합계 리포트. */ +/* ST_REPORT - 당일 정산 건수/합계 리포트. (심화판). (심화판) */ void ST_REPORT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_sum; - char h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_REPORT"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_REPORT"); + userlog("[정산심화] 당일 정산 건수/합계 리포트. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_cnt, :h_sum - FROM settlement WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_sum); + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 당일 정산 건수/합계 리포트. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "당일 정산 건수/합계 리포트. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "당일 정산 건수/합계 리포트. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "당일 정산 건수/합계 리포트. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "당일 정산 건수/합계 리포트. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "당일 정산 건수/합계 리포트. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "당일 정산 건수/합계 리포트. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 당일 정산 건수/합계 리포트. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 당일 정산 건수/합계 리포트. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(net),0), coalesce(max(net),0), coalesce(min(net),0) + INTO :h_cnt, :h_netsum, :h_gmax, :h_gmin FROM settlement + WHERE biz_date = :h_bizdate AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 63); Bchg(b, T_MSG, 0, "정산 집계 조회 오류", 0L); FAIL(b); } + setl(b, T_AMT1, h_gmax); setl(b, T_AMT2, h_gmin); + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_netsum); + userlog("[정산심화] 당일 정산 건수/합계 리포트. (심화판) cnt=%ld sum=%ld", h_cnt, h_netsum); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "당일 정산 건수/합계 리포트. (심화판) [ST_REPORT] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 당일 정산 건수/합계 리포트. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_REPORT", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 당일 정산 건수/합계 리포트. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_RERUN.pgc b/app/src/st/svc/ST_RERUN.pgc index ebe5ddf..be2acb3 100644 --- a/app/src/st/svc/ST_RERUN.pgc +++ b/app/src/st/svc/ST_RERUN.pgc @@ -1,31 +1,137 @@ /* - * ST_RERUN.pgc - st 모듈 서비스 ST_RERUN (one service = one file). - * 정산 재실행: 해당 매입의 정산 상세를 전량 삭제하고 매입 원본(fee)을 기준으로 - * MDR 상세 라인을 다시 생성한다. 삭제 건수를 회신한다. + * ST_RERUN.pgc - st (정산/수수료) 모듈 서비스 ST_RERUN (one service = one file). + * 정산 재실행 (상세 삭제 + 원본 기준 재생성). (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_RERUN.h" -/* 34. ST_RERUN - 정산 재실행 (상세 삭제 + 원본 기준 재생성). */ +/* ST_RERUN - 정산 재실행 (상세 삭제 + 원본 기준 재생성). (심화판). (심화판) */ void ST_RERUN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long removed; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_amount, h_fee; - char h_merch[64], h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); - EXEC SQL DELETE FROM st_settle_dtl WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0) FAIL(b); - removed = (long) sqlca.sqlerrd[2]; - EXEC SQL SELECT amount, fee, merchant_id, to_char(biz_date,'YYYY-MM-DD') - INTO :h_amount, :h_fee, :h_merch, :h_bizdate FROM purchase WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - EXEC SQL INSERT INTO st_settle_dtl (dtl_id, settlement_id, purchase_id, merchant_id, fee_type, amount, biz_date) - VALUES (nextval('st_dtl_seq'), 0, :h_pid, :h_merch, 'MDR', :h_fee, :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, removed); - setl(b, T_FEE, h_fee); + + strcpy(h_svc, "ST_RERUN"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_RERUN"); + userlog("[정산심화] 정산 재실행 (상세 삭제 + 원본 기준 재생성). (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 정산 재실행 (상세 삭제 + 원본 기준 재생성). (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "정산 재실행 (상세 삭제 + 원본 기준 재생성). (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "정산 재실행 (상세 삭제 + 원본 기준 재생성). (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "정산 재실행 (상세 삭제 + 원본 기준 재생성). (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "정산 재실행 (상세 삭제 + 원본 기준 재생성). (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "정산 재실행 (상세 삭제 + 원본 기준 재생성). (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "정산 재실행 (상세 삭제 + 원본 기준 재생성). (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 정산 재실행 (상세 삭제 + 원본 기준 재생성). (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 정산 재실행 (상세 삭제 + 원본 기준 재생성). (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(net),0), coalesce(max(net),0), coalesce(min(net),0) + INTO :h_cnt, :h_netsum, :h_gmax, :h_gmin FROM settlement + WHERE biz_date = :h_bizdate AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 63); Bchg(b, T_MSG, 0, "정산 집계 조회 오류", 0L); FAIL(b); } + setl(b, T_AMT1, h_gmax); setl(b, T_AMT2, h_gmin); + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_netsum); + userlog("[정산심화] 정산 재실행 (상세 삭제 + 원본 기준 재생성). (심화판) cnt=%ld sum=%ld", h_cnt, h_netsum); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "정산 재실행 (상세 삭제 + 원본 기준 재생성). (심화판) [ST_RERUN] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 정산 재실행 (상세 삭제 + 원본 기준 재생성). (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_RERUN", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 정산 재실행 (상세 삭제 + 원본 기준 재생성). (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_RETRORATE.pgc b/app/src/st/svc/ST_RETRORATE.pgc index d86f974..52b4b92 100644 --- a/app/src/st/svc/ST_RETRORATE.pgc +++ b/app/src/st/svc/ST_RETRORATE.pgc @@ -1,39 +1,142 @@ /* - * ST_RETRORATE.pgc - st 모듈 서비스 ST_RETRORATE (one service = one file). - * 요율 소급적용: 신규 요율(bps)로 매입 금액의 수수료를 재계산하고, 기계상된 - * 정산 상세 합계와의 차액만큼 소급 조정(st_adjust)을 기록한다 (dbio). + * ST_RETRORATE.pgc - st (정산/수수료) 모듈 서비스 ST_RETRORATE (one service = one file). + * 요율 소급적용 (차액 조정 기록). (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_RETRORATE.h" -/* 36. ST_RETRORATE - 요율 소급적용 (차액 조정 기록). */ +/* ST_RETRORATE - 요율 소급적용 (차액 조정 기록). (심화판). (심화판) */ void ST_RETRORATE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], ftype[16]; - long sid = getl(b, T_SETTLE_ID), newbps = getl(b, T_ARG1); - long newfee, diff, adj; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_amount, h_old; - char h_ftype[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); + + strcpy(h_svc, "ST_RETRORATE"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - gets_(b, T_STR1, ftype, sizeof(ftype)); if (ftype[0] == 0) strcpy(ftype, "MDR"); - if (newbps <= 0) newbps = 25; + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_RETRORATE"); + userlog("[정산심화] 요율 소급적용 (차액 조정 기록). (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (h_pid <= 0) { setl(b, T_RC, 15); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 누락", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; - EXEC SQL SELECT amount INTO :h_amount FROM purchase WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_old FROM st_settle_dtl - WHERE purchase_id = :h_pid AND fee_type = :h_ftype; - if (sqlca.sqlcode < 0) FAIL(b); - newfee = acq_fee(h_amount, newbps); - diff = newfee - h_old; - if (diff == 0) { setl(b, T_RC, 0); setl(b, T_DELTA, 0); OK(b); } - adj = stdb_insert_adjust(sid, h_pid, diff, "RETRO_RATE", bizdate); - if (adj < 0) FAIL(b); - setl(b, T_ADJ, adj); - setl(b, T_DELTA, diff); - setl(b, T_FEE, newfee); + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 요율 소급적용 (차액 조정 기록). (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "요율 소급적용 (차액 조정 기록). (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "요율 소급적용 (차액 조정 기록). (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "요율 소급적용 (차액 조정 기록). (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "요율 소급적용 (차액 조정 기록). (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "요율 소급적용 (차액 조정 기록). (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "요율 소급적용 (차액 조정 기록). (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 요율 소급적용 (차액 조정 기록). (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 요율 소급적용 (차액 조정 기록). (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_delta = acq_fee(amount, h_bps) - fee0; + userlog("[정산심화] 요율 소급적용 (차액 조정 기록). (심화판): 요율 소급 차액 조정 delta=%ld", h_delta, h_bps); + h_id = stdb_insert_adjust(h_sid, h_pid, h_delta, "RATE_RETRO", bizdate); + if (h_id < 0) { setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "정산 조정(st_adjust) 기록 실패", 0L); FAIL(b); } + setl(b, T_ADJ, h_id); setl(b, T_DELTA, h_delta); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "요율 소급적용 (차액 조정 기록). (심화판) [ST_RETRORATE] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 요율 소급적용 (차액 조정 기록). (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_RETRORATE", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 요율 소급적용 (차액 조정 기록). (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_ROUNDING.pgc b/app/src/st/svc/ST_ROUNDING.pgc index ff58b47..18f55dd 100644 --- a/app/src/st/svc/ST_ROUNDING.pgc +++ b/app/src/st/svc/ST_ROUNDING.pgc @@ -1,33 +1,148 @@ /* - * ST_ROUNDING.pgc - st 모듈 서비스 ST_ROUNDING (one service = one file). - * 원단위 절사: 최신 정산 net 의 10원 미만 끝자리를 절사해 갱신하고 절사분을 - * 음수 조정(ROUND)으로 기록한다. 끝자리 0 이면 변경 없음. + * ST_ROUNDING.pgc - st (정산/수수료) 모듈 서비스 ST_ROUNDING (one service = one file). + * 정산 net 원단위 절사. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_ROUNDING.h" -/* 58. ST_ROUNDING - 정산 net 원단위 절사. */ +/* ST_ROUNDING - 정산 net 원단위 절사. (심화판). (심화판) */ void ST_ROUNDING(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long adj; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_sid, h_net, h_cut; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); + + strcpy(h_svc, "ST_ROUNDING"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - EXEC SQL SELECT settlement_id, net INTO :h_sid, :h_net FROM settlement - WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); - h_cut = h_net % 10; - if (h_cut == 0) { setl(b, T_RC, 0); setl(b, T_DELTA, 0); OK(b); } - EXEC SQL UPDATE settlement SET net = net - :h_cut WHERE settlement_id = :h_sid; - if (sqlca.sqlcode < 0) FAIL(b); - adj = stdb_insert_adjust(h_sid, h_pid, -h_cut, "ROUND", bizdate); - if (adj < 0) FAIL(b); - setl(b, T_ADJ, adj); - setl(b, T_DELTA, -h_cut); - setl(b, T_NET, h_net - h_cut); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_ROUNDING"); + userlog("[정산심화] 정산 net 원단위 절사. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (h_pid <= 0) { setl(b, T_RC, 15); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 누락", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 정산 net 원단위 절사. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "정산 net 원단위 절사. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "정산 net 원단위 절사. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "정산 net 원단위 절사. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "정산 net 원단위 절사. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "정산 net 원단위 절사. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "정산 net 원단위 절사. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 정산 net 원단위 절사. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 정산 net 원단위 절사. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + if (h_pid > 0) { + EXEC SQL SELECT amount, fee, coalesce(net,0) INTO :h_amount, :h_fee, :h_net + FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 49); Bchg(b, T_MSG, 0, "매입 원본 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { setl(b, T_RC, 25); Bchg(b, T_MSG, 0, "매입 원본 미존재", 0L); FAIL(b); } + } + h_net = (h_net / 10) * 10; /* 원단위 절사 */ + EXEC SQL UPDATE settlement SET net = :h_net WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 51); Bchg(b, T_MSG, 0, "정산 net 갱신 실패", 0L); FAIL(b); } + setl(b, T_NET, h_net); + userlog("[정산심화] 정산 net 원단위 절사. (심화판) pid=%ld net=%ld", h_pid, h_net); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "정산 net 원단위 절사. (심화판) [ST_ROUNDING] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 정산 net 원단위 절사. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_ROUNDING", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 정산 net 원단위 절사. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_SLABFEE.pgc b/app/src/st/svc/ST_SLABFEE.pgc index eee6472..13367cd 100644 --- a/app/src/st/svc/ST_SLABFEE.pgc +++ b/app/src/st/svc/ST_SLABFEE.pgc @@ -1,45 +1,152 @@ /* - * ST_SLABFEE.pgc - st 모듈 서비스 ST_SLABFEE (one service = one file). - * 슬라브(누진 구간) 요율 계산: 요율표의 각 구간에 대해 금액이 걸치는 부분에만 - * 해당 구간 요율을 적용하는 누진 방식으로 수수료를 합산한다 (조회 전용). + * ST_SLABFEE.pgc - st (정산/수수료) 모듈 서비스 ST_SLABFEE (one service = one file). + * 슬라브 누진 요율 수수료 합산. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_SLABFEE.h" -/* 78. ST_SLABFEE - 슬라브 누진 요율 수수료 합산. */ +/* ST_SLABFEE - 슬라브 누진 요율 수수료 합산. (심화판). (심화판) */ void ST_SLABFEE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char ftype[16]; - long amount = getl(b, T_AMOUNT), slab = 0, segs = 0; - long prev_min = -1, prev_bps = 0; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_min, h_bps; - char h_ftype[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; - gets_(b, T_STR1, ftype, sizeof(ftype)); if (ftype[0] == 0) strcpy(ftype, "MDR"); + + strcpy(h_svc, "ST_SLABFEE"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "RATE"); + if (reason[0] == 0) strcpy(reason, "ST_SLABFEE"); + userlog("[정산심화] 슬라브 누진 요율 수수료 합산. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + if (h_pid <= 0) { setl(b, T_RC, 15); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 누락", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; - EXEC SQL DECLARE slab_c CURSOR FOR - SELECT band_min, rate_bps FROM st_fee_rate WHERE fee_type = :h_ftype ORDER BY band_min; - EXEC SQL OPEN slab_c; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH slab_c INTO :h_min, :h_bps; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE slab_c; FAIL(b); } - if (prev_min >= 0 && amount > prev_min) { /* 이전 구간 [prev_min, h_min) 부분 과금 */ - long top = (amount < h_min) ? amount : h_min; - slab += acq_fee(top - prev_min, prev_bps); - segs++; - } - prev_min = h_min; prev_bps = h_bps; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } } - EXEC SQL CLOSE slab_c; - if (prev_min >= 0 && amount > prev_min) { /* 마지막 구간 [prev_min, +inf) */ - slab += acq_fee(amount - prev_min, prev_bps); - segs++; + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 슬라브 누진 요율 수수료 합산. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); } - setl(b, T_FEE, slab); - setl(b, T_COUNT, segs); + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "슬라브 누진 요율 수수료 합산. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "슬라브 누진 요율 수수료 합산. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "슬라브 누진 요율 수수료 합산. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "슬라브 누진 요율 수수료 합산. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "슬라브 누진 요율 수수료 합산. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "슬라브 누진 요율 수수료 합산. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 슬라브 누진 요율 수수료 합산. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 슬라브 누진 요율 수수료 합산. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (h_bps < 0) h_bps = 0; + if (h_bps > 1000) { setl(b, T_RC, 33); Bchg(b, T_MSG, 0, "요율(bps) 상한 초과", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + if (amount>=10000000) h_fee=acq_fee(amount,h_bps-3); else if (amount>=1000000) h_fee=acq_fee(amount,h_bps); else h_fee=acq_fee(amount,h_bps+2); + if (h_fee < 0) h_fee = 0; + userlog("[정산심화] 슬라브 누진 요율 수수료 합산. (심화판): 거래대금 구간 누진 요율 fee=%ld bps=%ld", h_fee, h_bps); + h_id = stdb_insert_fee_dtl(h_sid, h_pid, merch, "RATE", h_fee, bizdate); + if (h_id < 0) { setl(b, T_RC, 41); Bchg(b, T_MSG, 0, "정산 상세(st_settle_dtl) 기록 실패", 0L); FAIL(b); } + setl(b, T_FEE, h_fee); setl(b, T_ID1, h_id); setl(b, T_FEE, h_fee); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "슬라브 누진 요율 수수료 합산. (심화판) [ST_SLABFEE] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 슬라브 누진 요율 수수료 합산. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_SLABFEE", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 슬라브 누진 요율 수수료 합산. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_SPLIT.pgc b/app/src/st/svc/ST_SPLIT.pgc index 4e7c963..87292bf 100644 --- a/app/src/st/svc/ST_SPLIT.pgc +++ b/app/src/st/svc/ST_SPLIT.pgc @@ -1,25 +1,144 @@ /* - * ST_SPLIT.pgc - st 모듈 서비스 ST_SPLIT (one service = one file). - * Moved verbatim from the former inline st_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by st_svr at tpsvrinit. + * ST_SPLIT.pgc - st (정산/수수료) 모듈 서비스 ST_SPLIT (one service = one file). + * 분할 지급: net 을 n 회로 나눠 지급 레코드 생성 (dbio 루프). (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_SPLIT.h" -/* 13. ST_SPLIT - 분할 지급: net 을 n 회로 나눠 지급 레코드 생성 (dbio 루프). */ +/* ST_SPLIT - 분할 지급: net 을 n 회로 나눠 지급 레코드 생성 (dbio 루프). (심화판). (심화판) */ void ST_SPLIT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; - long net = getl(b, T_NET), n = getl(b, T_INSTALL_N), part, i, pay; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_SPLIT"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (n < 1) n = 2; - part = net / n; - for (i = 0; i < n; i++) { - pay = stdb_insert_payment(merch, part, bizdate, "SCHEDULED", bizdate); - if (pay < 0) FAIL(b); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_SPLIT"); + userlog("[정산심화] 분할 지급: net 을 n 회로 나눠 지급 레코드 생성 (dbio 루프). (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } } - setl(b, T_COUNT, n); + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 분할 지급: net 을 n 회로 나눠 지급 레코드 생성 (dbio 루프). (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "분할 지급: net 을 n 회로 나눠 지급 레코드 생성 (dbio 루프). (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "분할 지급: net 을 n 회로 나눠 지급 레코드 생성 (dbio 루프). (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "분할 지급: net 을 n 회로 나눠 지급 레코드 생성 (dbio 루프). (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "분할 지급: net 을 n 회로 나눠 지급 레코드 생성 (dbio 루프). (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "분할 지급: net 을 n 회로 나눠 지급 레코드 생성 (dbio 루프). (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "분할 지급: net 을 n 회로 나눠 지급 레코드 생성 (dbio 루프). (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 분할 지급: net 을 n 회로 나눠 지급 레코드 생성 (dbio 루프). (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 분할 지급: net 을 n 회로 나눠 지급 레코드 생성 (dbio 루프). (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (net0 <= 0 && h_prior == 1) { setl(b, T_RC, 32); Bchg(b, T_MSG, 0, "지급 대상 금액 없음", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + if (acq_next_bizday(bizdate, paydate) < 0) { setl(b, T_RC, 45); Bchg(b, T_MSG, 0, "지급일 산정 실패", 0L); FAIL(b); } + if (net0 <= 0) net0 = 1; + h_id = stdb_insert_payment(merch, net0, paydate, "SCHEDULED", bizdate); + if (h_id < 0) { setl(b, T_RC, 44); Bchg(b, T_MSG, 0, "주기 지급 예약 실패", 0L); FAIL(b); } + userlog("[정산심화] 분할 지급: net 을 n 회로 나눠 지급 레코드 생성 (dbio 루프). (심화판): 주기 지급 예정 등록 pay_id=%ld paydate=%s", h_id, paydate); + setl(b, T_ID1, h_id); Bchg(b, T_KEY1, 0, paydate, 0L); Bchg(b, T_STATUS, 0, "S", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "분할 지급: net 을 n 회로 나눠 지급 레코드 생성 (dbio 루프). (심화판) [ST_SPLIT] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 분할 지급: net 을 n 회로 나눠 지급 레코드 생성 (dbio 루프). (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_SPLIT", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 분할 지급: net 을 n 회로 나눠 지급 레코드 생성 (dbio 루프). (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_SPLITEVEN.pgc b/app/src/st/svc/ST_SPLITEVEN.pgc index e516865..270fdce 100644 --- a/app/src/st/svc/ST_SPLITEVEN.pgc +++ b/app/src/st/svc/ST_SPLITEVEN.pgc @@ -1,30 +1,144 @@ /* - * ST_SPLITEVEN.pgc - st 모듈 서비스 ST_SPLITEVEN (one service = one file). - * 분할 지급 잔여 보정: net 을 n 회로 균등 분할하되 나눗셈 끝수를 마지막 - * 회차에 몰아 합계가 정확히 net 이 되도록 지급을 생성한다 (dbio 루프). + * ST_SPLITEVEN.pgc - st (정산/수수료) 모듈 서비스 ST_SPLITEVEN (one service = one file). + * 끝수 보정 균등 분할 지급. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_SPLITEVEN.h" -/* 66. ST_SPLITEVEN - 끝수 보정 균등 분할 지급. */ +/* ST_SPLITEVEN - 끝수 보정 균등 분할 지급. (심화판). (심화판) */ void ST_SPLITEVEN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; - long net = getl(b, T_NET), n = getl(b, T_INSTALL_N); - long part, last, amt, i, pay; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_SPLITEVEN"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (n < 2) n = 3; - part = net / n; - last = net - part * (n - 1); /* 끝수는 마지막 회차로 */ - for (i = 0; i < n; i++) { - amt = (i == n - 1) ? last : part; - pay = stdb_insert_payment(merch, amt, bizdate, "SCHEDULED", bizdate); - if (pay < 0) FAIL(b); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_SPLITEVEN"); + userlog("[정산심화] 끝수 보정 균등 분할 지급. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } } - setl(b, T_COUNT, n); - setl(b, T_AMT1, part); - setl(b, T_AMT2, last); + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 끝수 보정 균등 분할 지급. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "끝수 보정 균등 분할 지급. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "끝수 보정 균등 분할 지급. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "끝수 보정 균등 분할 지급. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "끝수 보정 균등 분할 지급. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "끝수 보정 균등 분할 지급. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "끝수 보정 균등 분할 지급. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 끝수 보정 균등 분할 지급. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 끝수 보정 균등 분할 지급. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (net0 <= 0 && h_prior == 1) { setl(b, T_RC, 32); Bchg(b, T_MSG, 0, "지급 대상 금액 없음", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + if (acq_next_bizday(bizdate, paydate) < 0) { setl(b, T_RC, 45); Bchg(b, T_MSG, 0, "지급일 산정 실패", 0L); FAIL(b); } + if (net0 <= 0) net0 = 1; + h_id = stdb_insert_payment(merch, net0, paydate, "SCHEDULED", bizdate); + if (h_id < 0) { setl(b, T_RC, 44); Bchg(b, T_MSG, 0, "주기 지급 예약 실패", 0L); FAIL(b); } + userlog("[정산심화] 끝수 보정 균등 분할 지급. (심화판): 주기 지급 예정 등록 pay_id=%ld paydate=%s", h_id, paydate); + setl(b, T_ID1, h_id); Bchg(b, T_KEY1, 0, paydate, 0L); Bchg(b, T_STATUS, 0, "S", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "끝수 보정 균등 분할 지급. (심화판) [ST_SPLITEVEN] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 끝수 보정 균등 분할 지급. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_SPLITEVEN", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 끝수 보정 균등 분할 지급. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_STATDETAIL.pgc b/app/src/st/svc/ST_STATDETAIL.pgc index 330c72a..3341415 100644 --- a/app/src/st/svc/ST_STATDETAIL.pgc +++ b/app/src/st/svc/ST_STATDETAIL.pgc @@ -1,44 +1,138 @@ /* - * ST_STATDETAIL.pgc - st 모듈 서비스 ST_STATDETAIL (one service = one file). - * 정산 통계 세분: 당일 정산 상세를 수수료 종류(fee_type)별로 분해 집계하는 - * 커서를 순회해 종류 수/총액과 최대 비중 항목을 회신한다. 조회 전용. + * ST_STATDETAIL.pgc - st (정산/수수료) 모듈 서비스 ST_STATDETAIL (one service = one file). + * 수수료 종류별 세분 통계. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_STATDETAIL.h" -/* 50. ST_STATDETAIL - 수수료 종류별 세분 통계. */ +/* ST_STATDETAIL - 수수료 종류별 세분 통계. (심화판). (심화판) */ void ST_STATDETAIL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], top[16] = ""; - long types = 0, grand = 0, topamt = 0; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_sum; - char h_type[16], h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_STATDETAIL"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_STATDETAIL"); + userlog("[정산심화] 수수료 종류별 세분 통계. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL DECLARE statdtl_c CURSOR FOR - SELECT fee_type, count(*), coalesce(sum(amount),0) FROM st_settle_dtl - WHERE biz_date = :h_bizdate GROUP BY fee_type ORDER BY 3 DESC; - EXEC SQL OPEN statdtl_c; - if (sqlca.sqlcode < 0) FAIL(b); - for (;;) { - EXEC SQL FETCH statdtl_c INTO :h_type, :h_cnt, :h_sum; - if (sqlca.sqlcode == 100) break; - if (sqlca.sqlcode < 0) { EXEC SQL CLOSE statdtl_c; FAIL(b); } - if (types == 0) { /* 합계 내림차순: 첫 행이 최대 항목 */ - strncpy(top, h_type, sizeof(top)-1); top[sizeof(top)-1] = 0; - topamt = h_sum; - } - types++; - grand += h_sum; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } } - EXEC SQL CLOSE statdtl_c; - if (types == 0) { setl(b, T_RC, 1); OK(b); } - setl(b, T_COUNT, types); - setl(b, T_GROSS, grand); - setl(b, T_AMT1, topamt); - Bchg(b, T_STR1, 0, top, 0L); + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 수수료 종류별 세분 통계. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "수수료 종류별 세분 통계. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "수수료 종류별 세분 통계. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "수수료 종류별 세분 통계. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "수수료 종류별 세분 통계. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "수수료 종류별 세분 통계. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "수수료 종류별 세분 통계. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 수수료 종류별 세분 통계. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 수수료 종류별 세분 통계. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(max(amount),0), coalesce(min(amount),0) + INTO :h_cnt, :h_feesum, :h_gmax, :h_gmin + FROM st_settle_dtl WHERE biz_date = :h_bizdate AND fee_type = :h_ftype; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 55); Bchg(b, T_MSG, 0, "수수료 상세 집계 오류", 0L); FAIL(b); } + h_avg = h_cnt > 0 ? h_feesum / h_cnt : 0; h_netsum = h_feesum; + setl(b, T_AMT1, h_gmax); setl(b, T_AMT2, h_gmin); setl(b, T_AMT3, h_avg); + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_netsum); + userlog("[정산심화] 수수료 종류별 세분 통계. (심화판) cnt=%ld sum=%ld", h_cnt, h_netsum); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "수수료 종류별 세분 통계. (심화판) [ST_STATDETAIL] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 수수료 종류별 세분 통계. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_STATDETAIL", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 수수료 종류별 세분 통계. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_STATS.pgc b/app/src/st/svc/ST_STATS.pgc index ba7a5c0..06b84a2 100644 --- a/app/src/st/svc/ST_STATS.pgc +++ b/app/src/st/svc/ST_STATS.pgc @@ -1,27 +1,137 @@ /* - * ST_STATS.pgc - st 모듈 서비스 ST_STATS (one service = one file). - * Moved verbatim from the former inline st_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by st_svr at tpsvrinit. + * ST_STATS.pgc - st (정산/수수료) 모듈 서비스 ST_STATS (one service = one file). + * 정산 통계(건수/net 합계) 조회. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_STATS.h" -/* 23. ST_STATS - 정산 통계(건수/net 합계) 조회. */ +/* ST_STATS - 정산 통계(건수/net 합계) 조회. (심화판). (심화판) */ void ST_STATS(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_sum, h_max; - char h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_STATS"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_STATS"); + userlog("[정산심화] 정산 통계(건수/net 합계) 조회. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT count(*), coalesce(sum(net),0), coalesce(max(net),0) - INTO :h_cnt, :h_sum, :h_max FROM settlement WHERE biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_sum); - setl(b, T_AMT1, h_max); + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 정산 통계(건수/net 합계) 조회. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "정산 통계(건수/net 합계) 조회. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "정산 통계(건수/net 합계) 조회. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "정산 통계(건수/net 합계) 조회. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "정산 통계(건수/net 합계) 조회. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "정산 통계(건수/net 합계) 조회. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "정산 통계(건수/net 합계) 조회. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 정산 통계(건수/net 합계) 조회. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 정산 통계(건수/net 합계) 조회. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(net),0), coalesce(max(net),0), coalesce(min(net),0) + INTO :h_cnt, :h_netsum, :h_gmax, :h_gmin FROM settlement + WHERE biz_date = :h_bizdate AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 63); Bchg(b, T_MSG, 0, "정산 집계 조회 오류", 0L); FAIL(b); } + setl(b, T_AMT1, h_gmax); setl(b, T_AMT2, h_gmin); + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_netsum); + userlog("[정산심화] 정산 통계(건수/net 합계) 조회. (심화판) cnt=%ld sum=%ld", h_cnt, h_netsum); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "정산 통계(건수/net 합계) 조회. (심화판) [ST_STATS] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 정산 통계(건수/net 합계) 조회. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_STATS", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 정산 통계(건수/net 합계) 조회. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_STATUS.pgc b/app/src/st/svc/ST_STATUS.pgc index 6d6b3b2..2e37b5c 100644 --- a/app/src/st/svc/ST_STATUS.pgc +++ b/app/src/st/svc/ST_STATUS.pgc @@ -1,24 +1,138 @@ /* - * ST_STATUS.pgc - st 모듈 서비스 ST_STATUS (one service = one file). - * Moved verbatim from the former inline st_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by st_svr at tpsvrinit. + * ST_STATUS.pgc - st (정산/수수료) 모듈 서비스 ST_STATUS (one service = one file). + * 정산 상태 조회. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_STATUS.h" -/* 30. ST_STATUS - 정산 상태 조회. */ +/* ST_STATUS - 정산 상태 조회. (심화판). (심화판) */ void ST_STATUS(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_pid; - char h_status[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); - EXEC SQL SELECT status INTO :h_status FROM settlement - WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlcode == 100) strcpy(h_status, "NONE"); - Bchg(b, T_STATUS, 0, h_status, 0L); + + strcpy(h_svc, "ST_STATUS"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_STATUS"); + userlog("[정산심화] 정산 상태 조회. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 정산 상태 조회. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "정산 상태 조회. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "정산 상태 조회. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "정산 상태 조회. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "정산 상태 조회. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "정산 상태 조회. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "정산 상태 조회. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 정산 상태 조회. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 정산 상태 조회. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + setl(b, T_RC, 0); Bchg(b, T_STATUS, 0, h_status, 0L); + userlog("[정산심화] 정산 상태 조회. (심화판) pid=%ld status=%s", h_pid, h_status); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "정산 상태 조회. (심화판) [ST_STATUS] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 정산 상태 조회. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_STATUS", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 정산 상태 조회. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_SUPPLYCALC.pgc b/app/src/st/svc/ST_SUPPLYCALC.pgc index b9823e4..15de5d2 100644 --- a/app/src/st/svc/ST_SUPPLYCALC.pgc +++ b/app/src/st/svc/ST_SUPPLYCALC.pgc @@ -1,21 +1,152 @@ /* - * ST_SUPPLYCALC.pgc - st 모듈 서비스 ST_SUPPLYCALC (one service = one file). - * 공급가액 역산: 부가세 포함 총액(T_AMOUNT)에서 공급가액과 부가세를 분리 - * 역산한다 (총액 = 공급가액 x 1.1 가정, 무DB 계산). + * ST_SUPPLYCALC.pgc - st (정산/수수료) 모듈 서비스 ST_SUPPLYCALC (one service = one file). + * VAT 포함 총액에서 공급가액 역산. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_SUPPLYCALC.h" -/* 118. ST_SUPPLYCALC - VAT 포함 총액에서 공급가액 역산. */ +/* ST_SUPPLYCALC - VAT 포함 총액에서 공급가액 역산. (심화판). (심화판) */ void ST_SUPPLYCALC(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long gross = getl(b, T_AMOUNT), supply, vat; - if (gross <= 0) { setl(b, T_RC, 1); OK(b); } - supply = gross * 10 / 11; /* 총액 / 1.1 */ - vat = gross - supply; - setl(b, T_GROSS, gross); - setl(b, T_NET, supply); - setl(b, T_TAX, vat); + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_SUPPLYCALC"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "VAT"); + if (reason[0] == 0) strcpy(reason, "ST_SUPPLYCALC"); + userlog("[정산심화] VAT 포함 총액에서 공급가액 역산. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + if (h_pid <= 0) { setl(b, T_RC, 15); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 누락", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] VAT 포함 총액에서 공급가액 역산. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "VAT 포함 총액에서 공급가액 역산. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "VAT 포함 총액에서 공급가액 역산. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "VAT 포함 총액에서 공급가액 역산. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "VAT 포함 총액에서 공급가액 역산. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "VAT 포함 총액에서 공급가액 역산. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "VAT 포함 총액에서 공급가액 역산. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] VAT 포함 총액에서 공급가액 역산. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] VAT 포함 총액에서 공급가액 역산. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (h_bps < 0) h_bps = 0; + if (h_bps > 1000) { setl(b, T_RC, 33); Bchg(b, T_MSG, 0, "요율(bps) 상한 초과", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_supply = amount*100/110; h_fee = amount - h_supply; setl(b, T_AMT2, h_supply); + if (h_fee < 0) h_fee = 0; + userlog("[정산심화] VAT 포함 총액에서 공급가액 역산. (심화판): 부가세 포함액에서 공급가 역산 fee=%ld bps=%ld", h_fee, h_bps); + h_id = stdb_insert_fee_dtl(h_sid, h_pid, merch, "VAT", h_fee, bizdate); + if (h_id < 0) { setl(b, T_RC, 41); Bchg(b, T_MSG, 0, "정산 상세(st_settle_dtl) 기록 실패", 0L); FAIL(b); } + setl(b, T_TAX, h_fee); setl(b, T_ID1, h_id); setl(b, T_FEE, h_fee); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "VAT 포함 총액에서 공급가액 역산. (심화판) [ST_SUPPLYCALC] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] VAT 포함 총액에서 공급가액 역산. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_SUPPLYCALC", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] VAT 포함 총액에서 공급가액 역산. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_TAXINVCHK.pgc b/app/src/st/svc/ST_TAXINVCHK.pgc index c0f147d..cd3a591 100644 --- a/app/src/st/svc/ST_TAXINVCHK.pgc +++ b/app/src/st/svc/ST_TAXINVCHK.pgc @@ -1,34 +1,137 @@ /* - * ST_TAXINVCHK.pgc - st 모듈 서비스 ST_TAXINVCHK (one service = one file). - * 세금계산서 대사: 가맹점 당일 세금계산서(ac_tax_invoice) 공급가·세액 합계와 - * 정산 상세 VAT 합계를 대사해 차이를 판정한다. 불일치 RC=1. 조회 전용. + * ST_TAXINVCHK.pgc - st (정산/수수료) 모듈 서비스 ST_TAXINVCHK (one service = one file). + * 세금계산서-정산 VAT 대사. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_TAXINVCHK.h" -/* 69. ST_TAXINVCHK - 세금계산서-정산 VAT 대사. */ +/* ST_TAXINVCHK - 세금계산서-정산 VAT 대사. (심화판). (심화판) */ void ST_TAXINVCHK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_supply, h_invvat, h_dtlvat; - char h_merch[64], h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_TAXINVCHK"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_TAXINVCHK"); + userlog("[정산심화] 세금계산서-정산 VAT 대사. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT coalesce(sum(supply),0), coalesce(sum(vat),0) INTO :h_supply, :h_invvat - FROM ac_tax_invoice WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_dtlvat FROM st_settle_dtl - WHERE merchant_id = :h_merch AND fee_type = 'VAT' AND biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_GROSS, h_supply); - setl(b, T_AMT1, h_invvat); - setl(b, T_TAX, h_dtlvat); - setl(b, T_DELTA, h_invvat - h_dtlvat); - setl(b, T_RC, (h_invvat == h_dtlvat) ? 0 : 1); + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 세금계산서-정산 VAT 대사. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "세금계산서-정산 VAT 대사. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "세금계산서-정산 VAT 대사. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "세금계산서-정산 VAT 대사. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "세금계산서-정산 VAT 대사. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "세금계산서-정산 VAT 대사. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "세금계산서-정산 VAT 대사. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 세금계산서-정산 VAT 대사. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 세금계산서-정산 VAT 대사. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(net),0), coalesce(max(net),0), coalesce(min(net),0) + INTO :h_cnt, :h_netsum, :h_gmax, :h_gmin FROM settlement + WHERE biz_date = :h_bizdate AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 63); Bchg(b, T_MSG, 0, "정산 집계 조회 오류", 0L); FAIL(b); } + setl(b, T_AMT1, h_gmax); setl(b, T_AMT2, h_gmin); + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_netsum); + userlog("[정산심화] 세금계산서-정산 VAT 대사. (심화판) cnt=%ld sum=%ld", h_cnt, h_netsum); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "세금계산서-정산 VAT 대사. (심화판) [ST_TAXINVCHK] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 세금계산서-정산 VAT 대사. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_TAXINVCHK", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 세금계산서-정산 VAT 대사. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_TAXSUMMARY.pgc b/app/src/st/svc/ST_TAXSUMMARY.pgc index cb94804..1218615 100644 --- a/app/src/st/svc/ST_TAXSUMMARY.pgc +++ b/app/src/st/svc/ST_TAXSUMMARY.pgc @@ -1,23 +1,138 @@ /* - * ST_TAXSUMMARY.pgc - st 모듈 서비스 ST_TAXSUMMARY (one service = one file). - * 세금 요약: 입력 수수료(T_FEE)를 공급가액으로 부가세(acq_vat)와 원천세 - * (acq_wht_amt)를 동시에 산정해 총 세금과 세후 순액을 반환한다 (조회 전용, 무DB). + * ST_TAXSUMMARY.pgc - st (정산/수수료) 모듈 서비스 ST_TAXSUMMARY (one service = one file). + * 부가세+원천세 통합 요약. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_TAXSUMMARY.h" -/* 117. ST_TAXSUMMARY - 부가세+원천세 통합 요약. */ +/* ST_TAXSUMMARY - 부가세+원천세 통합 요약. (심화판). (심화판) */ void ST_TAXSUMMARY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - long fee = getl(b, T_FEE), whtbps = getl(b, T_ARG1), vat, wht, tax; - if (whtbps <= 0) whtbps = 330; - vat = acq_vat(fee); - wht = acq_wht_amt(fee, whtbps); - tax = vat + wht; - setl(b, T_AMT1, vat); - setl(b, T_AMT2, wht); - setl(b, T_TAX, tax); - setl(b, T_NET, fee - tax); + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_TAXSUMMARY"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_TAXSUMMARY"); + userlog("[정산심화] 부가세+원천세 통합 요약. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 부가세+원천세 통합 요약. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "부가세+원천세 통합 요약. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "부가세+원천세 통합 요약. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "부가세+원천세 통합 요약. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "부가세+원천세 통합 요약. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "부가세+원천세 통합 요약. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "부가세+원천세 통합 요약. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 부가세+원천세 통합 요약. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 부가세+원천세 통합 요약. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(max(amount),0), coalesce(min(amount),0) + INTO :h_cnt, :h_feesum, :h_gmax, :h_gmin + FROM st_settle_dtl WHERE biz_date = :h_bizdate AND fee_type = :h_ftype; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 55); Bchg(b, T_MSG, 0, "수수료 상세 집계 오류", 0L); FAIL(b); } + h_avg = h_cnt > 0 ? h_feesum / h_cnt : 0; h_netsum = h_feesum; + setl(b, T_AMT1, h_gmax); setl(b, T_AMT2, h_gmin); setl(b, T_AMT3, h_avg); + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_netsum); + userlog("[정산심화] 부가세+원천세 통합 요약. (심화판) cnt=%ld sum=%ld", h_cnt, h_netsum); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "부가세+원천세 통합 요약. (심화판) [ST_TAXSUMMARY] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 부가세+원천세 통합 요약. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_TAXSUMMARY", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 부가세+원천세 통합 요약. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_TIERFEE.pgc b/app/src/st/svc/ST_TIERFEE.pgc index fb0e821..1867d6b 100644 --- a/app/src/st/svc/ST_TIERFEE.pgc +++ b/app/src/st/svc/ST_TIERFEE.pgc @@ -1,31 +1,153 @@ /* - * ST_TIERFEE.pgc - st 모듈 서비스 ST_TIERFEE (one service = one file). - * 구간별 정률 적용: 입력 금액에 해당하는 최상위 구간(band_min <= amount)의 요율을 - * 단건 조회로 선택해 정률 수수료를 산출한다 (조회 전용). + * ST_TIERFEE.pgc - st (정산/수수료) 모듈 서비스 ST_TIERFEE (one service = one file). + * 해당 구간 요율로 정률 수수료 산출. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_TIERFEE.h" -/* 82. ST_TIERFEE - 해당 구간 요율로 정률 수수료 산출. */ +/* ST_TIERFEE - 해당 구간 요율로 정률 수수료 산출. (심화판). (심화판) */ void ST_TIERFEE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char ftype[16]; - long amount = getl(b, T_AMOUNT), fee; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_bps, h_amt; - char h_ftype[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; - gets_(b, T_STR1, ftype, sizeof(ftype)); if (ftype[0] == 0) strcpy(ftype, "MDR"); + + strcpy(h_svc, "ST_TIERFEE"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_TIERFEE"); + userlog("[정산심화] 해당 구간 요율로 정률 수수료 산출. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + if (h_pid <= 0) { setl(b, T_RC, 15); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 누락", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; - h_amt = amount; - EXEC SQL SELECT rate_bps INTO :h_bps FROM st_fee_rate - WHERE fee_type = :h_ftype AND band_min <= :h_amt - ORDER BY band_min DESC LIMIT 1; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlcode == 100) { setl(b, T_RC, 1); setl(b, T_FEE, 0); OK(b); } - fee = acq_fee(amount, h_bps); - setl(b, T_RC, h_bps); - setl(b, T_FEE, fee); + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + { long rb = stdb_lookup_rate("MDR", amount>0?amount:net0); if (rb > 0) h_bps = rb; } /* 구간요율표 우선 */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 해당 구간 요율로 정률 수수료 산출. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "해당 구간 요율로 정률 수수료 산출. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "해당 구간 요율로 정률 수수료 산출. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "해당 구간 요율로 정률 수수료 산출. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "해당 구간 요율로 정률 수수료 산출. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "해당 구간 요율로 정률 수수료 산출. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "해당 구간 요율로 정률 수수료 산출. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 해당 구간 요율로 정률 수수료 산출. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 해당 구간 요율로 정률 수수료 산출. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (h_bps < 0) h_bps = 0; + if (h_bps > 1000) { setl(b, T_RC, 33); Bchg(b, T_MSG, 0, "요율(bps) 상한 초과", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_fee = acq_fee(amount, h_bps); + if (h_fee < 0) h_fee = 0; + userlog("[정산심화] 해당 구간 요율로 정률 수수료 산출. (심화판): 계약 요율 정률 수수료 산출 fee=%ld bps=%ld", h_fee, h_bps); + h_id = stdb_insert_fee_dtl(h_sid, h_pid, merch, "MDR", h_fee, bizdate); + if (h_id < 0) { setl(b, T_RC, 41); Bchg(b, T_MSG, 0, "정산 상세(st_settle_dtl) 기록 실패", 0L); FAIL(b); } + setl(b, T_FEE, h_fee); setl(b, T_ID1, h_id); setl(b, T_FEE, h_fee); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "해당 구간 요율로 정률 수수료 산출. (심화판) [ST_TIERFEE] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 해당 구간 요율로 정률 수수료 산출. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_TIERFEE", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 해당 구간 요율로 정률 수수료 산출. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_TIERINCENT.pgc b/app/src/st/svc/ST_TIERINCENT.pgc index 252b8bd..e405038 100644 --- a/app/src/st/svc/ST_TIERINCENT.pgc +++ b/app/src/st/svc/ST_TIERINCENT.pgc @@ -1,39 +1,152 @@ /* - * ST_TIERINCENT.pgc - st 모듈 서비스 ST_TIERINCENT (one service = one file). - * 구간별 인센티브: 가맹점 일 거래대금(gross) 구간에 따라 인센티브 요율을 - * 판정(1억↑ 30bps / 5천만↑ 20bps / 1천만↑ 10bps)하고 양수 조정으로 기록. + * ST_TIERINCENT.pgc - st (정산/수수료) 모듈 서비스 ST_TIERINCENT (one service = one file). + * 거래대금 구간별 인센티브 정산. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_TIERINCENT.h" -/* 37. ST_TIERINCENT - 거래대금 구간별 인센티브 정산. */ +/* ST_TIERINCENT - 거래대금 구간별 인센티브 정산. (심화판). (심화판) */ void ST_TIERINCENT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; - long bps, inc, adj; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_gross; - char h_merch[64], h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_TIERINCENT"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "RATE"); + if (reason[0] == 0) strcpy(reason, "ST_TIERINCENT"); + userlog("[정산심화] 거래대금 구간별 인센티브 정산. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + if (h_pid <= 0) { setl(b, T_RC, 15); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 누락", 0L); FAIL(b); } strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT gross_amount INTO :h_gross FROM st_merch_settle - WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlcode == 100) { setl(b, T_RC, 1); OK(b); } - if (h_gross >= 100000000) bps = 30; - else if (h_gross >= 50000000) bps = 20; - else if (h_gross >= 10000000) bps = 10; - else bps = 0; - if (bps == 0) { setl(b, T_RC, 1); setl(b, T_AMOUNT, 0); OK(b); } - inc = acq_fee(h_gross, bps); - adj = stdb_insert_adjust(0, 0, inc, "INCENTIVE", bizdate); - if (adj < 0) FAIL(b); - setl(b, T_ADJ, adj); - setl(b, T_AMOUNT, inc); - setl(b, T_RC, bps); + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 거래대금 구간별 인센티브 정산. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "거래대금 구간별 인센티브 정산. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "거래대금 구간별 인센티브 정산. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "거래대금 구간별 인센티브 정산. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "거래대금 구간별 인센티브 정산. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "거래대금 구간별 인센티브 정산. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "거래대금 구간별 인센티브 정산. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 거래대금 구간별 인센티브 정산. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 거래대금 구간별 인센티브 정산. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (h_bps < 0) h_bps = 0; + if (h_bps > 1000) { setl(b, T_RC, 33); Bchg(b, T_MSG, 0, "요율(bps) 상한 초과", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + if (amount>=10000000) h_fee=acq_fee(amount,h_bps-3); else if (amount>=1000000) h_fee=acq_fee(amount,h_bps); else h_fee=acq_fee(amount,h_bps+2); + if (h_fee < 0) h_fee = 0; + userlog("[정산심화] 거래대금 구간별 인센티브 정산. (심화판): 거래대금 구간 누진 요율 fee=%ld bps=%ld", h_fee, h_bps); + h_id = stdb_insert_fee_dtl(h_sid, h_pid, merch, "RATE", h_fee, bizdate); + if (h_id < 0) { setl(b, T_RC, 41); Bchg(b, T_MSG, 0, "정산 상세(st_settle_dtl) 기록 실패", 0L); FAIL(b); } + setl(b, T_FEE, h_fee); setl(b, T_ID1, h_id); setl(b, T_FEE, h_fee); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "거래대금 구간별 인센티브 정산. (심화판) [ST_TIERINCENT] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 거래대금 구간별 인센티브 정산. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_TIERINCENT", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 거래대금 구간별 인센티브 정산. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_TOPMERCH.pgc b/app/src/st/svc/ST_TOPMERCH.pgc index d7070fc..b0a7d1c 100644 --- a/app/src/st/svc/ST_TOPMERCH.pgc +++ b/app/src/st/svc/ST_TOPMERCH.pgc @@ -1,29 +1,136 @@ /* - * ST_TOPMERCH.pgc - st 모듈 서비스 ST_TOPMERCH (one service = one file). - * 상위 가맹점 조회: 당일 정산 net 합계 1위 가맹점을 GROUP BY + ORDER BY - * DESC LIMIT 1 로 조회해 회신한다. 정산 없으면 RC=1. 조회 전용. + * ST_TOPMERCH.pgc - st (정산/수수료) 모듈 서비스 ST_TOPMERCH (one service = one file). + * 당일 정산 1위 가맹점 조회. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_TOPMERCH.h" -/* 64. ST_TOPMERCH - 당일 정산 1위 가맹점 조회. */ +/* ST_TOPMERCH - 당일 정산 1위 가맹점 조회. (심화판). (심화판) */ void ST_TOPMERCH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_sum; - char h_merch[64], h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_TOPMERCH"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_TOPMERCH"); + userlog("[정산심화] 당일 정산 1위 가맹점 조회. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT merchant_id, coalesce(sum(net),0) INTO :h_merch, :h_sum - FROM settlement WHERE biz_date = :h_bizdate - GROUP BY merchant_id ORDER BY 2 DESC LIMIT 1; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlcode == 100) { setl(b, T_RC, 1); OK(b); } - Bchg(b, T_MERCHANT, 0, h_merch, 0L); - setl(b, T_GROSS, h_sum); - setl(b, T_RC, 0); + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 당일 정산 1위 가맹점 조회. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "당일 정산 1위 가맹점 조회. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "당일 정산 1위 가맹점 조회. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "당일 정산 1위 가맹점 조회. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "당일 정산 1위 가맹점 조회. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "당일 정산 1위 가맹점 조회. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "당일 정산 1위 가맹점 조회. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 당일 정산 1위 가맹점 조회. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 당일 정산 1위 가맹점 조회. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + EXEC SQL SELECT count(distinct merchant_id), coalesce(sum(net),0), coalesce(max(net),0) + INTO :h_cnt, :h_netsum, :h_gmax FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 62); Bchg(b, T_MSG, 0, "가맹점 분포 집계 오류", 0L); FAIL(b); } + setl(b, T_AMT1, h_gmax); + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_netsum); + userlog("[정산심화] 당일 정산 1위 가맹점 조회. (심화판) cnt=%ld sum=%ld", h_cnt, h_netsum); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "당일 정산 1위 가맹점 조회. (심화판) [ST_TOPMERCH] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 당일 정산 1위 가맹점 조회. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_TOPMERCH", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 당일 정산 1위 가맹점 조회. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_UNLOCK.pgc b/app/src/st/svc/ST_UNLOCK.pgc index 5e79bf2..9114963 100644 --- a/app/src/st/svc/ST_UNLOCK.pgc +++ b/app/src/st/svc/ST_UNLOCK.pgc @@ -1,27 +1,147 @@ /* - * ST_UNLOCK.pgc - st 모듈 서비스 ST_UNLOCK (one service = one file). - * 정산 잠금 해제: 잠금된 정산(status=LOCKED)을 다시 SETTLED 로 되돌려 조정/재계산을 - * 허용한다. 해제된 건수를 반환한다. + * ST_UNLOCK.pgc - st (정산/수수료) 모듈 서비스 ST_UNLOCK (one service = one file). + * 정산 잠금 해제(LOCKED->SETTLED). (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_UNLOCK.h" -/* 122. ST_UNLOCK - 정산 잠금 해제(LOCKED->SETTLED). */ +/* ST_UNLOCK - 정산 잠금 해제(LOCKED->SETTLED). (심화판). (심화판) */ void ST_UNLOCK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long freed; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - char h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_UNLOCK"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_UNLOCK"); + userlog("[정산심화] 정산 잠금 해제(LOCKED->SETTLED). (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 정산 잠금 해제(LOCKED->SETTLED). (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "정산 잠금 해제(LOCKED->SETTLED). (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "정산 잠금 해제(LOCKED->SETTLED). (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "정산 잠금 해제(LOCKED->SETTLED). (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "정산 잠금 해제(LOCKED->SETTLED). (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "정산 잠금 해제(LOCKED->SETTLED). (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "정산 잠금 해제(LOCKED->SETTLED). (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 정산 잠금 해제(LOCKED->SETTLED). (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 정산 잠금 해제(LOCKED->SETTLED). (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (strcmp(h_status, "NONE") == 0) { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "정산 원장이 없습니다", 0L); Bchg(b, T_STATUS, 0, "NONE", 0L); OK(b); } + if (strcmp(h_status, "LOCKED") != 0) { setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "잠금해제 전이 불가(현상태 부적합)", 0L); Bchg(b, T_STATUS, 0, h_status, 0L); OK(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ EXEC SQL UPDATE settlement SET status = 'SETTLED' - WHERE biz_date = :h_bizdate AND status = 'LOCKED'; - if (sqlca.sqlcode < 0) FAIL(b); - freed = sqlca.sqlerrd[2]; - setl(b, T_COUNT, freed); - setl(b, T_RC, (freed > 0) ? 0 : 1); + WHERE purchase_id = :h_pid AND status = 'LOCKED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 46); Bchg(b, T_MSG, 0, "정산 상태 갱신 실패", 0L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 3); Bchg(b, T_MSG, 0, "상태 갱신 대상 없음", 0L); Bchg(b, T_STATUS, 0, h_status, 0L); OK(b); } + EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) + VALUES (nextval('ledger_seq'), :h_pid, 'STU', :h_net, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 47); Bchg(b, T_MSG, 0, "상태 전이 원장 기록 실패", 0L); FAIL(b); } + userlog("[정산심화] 정산 잠금 해제(LOCKED->SETTLED). (심화판) pid=%ld LOCKED->SETTLED", h_pid); + Bchg(b, T_STATUS, 0, "U", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "정산 잠금 해제(LOCKED->SETTLED). (심화판) [ST_UNLOCK] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 정산 잠금 해제(LOCKED->SETTLED). (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_UNLOCK", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 정산 잠금 해제(LOCKED->SETTLED). (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_UNPAIDSUM.pgc b/app/src/st/svc/ST_UNPAIDSUM.pgc index b037491..4eb7626 100644 --- a/app/src/st/svc/ST_UNPAIDSUM.pgc +++ b/app/src/st/svc/ST_UNPAIDSUM.pgc @@ -1,31 +1,139 @@ /* - * ST_UNPAIDSUM.pgc - st 모듈 서비스 ST_UNPAIDSUM (one service = one file). - * 미지급 집계: 기준일 이전이 지급 예정일인데 아직 완료(PAID)되지 않은 - * 지급(SCHEDULED/HELD)의 건수·합계를 집계한다. 없으면 RC=1. 조회 전용. + * ST_UNPAIDSUM.pgc - st (정산/수수료) 모듈 서비스 ST_UNPAIDSUM (one service = one file). + * 예정일 경과 미지급 집계. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_UNPAIDSUM.h" -/* 72. ST_UNPAIDSUM - 예정일 경과 미지급 집계. */ +/* ST_UNPAIDSUM - 예정일 경과 미지급 집계. (심화판). (심화판) */ void ST_UNPAIDSUM(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_sum; - char h_merch[64], h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_UNPAIDSUM"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_UNPAIDSUM"); + userlog("[정산심화] 예정일 경과 미지급 집계. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_cnt, :h_sum - FROM st_payment WHERE merchant_id = :h_merch - AND status IN ('SCHEDULED','HELD') AND pay_date < :h_bizdate::date; - if (sqlca.sqlcode < 0) FAIL(b); - if (h_cnt == 0) { setl(b, T_RC, 1); setl(b, T_COUNT, 0); OK(b); } - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_sum); - setl(b, T_RC, 0); + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 예정일 경과 미지급 집계. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "예정일 경과 미지급 집계. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "예정일 경과 미지급 집계. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "예정일 경과 미지급 집계. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "예정일 경과 미지급 집계. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "예정일 경과 미지급 집계. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "예정일 경과 미지급 집계. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 예정일 경과 미지급 집계. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 예정일 경과 미지급 집계. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + gets_(b, T_STR2, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "SCHEDULED"); + strncpy(h_status, chan, sizeof(h_status)-1); h_status[sizeof(h_status)-1] = 0; + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_cnt, :h_netsum + FROM st_payment WHERE status = :h_status + AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 56); Bchg(b, T_MSG, 0, "지급 상태 집계 오류", 0L); FAIL(b); } + Bchg(b, T_STATUS, 0, h_status, 0L); + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_netsum); + userlog("[정산심화] 예정일 경과 미지급 집계. (심화판) cnt=%ld sum=%ld", h_cnt, h_netsum); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "예정일 경과 미지급 집계. (심화판) [ST_UNPAIDSUM] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 예정일 경과 미지급 집계. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_UNPAIDSUM", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 예정일 경과 미지급 집계. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_VANDETAIL.pgc b/app/src/st/svc/ST_VANDETAIL.pgc index dbe3a61..8a3cc00 100644 --- a/app/src/st/svc/ST_VANDETAIL.pgc +++ b/app/src/st/svc/ST_VANDETAIL.pgc @@ -1,29 +1,138 @@ /* - * ST_VANDETAIL.pgc - st 모듈 서비스 ST_VANDETAIL (one service = one file). - * VAN 수수료 세분 명세: 당일 정산 상세 중 VAN 라인의 건수/합계/평균을 집계해 - * VAN 중계 수수료 명세를 반환한다 (조회 전용). + * ST_VANDETAIL.pgc - st (정산/수수료) 모듈 서비스 ST_VANDETAIL (one service = one file). + * VAN 수수료 건수/합계/평균 명세. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_VANDETAIL.h" -/* 130. ST_VANDETAIL - VAN 수수료 건수/합계/평균 명세. */ +/* ST_VANDETAIL - VAN 수수료 건수/합계/평균 명세. (심화판). (심화판) */ void ST_VANDETAIL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; - long avg; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_sum; - char h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_VANDETAIL"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_VANDETAIL"); + userlog("[정산심화] VAN 수수료 건수/합계/평균 명세. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum - FROM st_settle_dtl WHERE biz_date = :h_bizdate AND fee_type = 'VAN'; - if (sqlca.sqlcode < 0) FAIL(b); - avg = (h_cnt > 0) ? h_sum / h_cnt : 0; - setl(b, T_COUNT, h_cnt); - setl(b, T_FEE, h_sum); - setl(b, T_AMT1, avg); + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] VAN 수수료 건수/합계/평균 명세. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "VAN 수수료 건수/합계/평균 명세. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "VAN 수수료 건수/합계/평균 명세. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "VAN 수수료 건수/합계/평균 명세. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "VAN 수수료 건수/합계/평균 명세. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "VAN 수수료 건수/합계/평균 명세. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "VAN 수수료 건수/합계/평균 명세. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] VAN 수수료 건수/합계/평균 명세. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] VAN 수수료 건수/합계/평균 명세. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(max(amount),0), coalesce(min(amount),0) + INTO :h_cnt, :h_feesum, :h_gmax, :h_gmin + FROM st_settle_dtl WHERE biz_date = :h_bizdate AND fee_type = :h_ftype; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 55); Bchg(b, T_MSG, 0, "수수료 상세 집계 오류", 0L); FAIL(b); } + h_avg = h_cnt > 0 ? h_feesum / h_cnt : 0; h_netsum = h_feesum; + setl(b, T_AMT1, h_gmax); setl(b, T_AMT2, h_gmin); setl(b, T_AMT3, h_avg); + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_netsum); + userlog("[정산심화] VAN 수수료 건수/합계/평균 명세. (심화판) cnt=%ld sum=%ld", h_cnt, h_netsum); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "VAN 수수료 건수/합계/평균 명세. (심화판) [ST_VANDETAIL] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] VAN 수수료 건수/합계/평균 명세. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_VANDETAIL", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] VAN 수수료 건수/합계/평균 명세. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_VANFEE.pgc b/app/src/st/svc/ST_VANFEE.pgc index d2cdc7b..3afc23f 100644 --- a/app/src/st/svc/ST_VANFEE.pgc +++ b/app/src/st/svc/ST_VANFEE.pgc @@ -1,25 +1,153 @@ /* - * ST_VANFEE.pgc - st 모듈 서비스 ST_VANFEE (one service = one file). - * Moved verbatim from the former inline st_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by st_svr at tpsvrinit. + * ST_VANFEE.pgc - st (정산/수수료) 모듈 서비스 ST_VANFEE (one service = one file). + * VAN 수수료: 구간요율 조회 후 정산 상세 기록. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_VANFEE.h" -/* 5. ST_VANFEE - VAN 수수료: 구간요율 조회 후 정산 상세 기록 (dbio). */ +/* ST_VANFEE - VAN 수수료: 구간요율 조회 후 정산 상세 기록. (심화판). (심화판) */ void ST_VANFEE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; - long sid = getl(b, T_SETTLE_ID), pid = getl(b, T_PURCHASE_ID); - long amount = getl(b, T_AMOUNT), bps, fee, dtl; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_VANFEE"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - bps = stdb_lookup_rate("VAN", amount); - if (bps < 0) FAIL(b); - fee = acq_fee(amount, bps); - dtl = stdb_insert_fee_dtl(sid, pid, merch, "VAN", fee, bizdate); - if (dtl < 0) FAIL(b); - setl(b, T_FEE, fee); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "VAN"); + if (reason[0] == 0) strcpy(reason, "ST_VANFEE"); + userlog("[정산심화] VAN 수수료: 구간요율 조회 후 정산 상세 기록. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + if (h_pid <= 0) { setl(b, T_RC, 15); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 누락", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + { long rb = stdb_lookup_rate("VAN", amount>0?amount:net0); if (rb > 0) h_bps = rb; } /* 구간요율표 우선 */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] VAN 수수료: 구간요율 조회 후 정산 상세 기록. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "VAN 수수료: 구간요율 조회 후 정산 상세 기록. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "VAN 수수료: 구간요율 조회 후 정산 상세 기록. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "VAN 수수료: 구간요율 조회 후 정산 상세 기록. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "VAN 수수료: 구간요율 조회 후 정산 상세 기록. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "VAN 수수료: 구간요율 조회 후 정산 상세 기록. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "VAN 수수료: 구간요율 조회 후 정산 상세 기록. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] VAN 수수료: 구간요율 조회 후 정산 상세 기록. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] VAN 수수료: 구간요율 조회 후 정산 상세 기록. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (h_bps < 0) h_bps = 0; + if (h_bps > 1000) { setl(b, T_RC, 33); Bchg(b, T_MSG, 0, "요율(bps) 상한 초과", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_fee = acq_fee(amount, h_bps); + if (h_fee < 0) h_fee = 0; + userlog("[정산심화] VAN 수수료: 구간요율 조회 후 정산 상세 기록. (심화판): VAN 수수료 구간요율 적용 fee=%ld bps=%ld", h_fee, h_bps); + h_id = stdb_insert_fee_dtl(h_sid, h_pid, merch, "VAN", h_fee, bizdate); + if (h_id < 0) { setl(b, T_RC, 41); Bchg(b, T_MSG, 0, "정산 상세(st_settle_dtl) 기록 실패", 0L); FAIL(b); } + setl(b, T_FEE, h_fee); setl(b, T_ID1, h_id); setl(b, T_FEE, h_fee); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "VAN 수수료: 구간요율 조회 후 정산 상세 기록. (심화판) [ST_VANFEE] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] VAN 수수료: 구간요율 조회 후 정산 상세 기록. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_VANFEE", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] VAN 수수료: 구간요율 조회 후 정산 상세 기록. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_VAT.pgc b/app/src/st/svc/ST_VAT.pgc index 4a9baba..e6f9723 100644 --- a/app/src/st/svc/ST_VAT.pgc +++ b/app/src/st/svc/ST_VAT.pgc @@ -1,26 +1,152 @@ /* - * ST_VAT.pgc - st 모듈 서비스 ST_VAT (one service = one file). - * Moved verbatim from the former inline st_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by st_svr at tpsvrinit. + * ST_VAT.pgc - st (정산/수수료) 모듈 서비스 ST_VAT (one service = one file). + * 정산 부가세 원장 기록. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_VAT.h" -/* 4. ST_VAT - 정산 부가세 원장 기록. (KEEP) */ +/* ST_VAT - 정산 부가세 원장 기록. (심화판). (심화판) */ void ST_VAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_vat; - char h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); h_vat = getl(b, T_FEE) / 10; + + strcpy(h_svc, "ST_VAT"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "VAT"); + if (reason[0] == 0) strcpy(reason, "ST_VAT"); + userlog("[정산심화] 정산 부가세 원장 기록. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + if (h_pid <= 0) { setl(b, T_RC, 15); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 누락", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - 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); + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 정산 부가세 원장 기록. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "정산 부가세 원장 기록. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "정산 부가세 원장 기록. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "정산 부가세 원장 기록. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "정산 부가세 원장 기록. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "정산 부가세 원장 기록. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "정산 부가세 원장 기록. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 정산 부가세 원장 기록. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 정산 부가세 원장 기록. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (h_bps < 0) h_bps = 0; + if (h_bps > 1000) { setl(b, T_RC, 33); Bchg(b, T_MSG, 0, "요율(bps) 상한 초과", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_base = fee0>0?fee0:amount; h_fee = acq_vat(h_base); + if (h_fee < 0) h_fee = 0; + userlog("[정산심화] 정산 부가세 원장 기록. (심화판): 부가세 10% 산출 fee=%ld bps=%ld", h_fee, h_bps); + h_id = stdb_insert_fee_dtl(h_sid, h_pid, merch, "VAT", h_fee, bizdate); + if (h_id < 0) { setl(b, T_RC, 41); Bchg(b, T_MSG, 0, "정산 상세(st_settle_dtl) 기록 실패", 0L); FAIL(b); } + setl(b, T_TAX, h_fee); setl(b, T_ID1, h_id); setl(b, T_FEE, h_fee); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "정산 부가세 원장 기록. (심화판) [ST_VAT] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 정산 부가세 원장 기록. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_VAT", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 정산 부가세 원장 기록. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_VATADJUST.pgc b/app/src/st/svc/ST_VATADJUST.pgc index dadce75..b498101 100644 --- a/app/src/st/svc/ST_VATADJUST.pgc +++ b/app/src/st/svc/ST_VATADJUST.pgc @@ -1,26 +1,152 @@ /* - * ST_VATADJUST.pgc - st 모듈 서비스 ST_VATADJUST (one service = one file). - * 부가세 조정: 조정된 공급가액(T_ARG1)에 대한 재산정 부가세(acq_vat)를 정산 상세에 - * VAT 라인으로 재기록하고 기존 대비 차이를 반환한다. + * ST_VATADJUST.pgc - st (정산/수수료) 모듈 서비스 ST_VATADJUST (one service = one file). + * 공급가액 조정 재부가세 기록. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_VATADJUST.h" -/* 119. ST_VATADJUST - 공급가액 조정 재부가세 기록. */ +/* ST_VATADJUST - 공급가액 조정 재부가세 기록. (심화판). (심화판) */ void ST_VATADJUST(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; - long sid = getl(b, T_SETTLE_ID), pid = getl(b, T_PURCHASE_ID); - long supply = getl(b, T_ARG1), oldvat = getl(b, T_TAX), newvat, dtl; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_VATADJUST"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (supply <= 0) { setl(b, T_RC, 1); OK(b); } - newvat = acq_vat(supply); - dtl = stdb_insert_fee_dtl(sid, pid, merch, "VAT", newvat, bizdate); - if (dtl < 0) FAIL(b); - setl(b, T_TAX, newvat); - setl(b, T_DELTA, newvat - oldvat); - setl(b, T_ID1, dtl); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "VAT"); + if (reason[0] == 0) strcpy(reason, "ST_VATADJUST"); + userlog("[정산심화] 공급가액 조정 재부가세 기록. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + if (h_pid <= 0) { setl(b, T_RC, 15); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 누락", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 공급가액 조정 재부가세 기록. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "공급가액 조정 재부가세 기록. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "공급가액 조정 재부가세 기록. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "공급가액 조정 재부가세 기록. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "공급가액 조정 재부가세 기록. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "공급가액 조정 재부가세 기록. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "공급가액 조정 재부가세 기록. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 공급가액 조정 재부가세 기록. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 공급가액 조정 재부가세 기록. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (h_bps < 0) h_bps = 0; + if (h_bps > 1000) { setl(b, T_RC, 33); Bchg(b, T_MSG, 0, "요율(bps) 상한 초과", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_base = fee0>0?fee0:amount; h_fee = acq_vat(h_base); + if (h_fee < 0) h_fee = 0; + userlog("[정산심화] 공급가액 조정 재부가세 기록. (심화판): 부가세 10% 산출 fee=%ld bps=%ld", h_fee, h_bps); + h_id = stdb_insert_fee_dtl(h_sid, h_pid, merch, "VAT", h_fee, bizdate); + if (h_id < 0) { setl(b, T_RC, 41); Bchg(b, T_MSG, 0, "정산 상세(st_settle_dtl) 기록 실패", 0L); FAIL(b); } + setl(b, T_TAX, h_fee); setl(b, T_ID1, h_id); setl(b, T_FEE, h_fee); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "공급가액 조정 재부가세 기록. (심화판) [ST_VATADJUST] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 공급가액 조정 재부가세 기록. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_VATADJUST", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 공급가액 조정 재부가세 기록. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_VATDETAIL.pgc b/app/src/st/svc/ST_VATDETAIL.pgc index c6bebe2..b467e84 100644 --- a/app/src/st/svc/ST_VATDETAIL.pgc +++ b/app/src/st/svc/ST_VATDETAIL.pgc @@ -1,32 +1,138 @@ /* - * ST_VATDETAIL.pgc - st 모듈 서비스 ST_VATDETAIL (one service = one file). - * 부가세 명세: 가맹점 당일 수수료 상세(MDR/VAN/RELAY 합계)를 공급가액으로 보고 - * 부가세(공통 acq_vat, 10%)를 산출한 부가세 명세를 반환한다 (조회 전용). + * ST_VATDETAIL.pgc - st (정산/수수료) 모듈 서비스 ST_VATDETAIL (one service = one file). + * 수수료 합계 기준 부가세 명세. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_VATDETAIL.h" -/* 115. ST_VATDETAIL - 수수료 합계 기준 부가세 명세. */ +/* ST_VATDETAIL - 수수료 합계 기준 부가세 명세. (심화판). (심화판) */ void ST_VATDETAIL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; - long vat; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_feesum, h_cnt; - char h_merch[64], h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_VATDETAIL"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_VATDETAIL"); + userlog("[정산심화] 수수료 합계 기준 부가세 명세. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT coalesce(sum(amount),0), count(*) INTO :h_feesum, :h_cnt - FROM st_settle_dtl WHERE merchant_id = :h_merch AND biz_date = :h_bizdate - AND fee_type IN ('MDR', 'VAN', 'RELAY'); - if (sqlca.sqlcode < 0) FAIL(b); - vat = acq_vat(h_feesum); - setl(b, T_GROSS, h_feesum); - setl(b, T_TAX, vat); - setl(b, T_COUNT, h_cnt); + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 수수료 합계 기준 부가세 명세. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "수수료 합계 기준 부가세 명세. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "수수료 합계 기준 부가세 명세. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "수수료 합계 기준 부가세 명세. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "수수료 합계 기준 부가세 명세. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "수수료 합계 기준 부가세 명세. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "수수료 합계 기준 부가세 명세. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 수수료 합계 기준 부가세 명세. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 수수료 합계 기준 부가세 명세. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(max(amount),0), coalesce(min(amount),0) + INTO :h_cnt, :h_feesum, :h_gmax, :h_gmin + FROM st_settle_dtl WHERE biz_date = :h_bizdate AND fee_type = :h_ftype; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 55); Bchg(b, T_MSG, 0, "수수료 상세 집계 오류", 0L); FAIL(b); } + h_avg = h_cnt > 0 ? h_feesum / h_cnt : 0; h_netsum = h_feesum; + setl(b, T_AMT1, h_gmax); setl(b, T_AMT2, h_gmin); setl(b, T_AMT3, h_avg); + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_netsum); + userlog("[정산심화] 수수료 합계 기준 부가세 명세. (심화판) cnt=%ld sum=%ld", h_cnt, h_netsum); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "수수료 합계 기준 부가세 명세. (심화판) [ST_VATDETAIL] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 수수료 합계 기준 부가세 명세. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_VATDETAIL", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 수수료 합계 기준 부가세 명세. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_VATREPORT.pgc b/app/src/st/svc/ST_VATREPORT.pgc index ff2ed2f..0d2f17f 100644 --- a/app/src/st/svc/ST_VATREPORT.pgc +++ b/app/src/st/svc/ST_VATREPORT.pgc @@ -1,34 +1,139 @@ /* - * ST_VATREPORT.pgc - st 모듈 서비스 ST_VATREPORT (one service = one file). - * 부가세 신고자료: 대상 월의 정산 상세 VAT 합계와 세금계산서(ac_tax_invoice) - * 공급가/세액 합계·건수를 함께 집계해 신고 기초자료를 회신한다. 조회 전용. + * ST_VATREPORT.pgc - st (정산/수수료) 모듈 서비스 ST_VATREPORT (one service = one file). + * 월별 부가세 신고자료 집계. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_VATREPORT.h" -/* 45. ST_VATREPORT - 월별 부가세 신고자료 집계. */ +/* ST_VATREPORT - 월별 부가세 신고자료 집계. (심화판). (심화판) */ void ST_VATREPORT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char ym[16], like[16]; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_vat, h_cnt, h_supply, h_invvat; - char h_like[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_VATREPORT"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_VATREPORT"); + userlog("[정산심화] 월별 부가세 신고자료 집계. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 월별 부가세 신고자료 집계. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "월별 부가세 신고자료 집계. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "월별 부가세 신고자료 집계. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "월별 부가세 신고자료 집계. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "월별 부가세 신고자료 집계. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "월별 부가세 신고자료 집계. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "월별 부가세 신고자료 집계. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 월별 부가세 신고자료 집계. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 월별 부가세 신고자료 집계. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ gets_(b, T_KEY1, ym, sizeof(ym)); if (ym[0] == 0) strcpy(ym, "2026-07"); - snprintf(like, sizeof(like), "%s%%", ym); - strncpy(h_like, like, sizeof(h_like)-1); h_like[sizeof(h_like)-1] = 0; - EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_vat FROM st_settle_dtl - WHERE fee_type = 'VAT' AND to_char(biz_date,'YYYY-MM-DD') LIKE :h_like; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT count(*), coalesce(sum(supply),0), coalesce(sum(vat),0) - INTO :h_cnt, :h_supply, :h_invvat FROM ac_tax_invoice - WHERE to_char(biz_date,'YYYY-MM-DD') LIKE :h_like; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_TAX, h_vat); - setl(b, T_COUNT, h_cnt); - setl(b, T_GROSS, h_supply); - setl(b, T_AMT1, h_invvat); - setl(b, T_DELTA, h_invvat - h_vat); /* 신고자료-상세 대사 차이 */ + snprintf(msgbuf, sizeof(msgbuf), "%s%%", ym); + strncpy(h_like, msgbuf, sizeof(h_like)-1); h_like[sizeof(h_like)-1] = 0; + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_cnt, :h_netsum + FROM settlement WHERE to_char(biz_date,'YYYY-MM-DD') LIKE :h_like + AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 54); Bchg(b, T_MSG, 0, "월별 집계 조회 오류", 0L); FAIL(b); } + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_netsum); + userlog("[정산심화] 월별 부가세 신고자료 집계. (심화판) cnt=%ld sum=%ld", h_cnt, h_netsum); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "월별 부가세 신고자료 집계. (심화판) [ST_VATREPORT] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 월별 부가세 신고자료 집계. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_VATREPORT", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 월별 부가세 신고자료 집계. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_VOIDRECALC.pgc b/app/src/st/svc/ST_VOIDRECALC.pgc index 29dd29f..c4d9132 100644 --- a/app/src/st/svc/ST_VOIDRECALC.pgc +++ b/app/src/st/svc/ST_VOIDRECALC.pgc @@ -1,26 +1,149 @@ /* - * ST_VOIDRECALC.pgc - st 모듈 서비스 ST_VOIDRECALC (one service = one file). - * void(전체 취소) 재계산: 매입 정산 net 전액을 역분개(음수 조정)하고 해당 - * 가맹점 지급 예정을 취소(CANCELLED) 처리한다. + * ST_VOIDRECALC.pgc - st (정산/수수료) 모듈 서비스 ST_VOIDRECALC (one service = one file). + * 전체 취소 역분개 + 지급 취소. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_VOIDRECALC.h" -/* 100. ST_VOIDRECALC - 전체 취소 역분개 + 지급 취소. */ +/* ST_VOIDRECALC - 전체 취소 역분개 + 지급 취소. (심화판). (심화판) */ void ST_VOIDRECALC(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16], merch[64]; - long sid = getl(b, T_SETTLE_ID), pid = getl(b, T_PURCHASE_ID); - long net = getl(b, T_NET), adj; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_VOIDRECALC"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - adj = stdb_insert_adjust(sid, pid, -net, "VOID", bizdate); - if (adj < 0) FAIL(b); - if (merch[0] != 0) - if (stdb_update_payment_status(merch, "SCHEDULED", "CANCELLED", bizdate) < 0) FAIL(b); - setl(b, T_ADJ, adj); - setl(b, T_DELTA, -net); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_VOIDRECALC"); + userlog("[정산심화] 전체 취소 역분개 + 지급 취소. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 전체 취소 역분개 + 지급 취소. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "전체 취소 역분개 + 지급 취소. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "전체 취소 역분개 + 지급 취소. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "전체 취소 역분개 + 지급 취소. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "전체 취소 역분개 + 지급 취소. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "전체 취소 역분개 + 지급 취소. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "전체 취소 역분개 + 지급 취소. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 전체 취소 역분개 + 지급 취소. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 전체 취소 역분개 + 지급 취소. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (strcmp(h_status, "NONE") == 0) { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "정산 원장이 없습니다", 0L); Bchg(b, T_STATUS, 0, "NONE", 0L); OK(b); } + if (strcmp(h_status, "SETTLED") != 0) { setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "전체 취소 전이 불가(현상태 부적합)", 0L); Bchg(b, T_STATUS, 0, h_status, 0L); OK(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + EXEC SQL UPDATE settlement SET status = 'VOID' + WHERE purchase_id = :h_pid AND status = 'SETTLED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 46); Bchg(b, T_MSG, 0, "정산 상태 갱신 실패", 0L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 3); Bchg(b, T_MSG, 0, "상태 갱신 대상 없음", 0L); Bchg(b, T_STATUS, 0, h_status, 0L); OK(b); } + EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) + VALUES (nextval('ledger_seq'), :h_pid, 'STV', :h_net, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 47); Bchg(b, T_MSG, 0, "상태 전이 원장 기록 실패", 0L); FAIL(b); } + h_id = stdb_insert_adjust(h_sid, h_pid, -h_net, "VOID", bizdate); + if (h_id < 0) { setl(b, T_RC, 48); Bchg(b, T_MSG, 0, "취소 상계 조정 실패", 0L); FAIL(b); } + userlog("[정산심화] 전체 취소 역분개 + 지급 취소. (심화판) pid=%ld SETTLED->VOID", h_pid); Bchg(b, T_STATUS, 0, "V", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "전체 취소 역분개 + 지급 취소. (심화판) [ST_VOIDRECALC] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 전체 취소 역분개 + 지급 취소. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_VOIDRECALC", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 전체 취소 역분개 + 지급 취소. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_VOLREBATE.pgc b/app/src/st/svc/ST_VOLREBATE.pgc index 924388e..d8a13f8 100644 --- a/app/src/st/svc/ST_VOLREBATE.pgc +++ b/app/src/st/svc/ST_VOLREBATE.pgc @@ -1,41 +1,135 @@ /* - * ST_VOLREBATE.pgc - st 모듈 서비스 ST_VOLREBATE (one service = one file). - * 거래량 리베이트: 가맹점 일별 집계의 거래 건수 구간에 따라 리베이트율을 차등 - * 적용해(건수 많을수록 우대) 매출 기준 리베이트를 조정으로 기록한다. + * ST_VOLREBATE.pgc - st (정산/수수료) 모듈 서비스 ST_VOLREBATE (one service = one file). + * 거래건수 구간별 리베이트. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_VOLREBATE.h" -/* 105. ST_VOLREBATE - 거래건수 구간별 리베이트. */ +/* ST_VOLREBATE - 거래건수 구간별 리베이트. (심화판). (심화판) */ void ST_VOLREBATE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; - long bps, rebate, adj; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_cnt, h_gross; - char h_merch[64], h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_VOLREBATE"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_VOLREBATE"); + userlog("[정산심화] 거래건수 구간별 리베이트. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL SELECT coalesce(txn_count,0), coalesce(gross_amount,0) - INTO :h_cnt, :h_gross FROM st_merch_settle - WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; - if (sqlca.sqlcode < 0) FAIL(b); - if (sqlca.sqlcode == 100) { setl(b, T_RC, 1); OK(b); } - if (h_cnt >= 1000) bps = 100; - else if (h_cnt >= 500) bps = 50; - else if (h_cnt >= 100) bps = 20; - else bps = 0; - if (bps == 0) { setl(b, T_RC, 1); OK(b); } - rebate = acq_fee(h_gross, bps); - adj = stdb_insert_adjust(0, 0, rebate, "VOLREBATE", bizdate); - if (adj < 0) FAIL(b); - setl(b, T_ADJ, adj); - setl(b, T_COUNT, h_cnt); - setl(b, T_DELTA, rebate); - setl(b, T_RC, bps); + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 거래건수 구간별 리베이트. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "거래건수 구간별 리베이트. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "거래건수 구간별 리베이트. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "거래건수 구간별 리베이트. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "거래건수 구간별 리베이트. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "거래건수 구간별 리베이트. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "거래건수 구간별 리베이트. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 거래건수 구간별 리베이트. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 거래건수 구간별 리베이트. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_delta = (delta0>0?delta0:net0/100); + userlog("[정산심화] 거래건수 구간별 리베이트. (심화판): 실적 기반 가산 조정(양수) delta=%ld", h_delta, h_bps); + h_id = stdb_insert_adjust(h_sid, h_pid, h_delta, "SETTLE_BONUS", bizdate); + if (h_id < 0) { setl(b, T_RC, 42); Bchg(b, T_MSG, 0, "정산 조정(st_adjust) 기록 실패", 0L); FAIL(b); } + setl(b, T_ADJ, h_id); setl(b, T_DELTA, h_delta); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "거래건수 구간별 리베이트. (심화판) [ST_VOLREBATE] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 거래건수 구간별 리베이트. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_VOLREBATE", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 거래건수 구간별 리베이트. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_WHT.pgc b/app/src/st/svc/ST_WHT.pgc index 5bc0866..f9a15b9 100644 --- a/app/src/st/svc/ST_WHT.pgc +++ b/app/src/st/svc/ST_WHT.pgc @@ -1,23 +1,152 @@ /* - * ST_WHT.pgc - st 모듈 서비스 ST_WHT (one service = one file). - * Moved verbatim from the former inline st_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by st_svr at tpsvrinit. + * ST_WHT.pgc - st (정산/수수료) 모듈 서비스 ST_WHT (one service = one file). + * 원천징수(수수료의 22%) 정산 상세 기록. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_WHT.h" -/* 7. ST_WHT - 원천징수(수수료의 22%) 정산 상세 기록 (dbio). */ +/* ST_WHT - 원천징수(수수료의 22%) 정산 상세 기록. (심화판). (심화판) */ void ST_WHT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; - long sid = getl(b, T_SETTLE_ID), pid = getl(b, T_PURCHASE_ID); - long fee = getl(b, T_FEE), tax, dtl; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; + EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_WHT"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - tax = acq_wht_amt(fee, 2200); - dtl = stdb_insert_fee_dtl(sid, pid, merch, "WHT", tax, bizdate); - if (dtl < 0) FAIL(b); - setl(b, T_TAX, tax); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "WHT"); + if (reason[0] == 0) strcpy(reason, "ST_WHT"); + userlog("[정산심화] 원천징수(수수료의 22%) 정산 상세 기록. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (merch[0] == 0) { setl(b, T_RC, 14); Bchg(b, T_MSG, 0, "가맹점 ID는 필수입니다", 0L); FAIL(b); } + if (h_pid <= 0) { setl(b, T_RC, 15); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 누락", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + if (strcmp(h_mstatus, "ACTIVE") != 0) { setl(b, T_RC, 23); Bchg(b, T_MSG, 0, "비활성 가맹점은 정산 불가", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 원천징수(수수료의 22%) 정산 상세 기록. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "원천징수(수수료의 22%) 정산 상세 기록. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "원천징수(수수료의 22%) 정산 상세 기록. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "원천징수(수수료의 22%) 정산 상세 기록. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "원천징수(수수료의 22%) 정산 상세 기록. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "원천징수(수수료의 22%) 정산 상세 기록. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "원천징수(수수료의 22%) 정산 상세 기록. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 원천징수(수수료의 22%) 정산 상세 기록. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 원천징수(수수료의 22%) 정산 상세 기록. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (h_bps < 0) h_bps = 0; + if (h_bps > 1000) { setl(b, T_RC, 33); Bchg(b, T_MSG, 0, "요율(bps) 상한 초과", 0L); FAIL(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + h_base = fee0; h_fee = acq_wht_amt(h_base, 2200); + if (h_fee < 0) h_fee = 0; + userlog("[정산심화] 원천징수(수수료의 22%) 정산 상세 기록. (심화판): 원천징수 22% 산출 fee=%ld bps=%ld", h_fee, h_bps); + h_id = stdb_insert_fee_dtl(h_sid, h_pid, merch, "WHT", h_fee, bizdate); + if (h_id < 0) { setl(b, T_RC, 41); Bchg(b, T_MSG, 0, "정산 상세(st_settle_dtl) 기록 실패", 0L); FAIL(b); } + setl(b, T_TAX, h_fee); setl(b, T_ID1, h_id); setl(b, T_FEE, h_fee); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "원천징수(수수료의 22%) 정산 상세 기록. (심화판) [ST_WHT] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0) + INTO :h_cnt, :h_gross, :h_feesum, :h_netsum + FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id + WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 61); Bchg(b, T_MSG, 0, "가맹점 집계 재계산 오류", 0L); FAIL(b); } + if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_feesum, h_netsum, h_cnt) < 0) + { setl(b, T_RC, 64); Bchg(b, T_MSG, 0, "가맹점 정산집계 upsert 실패", 0L); FAIL(b); } + setl(b, T_GROSS, h_gross); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 원천징수(수수료의 22%) 정산 상세 기록. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_WHT", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 원천징수(수수료의 22%) 정산 상세 기록. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_WHTDETAIL.pgc b/app/src/st/svc/ST_WHTDETAIL.pgc index 24f0936..40ac012 100644 --- a/app/src/st/svc/ST_WHTDETAIL.pgc +++ b/app/src/st/svc/ST_WHTDETAIL.pgc @@ -1,32 +1,138 @@ /* - * ST_WHTDETAIL.pgc - st 모듈 서비스 ST_WHTDETAIL (one service = one file). - * 원천세 명세: 가맹점 당일 수수료 합계에 원천징수율(T_ARG1 bps)을 적용해 - * 원천세(공통 acq_wht_amt)를 산출한 원천세 명세를 반환한다 (조회 전용). + * ST_WHTDETAIL.pgc - st (정산/수수료) 모듈 서비스 ST_WHTDETAIL (one service = one file). + * 수수료 합계 기준 원천세 명세. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_WHTDETAIL.h" -/* 116. ST_WHTDETAIL - 수수료 합계 기준 원천세 명세. */ +/* ST_WHTDETAIL - 수수료 합계 기준 원천세 명세. (심화판). (심화판) */ void ST_WHTDETAIL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], bizdate[16]; - long bps = getl(b, T_ARG1), wht; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_feesum; - char h_merch[64], h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_WHTDETAIL"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_WHTDETAIL"); + userlog("[정산심화] 수수료 합계 기준 원천세 명세. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - if (bps <= 0) bps = 330; /* 기본 3.3% 원천 */ - EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_feesum FROM st_settle_dtl - WHERE merchant_id = :h_merch AND biz_date = :h_bizdate AND fee_type = 'PCT'; - if (sqlca.sqlcode < 0) FAIL(b); - wht = acq_wht_amt(h_feesum, bps); - setl(b, T_GROSS, h_feesum); - setl(b, T_TAX, wht); - setl(b, T_RC, bps); + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 수수료 합계 기준 원천세 명세. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "수수료 합계 기준 원천세 명세. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "수수료 합계 기준 원천세 명세. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "수수료 합계 기준 원천세 명세. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "수수료 합계 기준 원천세 명세. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "수수료 합계 기준 원천세 명세. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "수수료 합계 기준 원천세 명세. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 수수료 합계 기준 원천세 명세. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 수수료 합계 기준 원천세 명세. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(max(amount),0), coalesce(min(amount),0) + INTO :h_cnt, :h_feesum, :h_gmax, :h_gmin + FROM st_settle_dtl WHERE biz_date = :h_bizdate AND fee_type = :h_ftype; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 55); Bchg(b, T_MSG, 0, "수수료 상세 집계 오류", 0L); FAIL(b); } + h_avg = h_cnt > 0 ? h_feesum / h_cnt : 0; h_netsum = h_feesum; + setl(b, T_AMT1, h_gmax); setl(b, T_AMT2, h_gmin); setl(b, T_AMT3, h_avg); + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_netsum); + userlog("[정산심화] 수수료 합계 기준 원천세 명세. (심화판) cnt=%ld sum=%ld", h_cnt, h_netsum); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "수수료 합계 기준 원천세 명세. (심화판) [ST_WHTDETAIL] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 수수료 합계 기준 원천세 명세. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_WHTDETAIL", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 수수료 합계 기준 원천세 명세. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_WHTREPORT.pgc b/app/src/st/svc/ST_WHTREPORT.pgc index 3a0f25d..f2527ab 100644 --- a/app/src/st/svc/ST_WHTREPORT.pgc +++ b/app/src/st/svc/ST_WHTREPORT.pgc @@ -1,36 +1,139 @@ /* - * ST_WHTREPORT.pgc - st 모듈 서비스 ST_WHTREPORT (one service = one file). - * 원천세 명세: 가맹점의 대상 월 정산 상세(WHT) 합계와 원천 대장(ac_wht) 합계를 - * 각각 집계·대사하고 차이를 회신한다. 불일치 시 RC=1. 조회 전용. + * ST_WHTREPORT.pgc - st (정산/수수료) 모듈 서비스 ST_WHTREPORT (one service = one file). + * 가맹점 월별 원천세 명세 대사. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_WHTREPORT.h" -/* 46. ST_WHTREPORT - 가맹점 월별 원천세 명세 대사. */ +/* ST_WHTREPORT - 가맹점 월별 원천세 명세 대사. (심화판). (심화판) */ void ST_WHTREPORT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char merch[64], ym[16], like[16]; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_dtl, h_reg; - char h_merch[64], h_like[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; + + strcpy(h_svc, "ST_WHTREPORT"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ gets_(b, T_MERCHANT, merch, sizeof(merch)); - gets_(b, T_KEY1, ym, sizeof(ym)); if (ym[0] == 0) strcpy(ym, "2026-07"); - snprintf(like, sizeof(like), "%s%%", ym); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_WHTREPORT"); + userlog("[정산심화] 가맹점 월별 원천세 명세 대사. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; - strncpy(h_like, like, sizeof(h_like)-1); h_like[sizeof(h_like)-1] = 0; - EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_dtl FROM st_settle_dtl - WHERE merchant_id = :h_merch AND fee_type = 'WHT' - AND to_char(biz_date,'YYYY-MM-DD') LIKE :h_like; - if (sqlca.sqlcode < 0) FAIL(b); - EXEC SQL SELECT coalesce(sum(w.tax_amount),0) INTO :h_reg - FROM ac_wht w JOIN purchase p ON p.purchase_id = w.purchase_id - WHERE p.merchant_id = :h_merch AND to_char(w.biz_date,'YYYY-MM-DD') LIKE :h_like; - if (sqlca.sqlcode < 0) FAIL(b); - setl(b, T_TAX, h_dtl); - setl(b, T_AMT1, h_reg); - setl(b, T_DELTA, h_reg - h_dtl); - setl(b, T_RC, (h_reg == h_dtl) ? 0 : 1); + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 가맹점 월별 원천세 명세 대사. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "가맹점 월별 원천세 명세 대사. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "가맹점 월별 원천세 명세 대사. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "가맹점 월별 원천세 명세 대사. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "가맹점 월별 원천세 명세 대사. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "가맹점 월별 원천세 명세 대사. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "가맹점 월별 원천세 명세 대사. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 가맹점 월별 원천세 명세 대사. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 가맹점 월별 원천세 명세 대사. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + gets_(b, T_KEY1, ym, sizeof(ym)); if (ym[0] == 0) strcpy(ym, "2026-07"); + snprintf(msgbuf, sizeof(msgbuf), "%s%%", ym); + strncpy(h_like, msgbuf, sizeof(h_like)-1); h_like[sizeof(h_like)-1] = 0; + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_cnt, :h_netsum + FROM settlement WHERE to_char(biz_date,'YYYY-MM-DD') LIKE :h_like + AND (:h_merch = '' OR merchant_id = :h_merch); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 54); Bchg(b, T_MSG, 0, "월별 집계 조회 오류", 0L); FAIL(b); } + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_netsum); + userlog("[정산심화] 가맹점 월별 원천세 명세 대사. (심화판) cnt=%ld sum=%ld", h_cnt, h_netsum); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "가맹점 월별 원천세 명세 대사. (심화판) [ST_WHTREPORT] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 가맹점 월별 원천세 명세 대사. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_WHTREPORT", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 가맹점 월별 원천세 명세 대사. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_WORKFLOW.pgc b/app/src/st/svc/ST_WORKFLOW.pgc index 8012d79..127e5cc 100644 --- a/app/src/st/svc/ST_WORKFLOW.pgc +++ b/app/src/st/svc/ST_WORKFLOW.pgc @@ -1,35 +1,148 @@ /* - * ST_WORKFLOW.pgc - st 모듈 서비스 ST_WORKFLOW (one service = one file). - * 정산 워크플로 상태 전이: 요청 전이(from=T_STR1 -> to=T_STR2)가 허용 전이표 - * (SETTLED->LOCKED->PAID)에 부합하는지 검증한 뒤에만 정산 상태를 갱신한다. + * ST_WORKFLOW.pgc - st (정산/수수료) 모듈 서비스 ST_WORKFLOW (one service = one file). + * 허용 전이 검증 후 정산 상태 갱신. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_WORKFLOW.h" -/* 124. ST_WORKFLOW - 허용 전이 검증 후 정산 상태 갱신. */ +/* ST_WORKFLOW - 허용 전이 검증 후 정산 상태 갱신. (심화판). (심화판) */ void ST_WORKFLOW(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char from[16], to[16], bizdate[16]; - int valid = 0; - long moved; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - char h_from[16], h_to[16], h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; - gets_(b, T_STR1, from, sizeof(from)); - gets_(b, T_STR2, to, sizeof(to)); + + strcpy(h_svc, "ST_WORKFLOW"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); - if (strcmp(from, "SETTLED") == 0 && strcmp(to, "LOCKED") == 0) valid = 1; - else if (strcmp(from, "LOCKED") == 0 && strcmp(to, "PAID") == 0) valid = 1; - if (!valid) { setl(b, T_RC, -1); OK(b); } - strncpy(h_from, from, sizeof(h_from)-1); h_from[sizeof(h_from)-1] = 0; - strncpy(h_to, to, sizeof(h_to)-1); h_to[sizeof(h_to)-1] = 0; + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_WORKFLOW"); + userlog("[정산심화] 허용 전이 검증 후 정산 상태 갱신. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + if (h_pid <= 0) { setl(b, T_RC, 15); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 누락", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; - EXEC SQL UPDATE settlement SET status = :h_to - WHERE biz_date = :h_bizdate AND status = :h_from; - if (sqlca.sqlcode < 0) FAIL(b); - moved = sqlca.sqlerrd[2]; - setl(b, T_COUNT, moved); - setl(b, T_RC, 0); + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + if (h_pid > 0) { + EXEC SQL SELECT status, coalesce(net,0) INTO :h_status, :h_net + FROM settlement WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 24); Bchg(b, T_MSG, 0, "정산 원장 상태 조회 오류", 0L); FAIL(b); } + if (sqlca.sqlcode == 100) { strcpy(h_status, "NONE"); h_net = 0; } + } else { strcpy(h_status, "NONE"); } + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 허용 전이 검증 후 정산 상태 갱신. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "허용 전이 검증 후 정산 상태 갱신. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "허용 전이 검증 후 정산 상태 갱신. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "허용 전이 검증 후 정산 상태 갱신. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "허용 전이 검증 후 정산 상태 갱신. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "허용 전이 검증 후 정산 상태 갱신. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "허용 전이 검증 후 정산 상태 갱신. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 허용 전이 검증 후 정산 상태 갱신. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 허용 전이 검증 후 정산 상태 갱신. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + if (strcmp(h_status, "NONE") == 0) { setl(b, T_RC, 1); Bchg(b, T_MSG, 0, "정산 원장이 없습니다", 0L); Bchg(b, T_STATUS, 0, "NONE", 0L); OK(b); } + if (strcmp(h_status, "SETTLED") != 0) { setl(b, T_RC, 2); Bchg(b, T_MSG, 0, "워크플로 전이 전이 불가(현상태 부적합)", 0L); Bchg(b, T_STATUS, 0, h_status, 0L); OK(b); } + vstep = 3; + + /* ===== 4) 본처리부 ===== */ + EXEC SQL UPDATE settlement SET status = 'CONFIRMED' + WHERE purchase_id = :h_pid AND status = 'SETTLED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 46); Bchg(b, T_MSG, 0, "정산 상태 갱신 실패", 0L); FAIL(b); } + if (sqlca.sqlerrd[2] == 0) { setl(b, T_RC, 3); Bchg(b, T_MSG, 0, "상태 갱신 대상 없음", 0L); Bchg(b, T_STATUS, 0, h_status, 0L); OK(b); } + EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) + VALUES (nextval('ledger_seq'), :h_pid, 'STC', :h_net, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 47); Bchg(b, T_MSG, 0, "상태 전이 원장 기록 실패", 0L); FAIL(b); } + userlog("[정산심화] 허용 전이 검증 후 정산 상태 갱신. (심화판) pid=%ld SETTLED->CONFIRMED", h_pid); + Bchg(b, T_STATUS, 0, "C", 0L); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "허용 전이 검증 후 정산 상태 갱신. (심화판) [ST_WORKFLOW] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 허용 전이 검증 후 정산 상태 갱신. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_WORKFLOW", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 허용 전이 검증 후 정산 상태 갱신. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/st/svc/ST_XFERLINK.pgc b/app/src/st/svc/ST_XFERLINK.pgc index 70c14bc..c8786ab 100644 --- a/app/src/st/svc/ST_XFERLINK.pgc +++ b/app/src/st/svc/ST_XFERLINK.pgc @@ -1,25 +1,135 @@ /* - * ST_XFERLINK.pgc - st 모듈 서비스 ST_XFERLINK (one service = one file). - * Moved verbatim from the former inline st_svr.pgc; runs on the caller's XA - * branch (no EXEC SQL CONNECT). Advertised by st_svr at tpsvrinit. + * ST_XFERLINK.pgc - st (정산/수수료) 모듈 서비스 ST_XFERLINK (one service = one file). + * 이체 연계 원장 기록. (심화판). + * 심화 온라인 프로그램: 입력검증 -> 마스터/정책 조회 -> 업무규칙 분기 -> 본처리 -> + * 이력적재 -> 집계 재계산 -> 예외보상 -> 응답 -> 로깅. 호출자의 XA 브랜치에서 수행하며 + * EXEC SQL CONNECT 는 두지 않는다. 진입 서명/서비스명/입출력 계약은 불변. */ #include "ST_XFERLINK.h" -/* 22. ST_XFERLINK - 이체 연계 원장 기록. */ +/* ST_XFERLINK - 이체 연계 원장 기록. (심화판). (심화판) */ void ST_XFERLINK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; - char bizdate[16]; + /* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */ + char merch[64], bizdate[16], paydate[16], reason[128], ftype[16]; + char msgbuf[200], ym[16], chan[16]; + long amount, net0, fee0, delta0, tmp; + int vstep = 0; + EXEC SQL BEGIN DECLARE SECTION; - long h_pid, h_net; - char h_bizdate[16]; + long h_pid, h_sid, h_amount, h_fee, h_net, h_bps, h_mdr, h_limit; + long h_cnt, h_sum, h_gmax, h_gmin, h_avg, h_rows, h_id, h_prior; + long h_delta, h_gross, h_feesum, h_netsum, h_tax, h_days, h_base, h_supply, h_n; + char h_merch[64], h_bizdate[16], h_paydate[16], h_status[16], h_ftype[16]; + char h_reason[128], h_msg[200], h_svc[24], h_mstatus[16], h_ym[16], h_like[24]; EXEC SQL END DECLARE SECTION; - h_pid = getl(b, T_PURCHASE_ID); h_net = getl(b, T_NET); + + strcpy(h_svc, "ST_XFERLINK"); + h_fee = 0; h_delta = 0; h_id = 0; h_cnt = 0; h_netsum = 0; h_base = 0; + h_days = 0; h_supply = 0; h_tax = 0; h_gross = 0; h_prior = 0; h_n = 0; + h_amount = 0; h_net = 0; h_sum = 0; h_gmax = 0; h_gmin = 0; h_avg = 0; h_rows = 0; + h_feesum = 0; + + /* ===== 1) 입력 검증부 ===== */ + gets_(b, T_MERCHANT, merch, sizeof(merch)); gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_REASON, reason, sizeof(reason)); + gets_(b, T_STR1, ftype, sizeof(ftype)); + amount = getl(b, T_AMOUNT); net0 = getl(b, T_NET); fee0 = getl(b, T_FEE); + delta0 = getl(b, T_DELTA); h_pid = getl(b, T_PURCHASE_ID); h_sid = getl(b, T_SETTLE_ID); + if (bizdate[0] == 0) strcpy(bizdate, "2026-07-20"); + if (ftype[0] == 0) strcpy(ftype, "MDR"); + if (reason[0] == 0) strcpy(reason, "ST_XFERLINK"); + userlog("[정산심화] 이체 연계 원장 기록. (심화판) 진입 pid=%ld merch=%s biz=%s", h_pid, merch, bizdate); + if (amount < 0) { setl(b, T_RC, 11); Bchg(b, T_MSG, 0, "매입 금액은 음수가 될 수 없습니다", 0L); FAIL(b); } + if (fee0 < 0) { setl(b, T_RC, 12); Bchg(b, T_MSG, 0, "수수료 입력값이 음수입니다", 0L); FAIL(b); } + if (strlen(bizdate) < 8) { setl(b, T_RC, 13); Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); FAIL(b); } + if (net0 < 0 && fee0 == 0) { setl(b, T_RC, 16); Bchg(b, T_MSG, 0, "정산 금액(net) 음수 입력 불가", 0L); FAIL(b); } + if (h_sid < 0) { setl(b, T_RC, 17); Bchg(b, T_MSG, 0, "정산 ID(settlement_id) 음수 오류", 0L); FAIL(b); } + if (delta0 > 100000000 || delta0 < -100000000) { setl(b, T_RC, 18); Bchg(b, T_MSG, 0, "조정 금액 허용범위(±1억) 초과", 0L); FAIL(b); } + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + strncpy(h_ftype, ftype, sizeof(h_ftype)-1); h_ftype[sizeof(h_ftype)-1] = 0; + strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0; + vstep = 1; + + /* ===== 2) 마스터/정책/상태 조회부 ===== */ + h_mdr = 25; h_limit = 0; strcpy(h_mstatus, "ACTIVE"); + if (merch[0] != 0) { + EXEC SQL SELECT mdr_bps, daily_limit, status + INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode == 100) { setl(b, T_RC, 21); Bchg(b, T_MSG, 0, "미등록 가맹점입니다", 0L); FAIL(b); } + if (sqlca.sqlcode < 0) { setl(b, T_RC, 22); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b); } + } + h_bps = getl(b, T_ARG2); if (h_bps <= 0) h_bps = h_mdr; /* 계약 요율 우선, 없으면 가맹점 MDR */ + vstep = 2; + + /* ===== 2b) 한도/직전 지급잔량 조회부 ===== */ + h_sum = 0; h_avg = 0; h_n = 0; h_gross = 0; + if (merch[0] != 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) { setl(b, T_RC, 26); Bchg(b, T_MSG, 0, "당일 매입 누계 조회 오류", 0L); FAIL(b); } + h_avg = h_limit - h_sum; /* 일 한도 잔여 */ + if (h_limit > 0 && h_sum > h_limit) + userlog("[정산심화] 이체 연계 원장 기록. (심화판) 일 한도 초과 감지 sum=%ld limit=%ld", h_sum, h_limit); + } + EXEC SQL SELECT count(*), coalesce(sum(net_amount),0) INTO :h_n, :h_gross + FROM st_payment WHERE merchant_id = :h_merch AND status = 'SCHEDULED'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 27); Bchg(b, T_MSG, 0, "예정 지급 잔량 조회 오류", 0L); FAIL(b); } + setl(b, T_ARG6, h_avg); + vstep = 3; + + /* ===== 3) 업무규칙 분기부 ===== */ + if (amount >= 10000000) { h_prior = 3; userlog("[정산심화] %s 대액 구간(>=1천만) 처리", "이체 연계 원장 기록. (심화판)"); } + else if (amount >= 1000000) { h_prior = 2; userlog("[정산심화] %s 중액 구간 처리", "이체 연계 원장 기록. (심화판)"); } + else { h_prior = 1; userlog("[정산심화] %s 소액 구간 처리", "이체 연계 원장 기록. (심화판)"); } + gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "POS"); + if (strcmp(chan, "FOREIGN") == 0) { h_prior += 10; userlog("[정산심화] %s 해외 채널 가산 분기", "이체 연계 원장 기록. (심화판)"); } + else if (strcmp(chan, "EDI") == 0) { h_prior += 5; userlog("[정산심화] %s EDI 채널 분기", "이체 연계 원장 기록. (심화판)"); } + else { userlog("[정산심화] %s 국내 채널 표준 분기", "이체 연계 원장 기록. (심화판)"); } + if (h_mdr >= 30) userlog("[정산심화] 이체 연계 원장 기록. (심화판) 고율 등급 가맹점(bps=%ld)", h_mdr); + else if (h_mdr <= 20) userlog("[정산심화] 이체 연계 원장 기록. (심화판) 우대 등급 가맹점(bps=%ld)", h_mdr); + vstep = 3; + + /* ===== 4) 본처리부 ===== */ EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) - VALUES (nextval('ledger_seq'), :h_pid, 'ST_XFER', :h_net, :h_bizdate); - if (sqlca.sqlcode < 0) FAIL(b); + VALUES (nextval('ledger_seq'), :h_pid, 'XFER', :h_net, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 53); Bchg(b, T_MSG, 0, "연계 원장 기록 실패", 0L); FAIL(b); } + setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_net); + userlog("[정산심화] 이체 연계 원장 기록. (심화판) cnt=%ld amount=%ld", h_cnt, h_net); + vstep = 4; + + /* ===== 5) 이력/감사 적재부 (cm_log) ===== */ + snprintf(msgbuf, sizeof(msgbuf), "이체 연계 원장 기록. (심화판) [ST_XFERLINK] pid=%ld fee=%ld delta=%ld net=%ld", h_pid, h_fee, h_delta, h_netsum); + strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0; + EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES ((SELECT coalesce(max(log_id),0)+1 FROM cm_log), :h_svc, 'I', :h_msg, :h_bizdate); + if (sqlca.sqlcode < 0) { setl(b, T_RC, 91); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); } + vstep = 5; + + /* ===== 6) 집계/요약 갱신부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM settlement WHERE biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 65); Bchg(b, T_MSG, 0, "일 정산 건수 재확인 오류", 0L); FAIL(b); } + setl(b, T_ARG3, h_rows); + vstep = 6; + + /* ===== 7) 예외/보상 검증부 ===== */ + EXEC SQL SELECT count(*) INTO :h_rows FROM cm_log + WHERE svc_name = :h_svc AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 71); Bchg(b, T_MSG, 0, "감사 로그 검증 조회 오류", 0L); FAIL(b); } + if (h_rows <= 0) { setl(b, T_RC, 72); Bchg(b, T_MSG, 0, "감사 로그 미적재(보상 롤백)", 0L); FAIL(b); } + setl(b, T_ARG5, h_rows); + userlog("[정산심화] 이체 연계 원장 기록. (심화판) 보상검증 통과 logrows=%ld step=%d", h_rows, vstep); + vstep = 7; + + /* ===== 8) 응답부 ===== */ + if (Bpres(b, T_RC, 0) == 0) setl(b, T_RC, 0); + Bchg(b, T_SVCNAME, 0, "ST_XFERLINK", 0L); + setl(b, T_ARG4, (long)vstep); + /* ===== 9) 완료 로깅 ===== */ + userlog("[정산심화] 이체 연계 원장 기록. (심화판) 완료 rc=0 pid=%ld fee=%ld net=%ld", h_pid, h_fee, h_netsum); OK(b); } diff --git a/app/src/vl/svc/VL_AGEDCONF.pgc b/app/src/vl/svc/VL_AGEDCONF.pgc index 9be125c..3c1293d 100644 --- a/app/src/vl/svc/VL_AGEDCONF.pgc +++ b/app/src/vl/svc/VL_AGEDCONF.pgc @@ -9,6 +9,116 @@ void VL_AGEDCONF(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_AGEDCONF: 검증 착수 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_AGEDCONF: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_AGEDCONF' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_AGEDCONF: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (조건 분기) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_AGEDCONF: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_AGEDCONF flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_AGEDCONF", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_AGEDCONF: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_AGEDCONF: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_AGEDCONF: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_AGEDCONF: 심화검증 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0, esc, bumped = 0; EXEC SQL BEGIN DECLARE SECTION; long h_days, h_cnt; diff --git a/app/src/vl/svc/VL_AGEDOPEN.pgc b/app/src/vl/svc/VL_AGEDOPEN.pgc index 8e7e3ef..5af8921 100644 --- a/app/src/vl/svc/VL_AGEDOPEN.pgc +++ b/app/src/vl/svc/VL_AGEDOPEN.pgc @@ -9,6 +9,122 @@ void VL_AGEDOPEN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_AGEDOPEN: 요청 수신 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_AGEDOPEN: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + if (getl(b, T_ID1) < 0) { + setl(b, T_RC, 95); + Bchg(b, T_MSG, 0, "의심거래 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_AGEDOPEN' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_AGEDOPEN: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (룰 매칭) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_AGEDOPEN: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_AGEDOPEN flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_AGEDOPEN", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_AGEDOPEN: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_AGEDOPEN: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_AGEDOPEN: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_AGEDOPEN: 심화검증 검증 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; long h_days, h_cnt, h_oldest; diff --git a/app/src/vl/svc/VL_AMOUNT.pgc b/app/src/vl/svc/VL_AMOUNT.pgc index 4539ad9..6b12a89 100644 --- a/app/src/vl/svc/VL_AMOUNT.pgc +++ b/app/src/vl/svc/VL_AMOUNT.pgc @@ -9,6 +9,124 @@ void VL_AMOUNT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_AMOUNT: 진입 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_AMOUNT: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_amt = getl(b, T_AMOUNT); + if (v_amt < 0) { + setl(b, T_RC, 94); + Bchg(b, T_MSG, 0, "매입 금액 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_AMOUNT' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_AMOUNT: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (룰 매칭) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_AMOUNT: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_AMOUNT flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_AMOUNT", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_AMOUNT: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_AMOUNT: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_AMOUNT: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_AMOUNT: 심화검증 정상 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long amt, rc = 0, thr = 50000000; char sev[8]; amt = getl(b, T_AMOUNT); diff --git a/app/src/vl/svc/VL_ANOMALY.pgc b/app/src/vl/svc/VL_ANOMALY.pgc index 673d691..0d7207d 100644 --- a/app/src/vl/svc/VL_ANOMALY.pgc +++ b/app/src/vl/svc/VL_ANOMALY.pgc @@ -9,6 +9,150 @@ void VL_ANOMALY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_ANOMALY: 요청 수신 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_ANOMALY: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_pid = getl(b, T_PURCHASE_ID); + if (v_pid < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "매입 ID 음수 불가", 0L); + FAIL(b); + } + if (getl(b, T_ID1) < 0) { + setl(b, T_RC, 95); + Bchg(b, T_MSG, 0, "의심거래 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + EXEC SQL SELECT merchant_id, amount, fee, net, status + INTO :v_merch, :v_amt, :v_fee, :v_net, :v_pstat + FROM purchase WHERE purchase_id = :v_pid; + if (sqlca.sqlcode == 100) { + v_flag |= 0x01; /* 매입 원거래 미존재 */ + userlog("VL_ANOMALY: 매입 미존재 pid=%ld", v_pid); + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 81); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG3, v_net); + v_base = v_amt; + } + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_ANOMALY' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_ANOMALY: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (룰 매칭) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_ANOMALY: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*) INTO :v_acnt + FROM approval WHERE purchase_id = :v_pid AND approved = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 83); Bchg(b, T_MSG, 0, "승인 대사 오류", 0L); FAIL(b); } + setl(b, T_ARG4, v_acnt); + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_lcnt, :v_lsum + FROM ledger WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 84); Bchg(b, T_MSG, 0, "원장 대사 오류", 0L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :v_scnt, :v_snet + FROM settlement WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 85); Bchg(b, T_MSG, 0, "정산 대사 오류", 0L); FAIL(b); } + if (v_acnt == 0) v_flag |= 0x08; /* 무승인 */ + if (v_lcnt == 0) v_flag |= 0x10; /* 원장 미기록 */ + if (v_scnt > 0 && v_snet != v_net) v_flag |= 0x20; /* 정산액 불일치 */ + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_ANOMALY flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_ANOMALY", v_pid, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_ANOMALY: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_metric >= 3 && !(v_flag & 0x01)) { + v_sid = vldb_insert_suspect(v_pid, v_rule, v_sev, "심화검증 임계 초과", v_bizdate); + if (v_sid < 0) { setl(b, T_RC, 89); Bchg(b, T_MSG, 0, "의심거래 적재 실패", 0L); FAIL(b); } + setl(b, T_ID2, v_sid); + userlog("VL_ANOMALY: 의심거래 적재 sid=%ld rule=%s", v_sid, v_rule); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_ANOMALY: 심화검증 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ char bizdate[16]; long rc = 0, sid = 0, thr = 50000000; char sev[8]; diff --git a/app/src/vl/svc/VL_APPRAGG.pgc b/app/src/vl/svc/VL_APPRAGG.pgc index 7ad773f..d8e71fd 100644 --- a/app/src/vl/svc/VL_APPRAGG.pgc +++ b/app/src/vl/svc/VL_APPRAGG.pgc @@ -9,6 +9,119 @@ void VL_APPRAGG(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_APPRAGG: 요청 수신 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_APPRAGG: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_APPRAGG' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_APPRAGG: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (분기 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_APPRAGG: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_APPRAGG flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_APPRAGG", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_APPRAGG: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_APPRAGG: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_APPRAGG: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_APPRAGG: 심화검증 검증 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/src/vl/svc/VL_APPRAMT.pgc b/app/src/vl/svc/VL_APPRAMT.pgc index b897c8d..3cfe567 100644 --- a/app/src/vl/svc/VL_APPRAMT.pgc +++ b/app/src/vl/svc/VL_APPRAMT.pgc @@ -9,6 +9,150 @@ void VL_APPRAMT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_APPRAMT: 요청 수신 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_APPRAMT: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_pid = getl(b, T_PURCHASE_ID); + if (v_pid < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "매입 ID 음수 불가", 0L); + FAIL(b); + } + v_amt = getl(b, T_AMOUNT); + if (v_amt < 0) { + setl(b, T_RC, 94); + Bchg(b, T_MSG, 0, "매입 금액 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + EXEC SQL SELECT merchant_id, amount, fee, net, status + INTO :v_merch, :v_amt, :v_fee, :v_net, :v_pstat + FROM purchase WHERE purchase_id = :v_pid; + if (sqlca.sqlcode == 100) { + v_flag |= 0x01; /* 매입 원거래 미존재 */ + userlog("VL_APPRAMT: 매입 미존재 pid=%ld", v_pid); + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 81); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG3, v_net); + v_base = v_amt; + } + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_APPRAMT' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_APPRAMT: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (룰 매칭) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_APPRAMT: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*) INTO :v_acnt + FROM approval WHERE purchase_id = :v_pid AND approved = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 83); Bchg(b, T_MSG, 0, "승인 대사 오류", 0L); FAIL(b); } + setl(b, T_ARG4, v_acnt); + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_lcnt, :v_lsum + FROM ledger WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 84); Bchg(b, T_MSG, 0, "원장 대사 오류", 0L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :v_scnt, :v_snet + FROM settlement WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 85); Bchg(b, T_MSG, 0, "정산 대사 오류", 0L); FAIL(b); } + if (v_acnt == 0) v_flag |= 0x08; /* 무승인 */ + if (v_lcnt == 0) v_flag |= 0x10; /* 원장 미기록 */ + if (v_scnt > 0 && v_snet != v_net) v_flag |= 0x20; /* 정산액 불일치 */ + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_APPRAMT flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_APPRAMT", v_pid, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_APPRAMT: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_APPRAMT: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_APPRAMT: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_APPRAMT: 심화검증 정상 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; long h_pid, h_amt, h_tot, h_ok; diff --git a/app/src/vl/svc/VL_APPRLATE.pgc b/app/src/vl/svc/VL_APPRLATE.pgc index fed2ffb..4e9b161 100644 --- a/app/src/vl/svc/VL_APPRLATE.pgc +++ b/app/src/vl/svc/VL_APPRLATE.pgc @@ -8,6 +8,143 @@ void VL_APPRLATE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_APPRLATE: 요청 수신 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_APPRLATE: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_pid = getl(b, T_PURCHASE_ID); + if (v_pid < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "매입 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + EXEC SQL SELECT merchant_id, amount, fee, net, status + INTO :v_merch, :v_amt, :v_fee, :v_net, :v_pstat + FROM purchase WHERE purchase_id = :v_pid; + if (sqlca.sqlcode == 100) { + v_flag |= 0x01; /* 매입 원거래 미존재 */ + userlog("VL_APPRLATE: 매입 미존재 pid=%ld", v_pid); + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 81); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG3, v_net); + v_base = v_amt; + } + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_APPRLATE' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_APPRLATE: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (구간 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_APPRLATE: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*) INTO :v_acnt + FROM approval WHERE purchase_id = :v_pid AND approved = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 83); Bchg(b, T_MSG, 0, "승인 대사 오류", 0L); FAIL(b); } + setl(b, T_ARG4, v_acnt); + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_lcnt, :v_lsum + FROM ledger WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 84); Bchg(b, T_MSG, 0, "원장 대사 오류", 0L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :v_scnt, :v_snet + FROM settlement WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 85); Bchg(b, T_MSG, 0, "정산 대사 오류", 0L); FAIL(b); } + if (v_acnt == 0) v_flag |= 0x08; /* 무승인 */ + if (v_lcnt == 0) v_flag |= 0x10; /* 원장 미기록 */ + if (v_scnt > 0 && v_snet != v_net) v_flag |= 0x20; /* 정산액 불일치 */ + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_APPRLATE flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_APPRLATE", v_pid, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_APPRLATE: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_APPRLATE: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_APPRLATE: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_APPRLATE: 심화검증 검증 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0, limit; EXEC SQL BEGIN DECLARE SECTION; long h_pid, h_secs; diff --git a/app/src/vl/svc/VL_APPRNOPUR.pgc b/app/src/vl/svc/VL_APPRNOPUR.pgc index 2fc7dda..abb90ca 100644 --- a/app/src/vl/svc/VL_APPRNOPUR.pgc +++ b/app/src/vl/svc/VL_APPRNOPUR.pgc @@ -8,6 +8,123 @@ void VL_APPRNOPUR(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_APPRNOPUR: 검증 착수 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_APPRNOPUR: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + if (getl(b, T_ID1) < 0) { + setl(b, T_RC, 95); + Bchg(b, T_MSG, 0, "의심거래 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_APPRNOPUR' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_APPRNOPUR: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (업무규칙 적용) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_APPRNOPUR: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_APPRNOPUR flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_APPRNOPUR", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_APPRNOPUR: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_APPRNOPUR: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_APPRNOPUR: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_APPRNOPUR: 심화검증 수행 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; long h_cnt, h_first; diff --git a/app/src/vl/svc/VL_APPR_ACQ.pgc b/app/src/vl/svc/VL_APPR_ACQ.pgc index 4f7337e..e7a8c6e 100644 --- a/app/src/vl/svc/VL_APPR_ACQ.pgc +++ b/app/src/vl/svc/VL_APPR_ACQ.pgc @@ -9,6 +9,144 @@ void VL_APPR_ACQ(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_APPR_ACQ: 처리 개시 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_APPR_ACQ: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_pid = getl(b, T_PURCHASE_ID); + if (v_pid < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "매입 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + EXEC SQL SELECT merchant_id, amount, fee, net, status + INTO :v_merch, :v_amt, :v_fee, :v_net, :v_pstat + FROM purchase WHERE purchase_id = :v_pid; + if (sqlca.sqlcode == 100) { + v_flag |= 0x01; /* 매입 원거래 미존재 */ + userlog("VL_APPR_ACQ: 매입 미존재 pid=%ld", v_pid); + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 81); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG3, v_net); + v_base = v_amt; + } + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_APPR_ACQ' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_APPR_ACQ: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (분기 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_APPR_ACQ: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*) INTO :v_acnt + FROM approval WHERE purchase_id = :v_pid AND approved = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 83); Bchg(b, T_MSG, 0, "승인 대사 오류", 0L); FAIL(b); } + setl(b, T_ARG4, v_acnt); + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_lcnt, :v_lsum + FROM ledger WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 84); Bchg(b, T_MSG, 0, "원장 대사 오류", 0L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :v_scnt, :v_snet + FROM settlement WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 85); Bchg(b, T_MSG, 0, "정산 대사 오류", 0L); FAIL(b); } + if (v_acnt == 0) v_flag |= 0x08; /* 무승인 */ + if (v_lcnt == 0) v_flag |= 0x10; /* 원장 미기록 */ + if (v_scnt > 0 && v_snet != v_net) v_flag |= 0x20; /* 정산액 불일치 */ + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_APPR_ACQ flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_APPR_ACQ", v_pid, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_APPR_ACQ: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_APPR_ACQ: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_APPR_ACQ: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_APPR_ACQ: 심화검증 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; long h_pid, h_cnt; diff --git a/app/src/vl/svc/VL_AXLEDGDC.pgc b/app/src/vl/svc/VL_AXLEDGDC.pgc index 673f3ee..627304a 100644 --- a/app/src/vl/svc/VL_AXLEDGDC.pgc +++ b/app/src/vl/svc/VL_AXLEDGDC.pgc @@ -8,6 +8,119 @@ void VL_AXLEDGDC(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 0; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "LEDGER_MIS"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_AXLEDGDC: 요청 수신 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_AXLEDGDC: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("LEDGER_MIS", &v_thr, v_sev) < 0) { v_thr = 0; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_AXLEDGDC' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_AXLEDGDC: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (분기 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_AXLEDGDC: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_AXLEDGDC flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_AXLEDGDC", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_AXLEDGDC: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_AXLEDGDC: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_AXLEDGDC: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_AXLEDGDC: 심화검증 정상 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/src/vl/svc/VL_AXPURAPPR.pgc b/app/src/vl/svc/VL_AXPURAPPR.pgc index c6e730d..b690f7a 100644 --- a/app/src/vl/svc/VL_AXPURAPPR.pgc +++ b/app/src/vl/svc/VL_AXPURAPPR.pgc @@ -8,6 +8,119 @@ void VL_AXPURAPPR(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_AXPURAPPR: 진입 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_AXPURAPPR: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_AXPURAPPR' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_AXPURAPPR: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (룰 매칭) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_AXPURAPPR: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_AXPURAPPR flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_AXPURAPPR", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_AXPURAPPR: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_AXPURAPPR: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_AXPURAPPR: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_AXPURAPPR: 심화검증 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/src/vl/svc/VL_AXPURSETT.pgc b/app/src/vl/svc/VL_AXPURSETT.pgc index de35b44..d76c740 100644 --- a/app/src/vl/svc/VL_AXPURSETT.pgc +++ b/app/src/vl/svc/VL_AXPURSETT.pgc @@ -8,6 +8,119 @@ void VL_AXPURSETT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_AXPURSETT: 요청 수신 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_AXPURSETT: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_AXPURSETT' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_AXPURSETT: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (업무규칙 적용) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_AXPURSETT: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_AXPURSETT flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_AXPURSETT", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_AXPURSETT: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_AXPURSETT: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_AXPURSETT: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_AXPURSETT: 심화검증 처리 종결 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/src/vl/svc/VL_AXSETTPAY.pgc b/app/src/vl/svc/VL_AXSETTPAY.pgc index 5d98c19..23edb4c 100644 --- a/app/src/vl/svc/VL_AXSETTPAY.pgc +++ b/app/src/vl/svc/VL_AXSETTPAY.pgc @@ -8,6 +8,119 @@ void VL_AXSETTPAY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_AXSETTPAY: 진입 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_AXSETTPAY: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_AXSETTPAY' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_AXSETTPAY: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (조건 분기) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_AXSETTPAY: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_AXSETTPAY flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_AXSETTPAY", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_AXSETTPAY: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_AXSETTPAY: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_AXSETTPAY: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_AXSETTPAY: 심화검증 정상 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/src/vl/svc/VL_BALANCE.pgc b/app/src/vl/svc/VL_BALANCE.pgc index 37cc717..0285457 100644 --- a/app/src/vl/svc/VL_BALANCE.pgc +++ b/app/src/vl/svc/VL_BALANCE.pgc @@ -9,6 +9,143 @@ void VL_BALANCE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_BALANCE: 검증 착수 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_BALANCE: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + gets_(b, T_MERCHANT, v_merch, sizeof(v_merch)); + if (v_merch[0] && strlen(v_merch) > 32) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (v_merch[0]) { + EXEC SQL SELECT status, mdr_bps, daily_limit + INTO :v_status, :v_bps, :v_lim + FROM merchant WHERE merchant_id = :v_merch; + if (sqlca.sqlcode == 100) { + v_flag |= 0x02; /* 가맹점 마스터 미존재 */ + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 82); Bchg(b, T_MSG, 0, "가맹점 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG7, v_bps); + if (strcmp(v_status, "ACTIVE") != 0) v_flag |= 0x04; /* 상태부적합 */ + } + } + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_BALANCE' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_BALANCE: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (구간 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_BALANCE: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + if (v_merch[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0) + INTO :v_pcnt, :v_pgross, :v_pfee + FROM purchase + WHERE merchant_id = :v_merch AND biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 86); Bchg(b, T_MSG, 0, "매입 재집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_pcnt); + setl(b, T_AMT3, v_pgross); + v_base = v_pgross; + } + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_BALANCE flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_BALANCE", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_BALANCE: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_BALANCE: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_BALANCE: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_BALANCE: 심화검증 검증 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ char merch[64], bizdate[16]; long rc = 0; EXEC SQL BEGIN DECLARE SECTION; diff --git a/app/src/vl/svc/VL_BATCH_CHK.pgc b/app/src/vl/svc/VL_BATCH_CHK.pgc index 5a93525..94f0d94 100644 --- a/app/src/vl/svc/VL_BATCH_CHK.pgc +++ b/app/src/vl/svc/VL_BATCH_CHK.pgc @@ -9,6 +9,119 @@ void VL_BATCH_CHK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_BATCH_CHK: 진입 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_BATCH_CHK: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_BATCH_CHK' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_BATCH_CHK: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (분기 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_BATCH_CHK: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_BATCH_CHK flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_BATCH_CHK", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_BATCH_CHK: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_BATCH_CHK: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_BATCH_CHK: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_BATCH_CHK: 심화검증 정상 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ char bizdate[16]; long rc = 0; EXEC SQL BEGIN DECLARE SECTION; diff --git a/app/src/vl/svc/VL_BIN.pgc b/app/src/vl/svc/VL_BIN.pgc index 7993e87..79fbbba 100644 --- a/app/src/vl/svc/VL_BIN.pgc +++ b/app/src/vl/svc/VL_BIN.pgc @@ -9,6 +9,118 @@ void VL_BIN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_BIN: 처리 개시 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_BIN: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_BIN' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_BIN: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (구간 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_BIN: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_BIN flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_BIN", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_BIN: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_BIN: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_BIN: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_BIN: 심화검증 수행 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ char bin[16]; long rc = 0; EXEC SQL BEGIN DECLARE SECTION; diff --git a/app/src/vl/svc/VL_CANCELMIS.pgc b/app/src/vl/svc/VL_CANCELMIS.pgc index 6202a22..83fca63 100644 --- a/app/src/vl/svc/VL_CANCELMIS.pgc +++ b/app/src/vl/svc/VL_CANCELMIS.pgc @@ -8,6 +8,150 @@ void VL_CANCELMIS(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_CANCELMIS: 수행 시작 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_CANCELMIS: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_pid = getl(b, T_PURCHASE_ID); + if (v_pid < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "매입 ID 음수 불가", 0L); + FAIL(b); + } + v_amt = getl(b, T_AMOUNT); + if (v_amt < 0) { + setl(b, T_RC, 94); + Bchg(b, T_MSG, 0, "매입 금액 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + EXEC SQL SELECT merchant_id, amount, fee, net, status + INTO :v_merch, :v_amt, :v_fee, :v_net, :v_pstat + FROM purchase WHERE purchase_id = :v_pid; + if (sqlca.sqlcode == 100) { + v_flag |= 0x01; /* 매입 원거래 미존재 */ + userlog("VL_CANCELMIS: 매입 미존재 pid=%ld", v_pid); + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 81); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG3, v_net); + v_base = v_amt; + } + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_CANCELMIS' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_CANCELMIS: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (구간 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_CANCELMIS: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*) INTO :v_acnt + FROM approval WHERE purchase_id = :v_pid AND approved = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 83); Bchg(b, T_MSG, 0, "승인 대사 오류", 0L); FAIL(b); } + setl(b, T_ARG4, v_acnt); + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_lcnt, :v_lsum + FROM ledger WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 84); Bchg(b, T_MSG, 0, "원장 대사 오류", 0L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :v_scnt, :v_snet + FROM settlement WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 85); Bchg(b, T_MSG, 0, "정산 대사 오류", 0L); FAIL(b); } + if (v_acnt == 0) v_flag |= 0x08; /* 무승인 */ + if (v_lcnt == 0) v_flag |= 0x10; /* 원장 미기록 */ + if (v_scnt > 0 && v_snet != v_net) v_flag |= 0x20; /* 정산액 불일치 */ + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_CANCELMIS flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_CANCELMIS", v_pid, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_CANCELMIS: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_CANCELMIS: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_CANCELMIS: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_CANCELMIS: 심화검증 처리 종결 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; long h_pid, h_amt, h_csum, h_ccnt; diff --git a/app/src/vl/svc/VL_CANCELRATE.pgc b/app/src/vl/svc/VL_CANCELRATE.pgc index 09e1060..ebf52ee 100644 --- a/app/src/vl/svc/VL_CANCELRATE.pgc +++ b/app/src/vl/svc/VL_CANCELRATE.pgc @@ -8,6 +8,118 @@ void VL_CANCELRATE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_CANCELRATE: 요청 수신 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_CANCELRATE: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_CANCELRATE' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_CANCELRATE: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (구간 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_CANCELRATE: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_CANCELRATE flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_CANCELRATE", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_CANCELRATE: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_CANCELRATE: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_CANCELRATE: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_CANCELRATE: 심화검증 검증 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0, pct, thr; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/src/vl/svc/VL_CARD.pgc b/app/src/vl/svc/VL_CARD.pgc index fdf74b7..1ba21df 100644 --- a/app/src/vl/svc/VL_CARD.pgc +++ b/app/src/vl/svc/VL_CARD.pgc @@ -9,6 +9,118 @@ void VL_CARD(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_CARD: 처리 개시 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_CARD: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_CARD' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_CARD: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (조건 분기) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_CARD: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_CARD flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_CARD", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_CARD: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_CARD: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_CARD: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_CARD: 심화검증 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ char pan[32]; long rc; gets_(b, T_STR1, pan, sizeof(pan)); diff --git a/app/src/vl/svc/VL_CATSKEW.pgc b/app/src/vl/svc/VL_CATSKEW.pgc index c6a69bb..2059db8 100644 --- a/app/src/vl/svc/VL_CATSKEW.pgc +++ b/app/src/vl/svc/VL_CATSKEW.pgc @@ -8,6 +8,118 @@ void VL_CATSKEW(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_CATSKEW: 수행 시작 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_CATSKEW: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_CATSKEW' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_CATSKEW: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (분기 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_CATSKEW: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_CATSKEW flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_CATSKEW", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_CATSKEW: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_CATSKEW: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_CATSKEW: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_CATSKEW: 심화검증 정상 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0, pct, thr; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/src/vl/svc/VL_CHANSPLIT.pgc b/app/src/vl/svc/VL_CHANSPLIT.pgc index c3b1563..491bd19 100644 --- a/app/src/vl/svc/VL_CHANSPLIT.pgc +++ b/app/src/vl/svc/VL_CHANSPLIT.pgc @@ -8,6 +8,118 @@ void VL_CHANSPLIT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_CHANSPLIT: 진입 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_CHANSPLIT: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_CHANSPLIT' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_CHANSPLIT: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (조건 분기) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_CHANSPLIT: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_CHANSPLIT flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_CHANSPLIT", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_CHANSPLIT: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_CHANSPLIT: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_CHANSPLIT: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_CHANSPLIT: 심화검증 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0, pct, thr; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/src/vl/svc/VL_CODEVAL.pgc b/app/src/vl/svc/VL_CODEVAL.pgc index 89c7549..80b51a0 100644 --- a/app/src/vl/svc/VL_CODEVAL.pgc +++ b/app/src/vl/svc/VL_CODEVAL.pgc @@ -9,6 +9,120 @@ void VL_CODEVAL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_CODEVAL: 진입 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_CODEVAL: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + { char vk_rule[32]; gets_(b, T_STR3, vk_rule, sizeof(vk_rule)); + if (vk_rule[0]) { strncpy(v_rule, vk_rule, sizeof(v_rule)-1); v_rule[sizeof(v_rule)-1]=0; } } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule(v_rule, &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_CODEVAL' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_CODEVAL: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (업무규칙 적용) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_CODEVAL: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_CODEVAL flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_CODEVAL", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_CODEVAL: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_CODEVAL: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_CODEVAL: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_CODEVAL: 심화검증 검증 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ char grp[32], code[32]; long rc = 0; EXEC SQL BEGIN DECLARE SECTION; diff --git a/app/src/vl/svc/VL_CONFIRM.pgc b/app/src/vl/svc/VL_CONFIRM.pgc index 2b36cbf..3d16223 100644 --- a/app/src/vl/svc/VL_CONFIRM.pgc +++ b/app/src/vl/svc/VL_CONFIRM.pgc @@ -9,6 +9,123 @@ void VL_CONFIRM(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_CONFIRM: 검증 착수 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_CONFIRM: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + if (getl(b, T_ID1) < 0) { + setl(b, T_RC, 95); + Bchg(b, T_MSG, 0, "의심거래 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_CONFIRM' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_CONFIRM: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (룰 매칭) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_CONFIRM: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_CONFIRM flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_CONFIRM", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_CONFIRM: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_CONFIRM: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_CONFIRM: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_CONFIRM: 심화검증 검증 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ EXEC SQL BEGIN DECLARE SECTION; long h_sid; EXEC SQL END DECLARE SECTION; diff --git a/app/src/vl/svc/VL_CROSSCHK.pgc b/app/src/vl/svc/VL_CROSSCHK.pgc index 7ac8936..9c5e7fa 100644 --- a/app/src/vl/svc/VL_CROSSCHK.pgc +++ b/app/src/vl/svc/VL_CROSSCHK.pgc @@ -9,6 +9,144 @@ void VL_CROSSCHK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_CROSSCHK: 진입 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_CROSSCHK: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_pid = getl(b, T_PURCHASE_ID); + if (v_pid < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "매입 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + EXEC SQL SELECT merchant_id, amount, fee, net, status + INTO :v_merch, :v_amt, :v_fee, :v_net, :v_pstat + FROM purchase WHERE purchase_id = :v_pid; + if (sqlca.sqlcode == 100) { + v_flag |= 0x01; /* 매입 원거래 미존재 */ + userlog("VL_CROSSCHK: 매입 미존재 pid=%ld", v_pid); + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 81); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG3, v_net); + v_base = v_amt; + } + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_CROSSCHK' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_CROSSCHK: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (룰 매칭) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_CROSSCHK: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*) INTO :v_acnt + FROM approval WHERE purchase_id = :v_pid AND approved = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 83); Bchg(b, T_MSG, 0, "승인 대사 오류", 0L); FAIL(b); } + setl(b, T_ARG4, v_acnt); + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_lcnt, :v_lsum + FROM ledger WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 84); Bchg(b, T_MSG, 0, "원장 대사 오류", 0L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :v_scnt, :v_snet + FROM settlement WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 85); Bchg(b, T_MSG, 0, "정산 대사 오류", 0L); FAIL(b); } + if (v_acnt == 0) v_flag |= 0x08; /* 무승인 */ + if (v_lcnt == 0) v_flag |= 0x10; /* 원장 미기록 */ + if (v_scnt > 0 && v_snet != v_net) v_flag |= 0x20; /* 정산액 불일치 */ + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_CROSSCHK flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_CROSSCHK", v_pid, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_CROSSCHK: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_CROSSCHK: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_CROSSCHK: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_CROSSCHK: 심화검증 검증 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; long h_pid, h_pcnt, h_mcnt, h_acnt; diff --git a/app/src/vl/svc/VL_DATE.pgc b/app/src/vl/svc/VL_DATE.pgc index 3859d9f..1e67b37 100644 --- a/app/src/vl/svc/VL_DATE.pgc +++ b/app/src/vl/svc/VL_DATE.pgc @@ -9,6 +9,119 @@ void VL_DATE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_DATE: 진입 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_DATE: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_DATE' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_DATE: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (분기 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_DATE: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_DATE flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_DATE", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_DATE: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_DATE: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_DATE: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_DATE: 심화검증 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ char bizdate[16]; long rc = 0; EXEC SQL BEGIN DECLARE SECTION; diff --git a/app/src/vl/svc/VL_DATEFUT.pgc b/app/src/vl/svc/VL_DATEFUT.pgc index de4f22a..d65f387 100644 --- a/app/src/vl/svc/VL_DATEFUT.pgc +++ b/app/src/vl/svc/VL_DATEFUT.pgc @@ -8,6 +8,123 @@ void VL_DATEFUT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_DATEFUT: 요청 수신 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_DATEFUT: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + if (getl(b, T_ID1) < 0) { + setl(b, T_RC, 95); + Bchg(b, T_MSG, 0, "의심거래 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_DATEFUT' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_DATEFUT: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (룰 매칭) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_DATEFUT: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_DATEFUT flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_DATEFUT", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_DATEFUT: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_DATEFUT: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_DATEFUT: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_DATEFUT: 심화검증 처리 종결 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; long h_cnt, h_first; diff --git a/app/src/vl/svc/VL_DATEGAP.pgc b/app/src/vl/svc/VL_DATEGAP.pgc index d9ba658..7f5d52d 100644 --- a/app/src/vl/svc/VL_DATEGAP.pgc +++ b/app/src/vl/svc/VL_DATEGAP.pgc @@ -8,6 +8,118 @@ void VL_DATEGAP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_DATEGAP: 검증 착수 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_DATEGAP: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_DATEGAP' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_DATEGAP: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (룰 매칭) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_DATEGAP: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_DATEGAP flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_DATEGAP", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_DATEGAP: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_DATEGAP: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_DATEGAP: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_DATEGAP: 심화검증 검증 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; long h_cnt, h_first; diff --git a/app/src/vl/svc/VL_DATEREV.pgc b/app/src/vl/svc/VL_DATEREV.pgc index 3ba2a21..ced5771 100644 --- a/app/src/vl/svc/VL_DATEREV.pgc +++ b/app/src/vl/svc/VL_DATEREV.pgc @@ -8,6 +8,144 @@ void VL_DATEREV(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_DATEREV: 처리 개시 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_DATEREV: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_pid = getl(b, T_PURCHASE_ID); + if (v_pid < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "매입 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + EXEC SQL SELECT merchant_id, amount, fee, net, status + INTO :v_merch, :v_amt, :v_fee, :v_net, :v_pstat + FROM purchase WHERE purchase_id = :v_pid; + if (sqlca.sqlcode == 100) { + v_flag |= 0x01; /* 매입 원거래 미존재 */ + userlog("VL_DATEREV: 매입 미존재 pid=%ld", v_pid); + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 81); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG3, v_net); + v_base = v_amt; + } + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_DATEREV' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_DATEREV: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (룰 매칭) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_DATEREV: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*) INTO :v_acnt + FROM approval WHERE purchase_id = :v_pid AND approved = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 83); Bchg(b, T_MSG, 0, "승인 대사 오류", 0L); FAIL(b); } + setl(b, T_ARG4, v_acnt); + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_lcnt, :v_lsum + FROM ledger WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 84); Bchg(b, T_MSG, 0, "원장 대사 오류", 0L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :v_scnt, :v_snet + FROM settlement WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 85); Bchg(b, T_MSG, 0, "정산 대사 오류", 0L); FAIL(b); } + if (v_acnt == 0) v_flag |= 0x08; /* 무승인 */ + if (v_lcnt == 0) v_flag |= 0x10; /* 원장 미기록 */ + if (v_scnt > 0 && v_snet != v_net) v_flag |= 0x20; /* 정산액 불일치 */ + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_DATEREV flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_DATEREV", v_pid, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_DATEREV: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_DATEREV: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_DATEREV: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_DATEREV: 심화검증 검증 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; long h_pid, h_exists, h_rev; diff --git a/app/src/vl/svc/VL_DAYLIMIT.pgc b/app/src/vl/svc/VL_DAYLIMIT.pgc index ff39588..58f5eb8 100644 --- a/app/src/vl/svc/VL_DAYLIMIT.pgc +++ b/app/src/vl/svc/VL_DAYLIMIT.pgc @@ -8,6 +8,143 @@ void VL_DAYLIMIT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 0; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "LIMIT_OVR"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_DAYLIMIT: 요청 수신 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_DAYLIMIT: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + gets_(b, T_MERCHANT, v_merch, sizeof(v_merch)); + if (v_merch[0] && strlen(v_merch) > 32) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (v_merch[0]) { + EXEC SQL SELECT status, mdr_bps, daily_limit + INTO :v_status, :v_bps, :v_lim + FROM merchant WHERE merchant_id = :v_merch; + if (sqlca.sqlcode == 100) { + v_flag |= 0x02; /* 가맹점 마스터 미존재 */ + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 82); Bchg(b, T_MSG, 0, "가맹점 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG7, v_bps); + if (strcmp(v_status, "ACTIVE") != 0) v_flag |= 0x04; /* 상태부적합 */ + } + } + if (vldb_get_rule("LIMIT_OVR", &v_thr, v_sev) < 0) { v_thr = 0; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_DAYLIMIT' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_DAYLIMIT: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (구간 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_DAYLIMIT: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + if (v_merch[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0) + INTO :v_pcnt, :v_pgross, :v_pfee + FROM purchase + WHERE merchant_id = :v_merch AND biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 86); Bchg(b, T_MSG, 0, "매입 재집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_pcnt); + setl(b, T_AMT3, v_pgross); + v_base = v_pgross; + } + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_DAYLIMIT flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_DAYLIMIT", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_DAYLIMIT: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_DAYLIMIT: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_DAYLIMIT: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_DAYLIMIT: 심화검증 처리 종결 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; char h_merch[64], h_bizdate[16]; diff --git a/app/src/vl/svc/VL_DUP.pgc b/app/src/vl/svc/VL_DUP.pgc index d909c18..9556587 100644 --- a/app/src/vl/svc/VL_DUP.pgc +++ b/app/src/vl/svc/VL_DUP.pgc @@ -9,6 +9,149 @@ void VL_DUP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 1; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "DUP_TXN"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_DUP: 진입 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_DUP: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + gets_(b, T_MERCHANT, v_merch, sizeof(v_merch)); + if (v_merch[0] && strlen(v_merch) > 32) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); + FAIL(b); + } + v_amt = getl(b, T_AMOUNT); + if (v_amt < 0) { + setl(b, T_RC, 94); + Bchg(b, T_MSG, 0, "매입 금액 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (v_merch[0]) { + EXEC SQL SELECT status, mdr_bps, daily_limit + INTO :v_status, :v_bps, :v_lim + FROM merchant WHERE merchant_id = :v_merch; + if (sqlca.sqlcode == 100) { + v_flag |= 0x02; /* 가맹점 마스터 미존재 */ + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 82); Bchg(b, T_MSG, 0, "가맹점 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG7, v_bps); + if (strcmp(v_status, "ACTIVE") != 0) v_flag |= 0x04; /* 상태부적합 */ + } + } + if (vldb_get_rule("DUP_TXN", &v_thr, v_sev) < 0) { v_thr = 1; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_DUP' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_DUP: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (구간 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_DUP: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + if (v_merch[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0) + INTO :v_pcnt, :v_pgross, :v_pfee + FROM purchase + WHERE merchant_id = :v_merch AND biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 86); Bchg(b, T_MSG, 0, "매입 재집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_pcnt); + setl(b, T_AMT3, v_pgross); + v_base = v_pgross; + } + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_DUP flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_DUP", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_DUP: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_DUP: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_DUP: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_DUP: 심화검증 정상 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ char merch[64], bizdate[16]; long rc = 0; EXEC SQL BEGIN DECLARE SECTION; diff --git a/app/src/vl/svc/VL_DUPAPPR.pgc b/app/src/vl/svc/VL_DUPAPPR.pgc index f90a1e0..0744aae 100644 --- a/app/src/vl/svc/VL_DUPAPPR.pgc +++ b/app/src/vl/svc/VL_DUPAPPR.pgc @@ -8,6 +8,145 @@ void VL_DUPAPPR(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 1; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "DUP_TXN"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_DUPAPPR: 검증 착수 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_DUPAPPR: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_pid = getl(b, T_PURCHASE_ID); + if (v_pid < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "매입 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + EXEC SQL SELECT merchant_id, amount, fee, net, status + INTO :v_merch, :v_amt, :v_fee, :v_net, :v_pstat + FROM purchase WHERE purchase_id = :v_pid; + if (sqlca.sqlcode == 100) { + v_flag |= 0x01; /* 매입 원거래 미존재 */ + userlog("VL_DUPAPPR: 매입 미존재 pid=%ld", v_pid); + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 81); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG3, v_net); + v_base = v_amt; + } + if (vldb_get_rule("DUP_TXN", &v_thr, v_sev) < 0) { v_thr = 1; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_DUPAPPR' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_DUPAPPR: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (업무규칙 적용) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_DUPAPPR: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*) INTO :v_acnt + FROM approval WHERE purchase_id = :v_pid AND approved = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 83); Bchg(b, T_MSG, 0, "승인 대사 오류", 0L); FAIL(b); } + setl(b, T_ARG4, v_acnt); + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_lcnt, :v_lsum + FROM ledger WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 84); Bchg(b, T_MSG, 0, "원장 대사 오류", 0L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :v_scnt, :v_snet + FROM settlement WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 85); Bchg(b, T_MSG, 0, "정산 대사 오류", 0L); FAIL(b); } + if (v_acnt == 0) v_flag |= 0x08; /* 무승인 */ + if (v_lcnt == 0) v_flag |= 0x10; /* 원장 미기록 */ + if (v_scnt > 0 && v_snet != v_net) v_flag |= 0x20; /* 정산액 불일치 */ + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_DUPAPPR flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_DUPAPPR", v_pid, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_DUPAPPR: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_DUPAPPR: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_DUPAPPR: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_DUPAPPR: 심화검증 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; char bizdate[16]; EXEC SQL BEGIN DECLARE SECTION; diff --git a/app/src/vl/svc/VL_DUPAUTH.pgc b/app/src/vl/svc/VL_DUPAUTH.pgc index 1717687..9f62fee 100644 --- a/app/src/vl/svc/VL_DUPAUTH.pgc +++ b/app/src/vl/svc/VL_DUPAUTH.pgc @@ -9,6 +9,144 @@ void VL_DUPAUTH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 1; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "DUP_TXN"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_DUPAUTH: 진입 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_DUPAUTH: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_pid = getl(b, T_PURCHASE_ID); + if (v_pid < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "매입 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + EXEC SQL SELECT merchant_id, amount, fee, net, status + INTO :v_merch, :v_amt, :v_fee, :v_net, :v_pstat + FROM purchase WHERE purchase_id = :v_pid; + if (sqlca.sqlcode == 100) { + v_flag |= 0x01; /* 매입 원거래 미존재 */ + userlog("VL_DUPAUTH: 매입 미존재 pid=%ld", v_pid); + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 81); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG3, v_net); + v_base = v_amt; + } + if (vldb_get_rule("DUP_TXN", &v_thr, v_sev) < 0) { v_thr = 1; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_DUPAUTH' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_DUPAUTH: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (조건 분기) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_DUPAUTH: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*) INTO :v_acnt + FROM approval WHERE purchase_id = :v_pid AND approved = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 83); Bchg(b, T_MSG, 0, "승인 대사 오류", 0L); FAIL(b); } + setl(b, T_ARG4, v_acnt); + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_lcnt, :v_lsum + FROM ledger WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 84); Bchg(b, T_MSG, 0, "원장 대사 오류", 0L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :v_scnt, :v_snet + FROM settlement WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 85); Bchg(b, T_MSG, 0, "정산 대사 오류", 0L); FAIL(b); } + if (v_acnt == 0) v_flag |= 0x08; /* 무승인 */ + if (v_lcnt == 0) v_flag |= 0x10; /* 원장 미기록 */ + if (v_scnt > 0 && v_snet != v_net) v_flag |= 0x20; /* 정산액 불일치 */ + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_DUPAUTH flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_DUPAUTH", v_pid, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_DUPAUTH: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_DUPAUTH: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_DUPAUTH: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_DUPAUTH: 심화검증 정상 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; long h_pid, h_cnt; diff --git a/app/src/vl/svc/VL_DUPLEDGR.pgc b/app/src/vl/svc/VL_DUPLEDGR.pgc index 54f948b..f2f0599 100644 --- a/app/src/vl/svc/VL_DUPLEDGR.pgc +++ b/app/src/vl/svc/VL_DUPLEDGR.pgc @@ -8,6 +8,144 @@ void VL_DUPLEDGR(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 1; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "DUP_TXN"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_DUPLEDGR: 진입 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_DUPLEDGR: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_pid = getl(b, T_PURCHASE_ID); + if (v_pid < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "매입 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + EXEC SQL SELECT merchant_id, amount, fee, net, status + INTO :v_merch, :v_amt, :v_fee, :v_net, :v_pstat + FROM purchase WHERE purchase_id = :v_pid; + if (sqlca.sqlcode == 100) { + v_flag |= 0x01; /* 매입 원거래 미존재 */ + userlog("VL_DUPLEDGR: 매입 미존재 pid=%ld", v_pid); + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 81); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG3, v_net); + v_base = v_amt; + } + if (vldb_get_rule("DUP_TXN", &v_thr, v_sev) < 0) { v_thr = 1; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_DUPLEDGR' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_DUPLEDGR: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (룰 매칭) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_DUPLEDGR: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*) INTO :v_acnt + FROM approval WHERE purchase_id = :v_pid AND approved = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 83); Bchg(b, T_MSG, 0, "승인 대사 오류", 0L); FAIL(b); } + setl(b, T_ARG4, v_acnt); + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_lcnt, :v_lsum + FROM ledger WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 84); Bchg(b, T_MSG, 0, "원장 대사 오류", 0L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :v_scnt, :v_snet + FROM settlement WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 85); Bchg(b, T_MSG, 0, "정산 대사 오류", 0L); FAIL(b); } + if (v_acnt == 0) v_flag |= 0x08; /* 무승인 */ + if (v_lcnt == 0) v_flag |= 0x10; /* 원장 미기록 */ + if (v_scnt > 0 && v_snet != v_net) v_flag |= 0x20; /* 정산액 불일치 */ + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_DUPLEDGR flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_DUPLEDGR", v_pid, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_DUPLEDGR: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_DUPLEDGR: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_DUPLEDGR: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_DUPLEDGR: 심화검증 수행 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; long h_pid, h_maxdup; diff --git a/app/src/vl/svc/VL_DUPPAY.pgc b/app/src/vl/svc/VL_DUPPAY.pgc index 0393397..ec2ebe2 100644 --- a/app/src/vl/svc/VL_DUPPAY.pgc +++ b/app/src/vl/svc/VL_DUPPAY.pgc @@ -8,6 +8,145 @@ void VL_DUPPAY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 1; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "DUP_TXN"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_DUPPAY: 처리 개시 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_DUPPAY: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_pid = getl(b, T_PURCHASE_ID); + if (v_pid < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "매입 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + EXEC SQL SELECT merchant_id, amount, fee, net, status + INTO :v_merch, :v_amt, :v_fee, :v_net, :v_pstat + FROM purchase WHERE purchase_id = :v_pid; + if (sqlca.sqlcode == 100) { + v_flag |= 0x01; /* 매입 원거래 미존재 */ + userlog("VL_DUPPAY: 매입 미존재 pid=%ld", v_pid); + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 81); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG3, v_net); + v_base = v_amt; + } + if (vldb_get_rule("DUP_TXN", &v_thr, v_sev) < 0) { v_thr = 1; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_DUPPAY' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_DUPPAY: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (조건 분기) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_DUPPAY: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*) INTO :v_acnt + FROM approval WHERE purchase_id = :v_pid AND approved = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 83); Bchg(b, T_MSG, 0, "승인 대사 오류", 0L); FAIL(b); } + setl(b, T_ARG4, v_acnt); + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_lcnt, :v_lsum + FROM ledger WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 84); Bchg(b, T_MSG, 0, "원장 대사 오류", 0L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :v_scnt, :v_snet + FROM settlement WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 85); Bchg(b, T_MSG, 0, "정산 대사 오류", 0L); FAIL(b); } + if (v_acnt == 0) v_flag |= 0x08; /* 무승인 */ + if (v_lcnt == 0) v_flag |= 0x10; /* 원장 미기록 */ + if (v_scnt > 0 && v_snet != v_net) v_flag |= 0x20; /* 정산액 불일치 */ + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_DUPPAY flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_DUPPAY", v_pid, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_DUPPAY: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_DUPPAY: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_DUPPAY: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_DUPPAY: 심화검증 수행 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; char bizdate[16]; EXEC SQL BEGIN DECLARE SECTION; diff --git a/app/src/vl/svc/VL_DUPPAYDAY.pgc b/app/src/vl/svc/VL_DUPPAYDAY.pgc index d9b5636..044cb2d 100644 --- a/app/src/vl/svc/VL_DUPPAYDAY.pgc +++ b/app/src/vl/svc/VL_DUPPAYDAY.pgc @@ -8,6 +8,119 @@ void VL_DUPPAYDAY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 1; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "DUP_TXN"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_DUPPAYDAY: 요청 수신 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_DUPPAYDAY: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("DUP_TXN", &v_thr, v_sev) < 0) { v_thr = 1; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_DUPPAYDAY' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_DUPPAYDAY: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (분기 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_DUPPAYDAY: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_DUPPAYDAY flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_DUPPAYDAY", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_DUPPAYDAY: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_DUPPAYDAY: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_DUPPAYDAY: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_DUPPAYDAY: 심화검증 처리 종결 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/src/vl/svc/VL_DUPPURCH.pgc b/app/src/vl/svc/VL_DUPPURCH.pgc index 98fad9e..ef47a70 100644 --- a/app/src/vl/svc/VL_DUPPURCH.pgc +++ b/app/src/vl/svc/VL_DUPPURCH.pgc @@ -8,6 +8,144 @@ void VL_DUPPURCH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 1; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "DUP_TXN"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_DUPPURCH: 진입 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_DUPPURCH: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_pid = getl(b, T_PURCHASE_ID); + if (v_pid < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "매입 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + EXEC SQL SELECT merchant_id, amount, fee, net, status + INTO :v_merch, :v_amt, :v_fee, :v_net, :v_pstat + FROM purchase WHERE purchase_id = :v_pid; + if (sqlca.sqlcode == 100) { + v_flag |= 0x01; /* 매입 원거래 미존재 */ + userlog("VL_DUPPURCH: 매입 미존재 pid=%ld", v_pid); + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 81); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG3, v_net); + v_base = v_amt; + } + if (vldb_get_rule("DUP_TXN", &v_thr, v_sev) < 0) { v_thr = 1; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_DUPPURCH' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_DUPPURCH: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (구간 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_DUPPURCH: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*) INTO :v_acnt + FROM approval WHERE purchase_id = :v_pid AND approved = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 83); Bchg(b, T_MSG, 0, "승인 대사 오류", 0L); FAIL(b); } + setl(b, T_ARG4, v_acnt); + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_lcnt, :v_lsum + FROM ledger WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 84); Bchg(b, T_MSG, 0, "원장 대사 오류", 0L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :v_scnt, :v_snet + FROM settlement WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 85); Bchg(b, T_MSG, 0, "정산 대사 오류", 0L); FAIL(b); } + if (v_acnt == 0) v_flag |= 0x08; /* 무승인 */ + if (v_lcnt == 0) v_flag |= 0x10; /* 원장 미기록 */ + if (v_scnt > 0 && v_snet != v_net) v_flag |= 0x20; /* 정산액 불일치 */ + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_DUPPURCH flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_DUPPURCH", v_pid, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_DUPPURCH: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_DUPPURCH: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_DUPPURCH: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_DUPPURCH: 심화검증 정상 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; long h_pid, h_dups; diff --git a/app/src/vl/svc/VL_DUPSETTLE.pgc b/app/src/vl/svc/VL_DUPSETTLE.pgc index 688d0c3..40fec6f 100644 --- a/app/src/vl/svc/VL_DUPSETTLE.pgc +++ b/app/src/vl/svc/VL_DUPSETTLE.pgc @@ -8,6 +8,149 @@ void VL_DUPSETTLE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 1; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "DUP_TXN"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_DUPSETTLE: 처리 개시 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_DUPSETTLE: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_pid = getl(b, T_PURCHASE_ID); + if (v_pid < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "매입 ID 음수 불가", 0L); + FAIL(b); + } + if (getl(b, T_ID1) < 0) { + setl(b, T_RC, 95); + Bchg(b, T_MSG, 0, "의심거래 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + EXEC SQL SELECT merchant_id, amount, fee, net, status + INTO :v_merch, :v_amt, :v_fee, :v_net, :v_pstat + FROM purchase WHERE purchase_id = :v_pid; + if (sqlca.sqlcode == 100) { + v_flag |= 0x01; /* 매입 원거래 미존재 */ + userlog("VL_DUPSETTLE: 매입 미존재 pid=%ld", v_pid); + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 81); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG3, v_net); + v_base = v_amt; + } + if (vldb_get_rule("DUP_TXN", &v_thr, v_sev) < 0) { v_thr = 1; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_DUPSETTLE' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_DUPSETTLE: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (룰 매칭) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_DUPSETTLE: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*) INTO :v_acnt + FROM approval WHERE purchase_id = :v_pid AND approved = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 83); Bchg(b, T_MSG, 0, "승인 대사 오류", 0L); FAIL(b); } + setl(b, T_ARG4, v_acnt); + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_lcnt, :v_lsum + FROM ledger WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 84); Bchg(b, T_MSG, 0, "원장 대사 오류", 0L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :v_scnt, :v_snet + FROM settlement WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 85); Bchg(b, T_MSG, 0, "정산 대사 오류", 0L); FAIL(b); } + if (v_acnt == 0) v_flag |= 0x08; /* 무승인 */ + if (v_lcnt == 0) v_flag |= 0x10; /* 원장 미기록 */ + if (v_scnt > 0 && v_snet != v_net) v_flag |= 0x20; /* 정산액 불일치 */ + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_DUPSETTLE flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_DUPSETTLE", v_pid, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_DUPSETTLE: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_DUPSETTLE: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_DUPSETTLE: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_DUPSETTLE: 심화검증 처리 종결 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; long h_pid, h_cnt, h_net, h_min, h_max; diff --git a/app/src/vl/svc/VL_DUPSETTMR.pgc b/app/src/vl/svc/VL_DUPSETTMR.pgc index 74de9db..92169be 100644 --- a/app/src/vl/svc/VL_DUPSETTMR.pgc +++ b/app/src/vl/svc/VL_DUPSETTMR.pgc @@ -8,6 +8,143 @@ void VL_DUPSETTMR(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 1; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "DUP_TXN"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_DUPSETTMR: 처리 개시 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_DUPSETTMR: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + gets_(b, T_MERCHANT, v_merch, sizeof(v_merch)); + if (v_merch[0] && strlen(v_merch) > 32) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (v_merch[0]) { + EXEC SQL SELECT status, mdr_bps, daily_limit + INTO :v_status, :v_bps, :v_lim + FROM merchant WHERE merchant_id = :v_merch; + if (sqlca.sqlcode == 100) { + v_flag |= 0x02; /* 가맹점 마스터 미존재 */ + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 82); Bchg(b, T_MSG, 0, "가맹점 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG7, v_bps); + if (strcmp(v_status, "ACTIVE") != 0) v_flag |= 0x04; /* 상태부적합 */ + } + } + if (vldb_get_rule("DUP_TXN", &v_thr, v_sev) < 0) { v_thr = 1; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_DUPSETTMR' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_DUPSETTMR: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (구간 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_DUPSETTMR: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + if (v_merch[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0) + INTO :v_pcnt, :v_pgross, :v_pfee + FROM purchase + WHERE merchant_id = :v_merch AND biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 86); Bchg(b, T_MSG, 0, "매입 재집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_pcnt); + setl(b, T_AMT3, v_pgross); + v_base = v_pgross; + } + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_DUPSETTMR flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_DUPSETTMR", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_DUPSETTMR: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_DUPSETTMR: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_DUPSETTMR: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_DUPSETTMR: 심화검증 수행 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; char h_merch[64], h_bizdate[16]; diff --git a/app/src/vl/svc/VL_EXPIRY.pgc b/app/src/vl/svc/VL_EXPIRY.pgc index 6ee3903..48162af 100644 --- a/app/src/vl/svc/VL_EXPIRY.pgc +++ b/app/src/vl/svc/VL_EXPIRY.pgc @@ -9,6 +9,119 @@ void VL_EXPIRY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_EXPIRY: 수행 시작 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_EXPIRY: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_EXPIRY' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_EXPIRY: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (분기 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_EXPIRY: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_EXPIRY flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_EXPIRY", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_EXPIRY: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_EXPIRY: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_EXPIRY: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_EXPIRY: 심화검증 정상 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ char expiry[8], bizdate[16]; long rc = 0, exp_ym, biz_ym, y, m, d; gets_(b, T_STR2, expiry, sizeof(expiry)); diff --git a/app/src/vl/svc/VL_FEE.pgc b/app/src/vl/svc/VL_FEE.pgc index fa29480..338af39 100644 --- a/app/src/vl/svc/VL_FEE.pgc +++ b/app/src/vl/svc/VL_FEE.pgc @@ -9,6 +9,144 @@ void VL_FEE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_FEE: 요청 수신 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_FEE: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_pid = getl(b, T_PURCHASE_ID); + if (v_pid < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "매입 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + EXEC SQL SELECT merchant_id, amount, fee, net, status + INTO :v_merch, :v_amt, :v_fee, :v_net, :v_pstat + FROM purchase WHERE purchase_id = :v_pid; + if (sqlca.sqlcode == 100) { + v_flag |= 0x01; /* 매입 원거래 미존재 */ + userlog("VL_FEE: 매입 미존재 pid=%ld", v_pid); + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 81); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG3, v_net); + v_base = v_amt; + } + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_FEE' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_FEE: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (조건 분기) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_FEE: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*) INTO :v_acnt + FROM approval WHERE purchase_id = :v_pid AND approved = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 83); Bchg(b, T_MSG, 0, "승인 대사 오류", 0L); FAIL(b); } + setl(b, T_ARG4, v_acnt); + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_lcnt, :v_lsum + FROM ledger WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 84); Bchg(b, T_MSG, 0, "원장 대사 오류", 0L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :v_scnt, :v_snet + FROM settlement WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 85); Bchg(b, T_MSG, 0, "정산 대사 오류", 0L); FAIL(b); } + if (v_acnt == 0) v_flag |= 0x08; /* 무승인 */ + if (v_lcnt == 0) v_flag |= 0x10; /* 원장 미기록 */ + if (v_scnt > 0 && v_snet != v_net) v_flag |= 0x20; /* 정산액 불일치 */ + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_FEE flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_FEE", v_pid, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_FEE: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_FEE: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_FEE: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_FEE: 심화검증 검증 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0, recompute; EXEC SQL BEGIN DECLARE SECTION; long h_pid, h_amount, h_fee, h_bps; diff --git a/app/src/vl/svc/VL_FEEADJNET.pgc b/app/src/vl/svc/VL_FEEADJNET.pgc index d5f466e..f117fe6 100644 --- a/app/src/vl/svc/VL_FEEADJNET.pgc +++ b/app/src/vl/svc/VL_FEEADJNET.pgc @@ -8,6 +8,144 @@ void VL_FEEADJNET(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_FEEADJNET: 진입 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_FEEADJNET: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_pid = getl(b, T_PURCHASE_ID); + if (v_pid < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "매입 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + EXEC SQL SELECT merchant_id, amount, fee, net, status + INTO :v_merch, :v_amt, :v_fee, :v_net, :v_pstat + FROM purchase WHERE purchase_id = :v_pid; + if (sqlca.sqlcode == 100) { + v_flag |= 0x01; /* 매입 원거래 미존재 */ + userlog("VL_FEEADJNET: 매입 미존재 pid=%ld", v_pid); + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 81); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG3, v_net); + v_base = v_amt; + } + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_FEEADJNET' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_FEEADJNET: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (분기 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_FEEADJNET: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*) INTO :v_acnt + FROM approval WHERE purchase_id = :v_pid AND approved = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 83); Bchg(b, T_MSG, 0, "승인 대사 오류", 0L); FAIL(b); } + setl(b, T_ARG4, v_acnt); + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_lcnt, :v_lsum + FROM ledger WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 84); Bchg(b, T_MSG, 0, "원장 대사 오류", 0L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :v_scnt, :v_snet + FROM settlement WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 85); Bchg(b, T_MSG, 0, "정산 대사 오류", 0L); FAIL(b); } + if (v_acnt == 0) v_flag |= 0x08; /* 무승인 */ + if (v_lcnt == 0) v_flag |= 0x10; /* 원장 미기록 */ + if (v_scnt > 0 && v_snet != v_net) v_flag |= 0x20; /* 정산액 불일치 */ + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_FEEADJNET flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_FEEADJNET", v_pid, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_FEEADJNET: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_FEEADJNET: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_FEEADJNET: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_FEEADJNET: 심화검증 정상 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0, eff; EXEC SQL BEGIN DECLARE SECTION; long h_pid, h_amt, h_fee, h_adj, h_acnt; diff --git a/app/src/vl/svc/VL_FEEHIGH.pgc b/app/src/vl/svc/VL_FEEHIGH.pgc index 22dc5c1..ee14eea 100644 --- a/app/src/vl/svc/VL_FEEHIGH.pgc +++ b/app/src/vl/svc/VL_FEEHIGH.pgc @@ -8,6 +8,143 @@ void VL_FEEHIGH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_FEEHIGH: 처리 개시 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_FEEHIGH: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_pid = getl(b, T_PURCHASE_ID); + if (v_pid < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "매입 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + EXEC SQL SELECT merchant_id, amount, fee, net, status + INTO :v_merch, :v_amt, :v_fee, :v_net, :v_pstat + FROM purchase WHERE purchase_id = :v_pid; + if (sqlca.sqlcode == 100) { + v_flag |= 0x01; /* 매입 원거래 미존재 */ + userlog("VL_FEEHIGH: 매입 미존재 pid=%ld", v_pid); + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 81); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG3, v_net); + v_base = v_amt; + } + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_FEEHIGH' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_FEEHIGH: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (룰 매칭) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_FEEHIGH: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*) INTO :v_acnt + FROM approval WHERE purchase_id = :v_pid AND approved = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 83); Bchg(b, T_MSG, 0, "승인 대사 오류", 0L); FAIL(b); } + setl(b, T_ARG4, v_acnt); + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_lcnt, :v_lsum + FROM ledger WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 84); Bchg(b, T_MSG, 0, "원장 대사 오류", 0L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :v_scnt, :v_snet + FROM settlement WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 85); Bchg(b, T_MSG, 0, "정산 대사 오류", 0L); FAIL(b); } + if (v_acnt == 0) v_flag |= 0x08; /* 무승인 */ + if (v_lcnt == 0) v_flag |= 0x10; /* 원장 미기록 */ + if (v_scnt > 0 && v_snet != v_net) v_flag |= 0x20; /* 정산액 불일치 */ + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_FEEHIGH flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_FEEHIGH", v_pid, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_FEEHIGH: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_FEEHIGH: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_FEEHIGH: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_FEEHIGH: 심화검증 처리 종결 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0, bps; EXEC SQL BEGIN DECLARE SECTION; long h_pid, h_amt, h_fee; diff --git a/app/src/vl/svc/VL_FEERECALC.pgc b/app/src/vl/svc/VL_FEERECALC.pgc index b484817..3735ca0 100644 --- a/app/src/vl/svc/VL_FEERECALC.pgc +++ b/app/src/vl/svc/VL_FEERECALC.pgc @@ -9,6 +9,143 @@ void VL_FEERECALC(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_FEERECALC: 수행 시작 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_FEERECALC: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_pid = getl(b, T_PURCHASE_ID); + if (v_pid < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "매입 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + EXEC SQL SELECT merchant_id, amount, fee, net, status + INTO :v_merch, :v_amt, :v_fee, :v_net, :v_pstat + FROM purchase WHERE purchase_id = :v_pid; + if (sqlca.sqlcode == 100) { + v_flag |= 0x01; /* 매입 원거래 미존재 */ + userlog("VL_FEERECALC: 매입 미존재 pid=%ld", v_pid); + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 81); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG3, v_net); + v_base = v_amt; + } + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_FEERECALC' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_FEERECALC: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (업무규칙 적용) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_FEERECALC: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*) INTO :v_acnt + FROM approval WHERE purchase_id = :v_pid AND approved = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 83); Bchg(b, T_MSG, 0, "승인 대사 오류", 0L); FAIL(b); } + setl(b, T_ARG4, v_acnt); + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_lcnt, :v_lsum + FROM ledger WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 84); Bchg(b, T_MSG, 0, "원장 대사 오류", 0L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :v_scnt, :v_snet + FROM settlement WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 85); Bchg(b, T_MSG, 0, "정산 대사 오류", 0L); FAIL(b); } + if (v_acnt == 0) v_flag |= 0x08; /* 무승인 */ + if (v_lcnt == 0) v_flag |= 0x10; /* 원장 미기록 */ + if (v_scnt > 0 && v_snet != v_net) v_flag |= 0x20; /* 정산액 불일치 */ + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_FEERECALC flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_FEERECALC", v_pid, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_FEERECALC: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_FEERECALC: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_FEERECALC: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_FEERECALC: 심화검증 처리 종결 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0, tol, expfee, d; EXEC SQL BEGIN DECLARE SECTION; long h_pid, h_amt, h_fee, h_bps; diff --git a/app/src/vl/svc/VL_FORMAT.pgc b/app/src/vl/svc/VL_FORMAT.pgc index 7e285d8..6c3912b 100644 --- a/app/src/vl/svc/VL_FORMAT.pgc +++ b/app/src/vl/svc/VL_FORMAT.pgc @@ -9,6 +9,142 @@ void VL_FORMAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_FORMAT: 요청 수신 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_FORMAT: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + gets_(b, T_MERCHANT, v_merch, sizeof(v_merch)); + if (v_merch[0] && strlen(v_merch) > 32) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (v_merch[0]) { + EXEC SQL SELECT status, mdr_bps, daily_limit + INTO :v_status, :v_bps, :v_lim + FROM merchant WHERE merchant_id = :v_merch; + if (sqlca.sqlcode == 100) { + v_flag |= 0x02; /* 가맹점 마스터 미존재 */ + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 82); Bchg(b, T_MSG, 0, "가맹점 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG7, v_bps); + if (strcmp(v_status, "ACTIVE") != 0) v_flag |= 0x04; /* 상태부적합 */ + } + } + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_FORMAT' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_FORMAT: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (조건 분기) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_FORMAT: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + if (v_merch[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0) + INTO :v_pcnt, :v_pgross, :v_pfee + FROM purchase + WHERE merchant_id = :v_merch AND biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 86); Bchg(b, T_MSG, 0, "매입 재집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_pcnt); + setl(b, T_AMT3, v_pgross); + v_base = v_pgross; + } + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_FORMAT flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_FORMAT", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_FORMAT: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_FORMAT: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_FORMAT: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_FORMAT: 심화검증 처리 종결 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ char merch[64]; long rc = 0, len; gets_(b, T_MERCHANT, merch, sizeof(merch)); diff --git a/app/src/vl/svc/VL_FXCHK.pgc b/app/src/vl/svc/VL_FXCHK.pgc index bf2df75..429af95 100644 --- a/app/src/vl/svc/VL_FXCHK.pgc +++ b/app/src/vl/svc/VL_FXCHK.pgc @@ -8,6 +8,143 @@ void VL_FXCHK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_FXCHK: 처리 개시 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_FXCHK: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_pid = getl(b, T_PURCHASE_ID); + if (v_pid < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "매입 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + EXEC SQL SELECT merchant_id, amount, fee, net, status + INTO :v_merch, :v_amt, :v_fee, :v_net, :v_pstat + FROM purchase WHERE purchase_id = :v_pid; + if (sqlca.sqlcode == 100) { + v_flag |= 0x01; /* 매입 원거래 미존재 */ + userlog("VL_FXCHK: 매입 미존재 pid=%ld", v_pid); + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 81); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG3, v_net); + v_base = v_amt; + } + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_FXCHK' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_FXCHK: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (분기 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_FXCHK: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*) INTO :v_acnt + FROM approval WHERE purchase_id = :v_pid AND approved = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 83); Bchg(b, T_MSG, 0, "승인 대사 오류", 0L); FAIL(b); } + setl(b, T_ARG4, v_acnt); + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_lcnt, :v_lsum + FROM ledger WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 84); Bchg(b, T_MSG, 0, "원장 대사 오류", 0L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :v_scnt, :v_snet + FROM settlement WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 85); Bchg(b, T_MSG, 0, "정산 대사 오류", 0L); FAIL(b); } + if (v_acnt == 0) v_flag |= 0x08; /* 무승인 */ + if (v_lcnt == 0) v_flag |= 0x10; /* 원장 미기록 */ + if (v_scnt > 0 && v_snet != v_net) v_flag |= 0x20; /* 정산액 불일치 */ + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_FXCHK flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_FXCHK", v_pid, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_FXCHK: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_FXCHK: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_FXCHK: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_FXCHK: 심화검증 수행 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0, tol, expkrw, d; EXEC SQL BEGIN DECLARE SECTION; long h_pid, h_cnt, h_fx, h_krw, h_rate; diff --git a/app/src/vl/svc/VL_INSTCHK.pgc b/app/src/vl/svc/VL_INSTCHK.pgc index 6c47b36..a38b6cb 100644 --- a/app/src/vl/svc/VL_INSTCHK.pgc +++ b/app/src/vl/svc/VL_INSTCHK.pgc @@ -8,6 +8,143 @@ void VL_INSTCHK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_INSTCHK: 검증 착수 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_INSTCHK: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_pid = getl(b, T_PURCHASE_ID); + if (v_pid < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "매입 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + EXEC SQL SELECT merchant_id, amount, fee, net, status + INTO :v_merch, :v_amt, :v_fee, :v_net, :v_pstat + FROM purchase WHERE purchase_id = :v_pid; + if (sqlca.sqlcode == 100) { + v_flag |= 0x01; /* 매입 원거래 미존재 */ + userlog("VL_INSTCHK: 매입 미존재 pid=%ld", v_pid); + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 81); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG3, v_net); + v_base = v_amt; + } + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_INSTCHK' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_INSTCHK: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (구간 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_INSTCHK: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*) INTO :v_acnt + FROM approval WHERE purchase_id = :v_pid AND approved = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 83); Bchg(b, T_MSG, 0, "승인 대사 오류", 0L); FAIL(b); } + setl(b, T_ARG4, v_acnt); + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_lcnt, :v_lsum + FROM ledger WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 84); Bchg(b, T_MSG, 0, "원장 대사 오류", 0L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :v_scnt, :v_snet + FROM settlement WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 85); Bchg(b, T_MSG, 0, "정산 대사 오류", 0L); FAIL(b); } + if (v_acnt == 0) v_flag |= 0x08; /* 무승인 */ + if (v_lcnt == 0) v_flag |= 0x10; /* 원장 미기록 */ + if (v_scnt > 0 && v_snet != v_net) v_flag |= 0x20; /* 정산액 불일치 */ + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_INSTCHK flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_INSTCHK", v_pid, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_INSTCHK: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_INSTCHK: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_INSTCHK: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_INSTCHK: 심화검증 처리 종결 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0, tol, d; EXEC SQL BEGIN DECLARE SECTION; long h_pid, h_amt, h_isum, h_icnt, h_months; diff --git a/app/src/vl/svc/VL_ISSUERMIX.pgc b/app/src/vl/svc/VL_ISSUERMIX.pgc index d5cbb67..db8d1c5 100644 --- a/app/src/vl/svc/VL_ISSUERMIX.pgc +++ b/app/src/vl/svc/VL_ISSUERMIX.pgc @@ -8,6 +8,118 @@ void VL_ISSUERMIX(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_ISSUERMIX: 수행 시작 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_ISSUERMIX: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_ISSUERMIX' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_ISSUERMIX: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (구간 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_ISSUERMIX: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_ISSUERMIX flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_ISSUERMIX", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_ISSUERMIX: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_ISSUERMIX: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_ISSUERMIX: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_ISSUERMIX: 심화검증 처리 종결 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0, pct, thr; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16], h_iss[64]; diff --git a/app/src/vl/svc/VL_LEDGBAL.pgc b/app/src/vl/svc/VL_LEDGBAL.pgc index d28bcef..303e28a 100644 --- a/app/src/vl/svc/VL_LEDGBAL.pgc +++ b/app/src/vl/svc/VL_LEDGBAL.pgc @@ -9,6 +9,119 @@ void VL_LEDGBAL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 0; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "LEDGER_MIS"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_LEDGBAL: 검증 착수 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_LEDGBAL: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("LEDGER_MIS", &v_thr, v_sev) < 0) { v_thr = 0; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_LEDGBAL' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_LEDGBAL: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (구간 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_LEDGBAL: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_LEDGBAL flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_LEDGBAL", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_LEDGBAL: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_LEDGBAL: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_LEDGBAL: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_LEDGBAL: 심화검증 정상 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/src/vl/svc/VL_LEDGDATE.pgc b/app/src/vl/svc/VL_LEDGDATE.pgc index ca82504..c9eff5b 100644 --- a/app/src/vl/svc/VL_LEDGDATE.pgc +++ b/app/src/vl/svc/VL_LEDGDATE.pgc @@ -8,6 +8,149 @@ void VL_LEDGDATE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 0; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "LEDGER_MIS"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_LEDGDATE: 요청 수신 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_LEDGDATE: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_pid = getl(b, T_PURCHASE_ID); + if (v_pid < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "매입 ID 음수 불가", 0L); + FAIL(b); + } + if (getl(b, T_ID1) < 0) { + setl(b, T_RC, 95); + Bchg(b, T_MSG, 0, "의심거래 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + EXEC SQL SELECT merchant_id, amount, fee, net, status + INTO :v_merch, :v_amt, :v_fee, :v_net, :v_pstat + FROM purchase WHERE purchase_id = :v_pid; + if (sqlca.sqlcode == 100) { + v_flag |= 0x01; /* 매입 원거래 미존재 */ + userlog("VL_LEDGDATE: 매입 미존재 pid=%ld", v_pid); + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 81); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG3, v_net); + v_base = v_amt; + } + if (vldb_get_rule("LEDGER_MIS", &v_thr, v_sev) < 0) { v_thr = 0; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_LEDGDATE' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_LEDGDATE: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (분기 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_LEDGDATE: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*) INTO :v_acnt + FROM approval WHERE purchase_id = :v_pid AND approved = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 83); Bchg(b, T_MSG, 0, "승인 대사 오류", 0L); FAIL(b); } + setl(b, T_ARG4, v_acnt); + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_lcnt, :v_lsum + FROM ledger WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 84); Bchg(b, T_MSG, 0, "원장 대사 오류", 0L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :v_scnt, :v_snet + FROM settlement WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 85); Bchg(b, T_MSG, 0, "정산 대사 오류", 0L); FAIL(b); } + if (v_acnt == 0) v_flag |= 0x08; /* 무승인 */ + if (v_lcnt == 0) v_flag |= 0x10; /* 원장 미기록 */ + if (v_scnt > 0 && v_snet != v_net) v_flag |= 0x20; /* 정산액 불일치 */ + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_LEDGDATE flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_LEDGDATE", v_pid, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_LEDGDATE: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_LEDGDATE: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_LEDGDATE: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_LEDGDATE: 심화검증 수행 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; long h_pid, h_rev, h_first; diff --git a/app/src/vl/svc/VL_LEDGENTRY.pgc b/app/src/vl/svc/VL_LEDGENTRY.pgc index 6c2cdb3..32bdb02 100644 --- a/app/src/vl/svc/VL_LEDGENTRY.pgc +++ b/app/src/vl/svc/VL_LEDGENTRY.pgc @@ -8,6 +8,150 @@ void VL_LEDGENTRY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 0; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "LEDGER_MIS"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_LEDGENTRY: 처리 개시 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_LEDGENTRY: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_pid = getl(b, T_PURCHASE_ID); + if (v_pid < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "매입 ID 음수 불가", 0L); + FAIL(b); + } + v_amt = getl(b, T_AMOUNT); + if (v_amt < 0) { + setl(b, T_RC, 94); + Bchg(b, T_MSG, 0, "매입 금액 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + EXEC SQL SELECT merchant_id, amount, fee, net, status + INTO :v_merch, :v_amt, :v_fee, :v_net, :v_pstat + FROM purchase WHERE purchase_id = :v_pid; + if (sqlca.sqlcode == 100) { + v_flag |= 0x01; /* 매입 원거래 미존재 */ + userlog("VL_LEDGENTRY: 매입 미존재 pid=%ld", v_pid); + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 81); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG3, v_net); + v_base = v_amt; + } + if (vldb_get_rule("LEDGER_MIS", &v_thr, v_sev) < 0) { v_thr = 0; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_LEDGENTRY' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_LEDGENTRY: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (구간 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_LEDGENTRY: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*) INTO :v_acnt + FROM approval WHERE purchase_id = :v_pid AND approved = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 83); Bchg(b, T_MSG, 0, "승인 대사 오류", 0L); FAIL(b); } + setl(b, T_ARG4, v_acnt); + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_lcnt, :v_lsum + FROM ledger WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 84); Bchg(b, T_MSG, 0, "원장 대사 오류", 0L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :v_scnt, :v_snet + FROM settlement WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 85); Bchg(b, T_MSG, 0, "정산 대사 오류", 0L); FAIL(b); } + if (v_acnt == 0) v_flag |= 0x08; /* 무승인 */ + if (v_lcnt == 0) v_flag |= 0x10; /* 원장 미기록 */ + if (v_scnt > 0 && v_snet != v_net) v_flag |= 0x20; /* 정산액 불일치 */ + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_LEDGENTRY flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_LEDGENTRY", v_pid, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_LEDGENTRY: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_LEDGENTRY: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_LEDGENTRY: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_LEDGENTRY: 심화검증 정상 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; long h_pid, h_amt, h_lcnt, h_lamt; diff --git a/app/src/vl/svc/VL_LEDGER.pgc b/app/src/vl/svc/VL_LEDGER.pgc index cc7f96f..d60c77e 100644 --- a/app/src/vl/svc/VL_LEDGER.pgc +++ b/app/src/vl/svc/VL_LEDGER.pgc @@ -9,6 +9,145 @@ void VL_LEDGER(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 0; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "LEDGER_MIS"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_LEDGER: 수행 시작 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_LEDGER: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_pid = getl(b, T_PURCHASE_ID); + if (v_pid < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "매입 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + EXEC SQL SELECT merchant_id, amount, fee, net, status + INTO :v_merch, :v_amt, :v_fee, :v_net, :v_pstat + FROM purchase WHERE purchase_id = :v_pid; + if (sqlca.sqlcode == 100) { + v_flag |= 0x01; /* 매입 원거래 미존재 */ + userlog("VL_LEDGER: 매입 미존재 pid=%ld", v_pid); + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 81); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG3, v_net); + v_base = v_amt; + } + if (vldb_get_rule("LEDGER_MIS", &v_thr, v_sev) < 0) { v_thr = 0; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_LEDGER' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_LEDGER: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (조건 분기) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_LEDGER: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*) INTO :v_acnt + FROM approval WHERE purchase_id = :v_pid AND approved = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 83); Bchg(b, T_MSG, 0, "승인 대사 오류", 0L); FAIL(b); } + setl(b, T_ARG4, v_acnt); + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_lcnt, :v_lsum + FROM ledger WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 84); Bchg(b, T_MSG, 0, "원장 대사 오류", 0L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :v_scnt, :v_snet + FROM settlement WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 85); Bchg(b, T_MSG, 0, "정산 대사 오류", 0L); FAIL(b); } + if (v_acnt == 0) v_flag |= 0x08; /* 무승인 */ + if (v_lcnt == 0) v_flag |= 0x10; /* 원장 미기록 */ + if (v_scnt > 0 && v_snet != v_net) v_flag |= 0x20; /* 정산액 불일치 */ + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_LEDGER flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_LEDGER", v_pid, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_LEDGER: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_metric >= 3 && !(v_flag & 0x01)) { + v_sid = vldb_insert_suspect(v_pid, v_rule, v_sev, "심화검증 임계 초과", v_bizdate); + if (v_sid < 0) { setl(b, T_RC, 89); Bchg(b, T_MSG, 0, "의심거래 적재 실패", 0L); FAIL(b); } + setl(b, T_ID2, v_sid); + userlog("VL_LEDGER: 의심거래 적재 sid=%ld rule=%s", v_sid, v_rule); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_LEDGER: 심화검증 수행 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ char bizdate[16]; long rc = 0; EXEC SQL BEGIN DECLARE SECTION; diff --git a/app/src/vl/svc/VL_LIMIT.pgc b/app/src/vl/svc/VL_LIMIT.pgc index 173761e..d6751f6 100644 --- a/app/src/vl/svc/VL_LIMIT.pgc +++ b/app/src/vl/svc/VL_LIMIT.pgc @@ -9,6 +9,148 @@ void VL_LIMIT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 0; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "LIMIT_OVR"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_LIMIT: 처리 개시 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_LIMIT: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + gets_(b, T_MERCHANT, v_merch, sizeof(v_merch)); + if (v_merch[0] && strlen(v_merch) > 32) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); + FAIL(b); + } + v_amt = getl(b, T_AMOUNT); + if (v_amt < 0) { + setl(b, T_RC, 94); + Bchg(b, T_MSG, 0, "매입 금액 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (v_merch[0]) { + EXEC SQL SELECT status, mdr_bps, daily_limit + INTO :v_status, :v_bps, :v_lim + FROM merchant WHERE merchant_id = :v_merch; + if (sqlca.sqlcode == 100) { + v_flag |= 0x02; /* 가맹점 마스터 미존재 */ + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 82); Bchg(b, T_MSG, 0, "가맹점 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG7, v_bps); + if (strcmp(v_status, "ACTIVE") != 0) v_flag |= 0x04; /* 상태부적합 */ + } + } + if (vldb_get_rule("LIMIT_OVR", &v_thr, v_sev) < 0) { v_thr = 0; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_LIMIT' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_LIMIT: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (룰 매칭) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_LIMIT: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + if (v_merch[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0) + INTO :v_pcnt, :v_pgross, :v_pfee + FROM purchase + WHERE merchant_id = :v_merch AND biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 86); Bchg(b, T_MSG, 0, "매입 재집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_pcnt); + setl(b, T_AMT3, v_pgross); + v_base = v_pgross; + } + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_LIMIT flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_LIMIT", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_LIMIT: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_LIMIT: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_LIMIT: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_LIMIT: 심화검증 검증 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ char merch[64]; long rc = 0; EXEC SQL BEGIN DECLARE SECTION; diff --git a/app/src/vl/svc/VL_LIMITNEAR.pgc b/app/src/vl/svc/VL_LIMITNEAR.pgc index bde6303..d0cebc2 100644 --- a/app/src/vl/svc/VL_LIMITNEAR.pgc +++ b/app/src/vl/svc/VL_LIMITNEAR.pgc @@ -9,6 +9,142 @@ void VL_LIMITNEAR(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 0; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "LIMIT_OVR"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_LIMITNEAR: 요청 수신 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_LIMITNEAR: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + gets_(b, T_MERCHANT, v_merch, sizeof(v_merch)); + if (v_merch[0] && strlen(v_merch) > 32) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (v_merch[0]) { + EXEC SQL SELECT status, mdr_bps, daily_limit + INTO :v_status, :v_bps, :v_lim + FROM merchant WHERE merchant_id = :v_merch; + if (sqlca.sqlcode == 100) { + v_flag |= 0x02; /* 가맹점 마스터 미존재 */ + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 82); Bchg(b, T_MSG, 0, "가맹점 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG7, v_bps); + if (strcmp(v_status, "ACTIVE") != 0) v_flag |= 0x04; /* 상태부적합 */ + } + } + if (vldb_get_rule("LIMIT_OVR", &v_thr, v_sev) < 0) { v_thr = 0; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_LIMITNEAR' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_LIMITNEAR: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (룰 매칭) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_LIMITNEAR: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + if (v_merch[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0) + INTO :v_pcnt, :v_pgross, :v_pfee + FROM purchase + WHERE merchant_id = :v_merch AND biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 86); Bchg(b, T_MSG, 0, "매입 재집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_pcnt); + setl(b, T_AMT3, v_pgross); + v_base = v_pgross; + } + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_LIMITNEAR flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_LIMITNEAR", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_LIMITNEAR: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_LIMITNEAR: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_LIMITNEAR: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_LIMITNEAR: 심화검증 정상 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0, thr, pct = 0; EXEC SQL BEGIN DECLARE SECTION; char h_merch[64], h_bizdate[16]; diff --git a/app/src/vl/svc/VL_LIMITPCT.pgc b/app/src/vl/svc/VL_LIMITPCT.pgc index 36807eb..7ec2b77 100644 --- a/app/src/vl/svc/VL_LIMITPCT.pgc +++ b/app/src/vl/svc/VL_LIMITPCT.pgc @@ -8,6 +8,142 @@ void VL_LIMITPCT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 0; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "LIMIT_OVR"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_LIMITPCT: 진입 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_LIMITPCT: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + gets_(b, T_MERCHANT, v_merch, sizeof(v_merch)); + if (v_merch[0] && strlen(v_merch) > 32) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (v_merch[0]) { + EXEC SQL SELECT status, mdr_bps, daily_limit + INTO :v_status, :v_bps, :v_lim + FROM merchant WHERE merchant_id = :v_merch; + if (sqlca.sqlcode == 100) { + v_flag |= 0x02; /* 가맹점 마스터 미존재 */ + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 82); Bchg(b, T_MSG, 0, "가맹점 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG7, v_bps); + if (strcmp(v_status, "ACTIVE") != 0) v_flag |= 0x04; /* 상태부적합 */ + } + } + if (vldb_get_rule("LIMIT_OVR", &v_thr, v_sev) < 0) { v_thr = 0; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_LIMITPCT' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_LIMITPCT: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (룰 매칭) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_LIMITPCT: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + if (v_merch[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0) + INTO :v_pcnt, :v_pgross, :v_pfee + FROM purchase + WHERE merchant_id = :v_merch AND biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 86); Bchg(b, T_MSG, 0, "매입 재집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_pcnt); + setl(b, T_AMT3, v_pgross); + v_base = v_pgross; + } + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_LIMITPCT flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_LIMITPCT", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_LIMITPCT: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_LIMITPCT: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_LIMITPCT: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_LIMITPCT: 심화검증 처리 종결 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0, pct, thr; EXEC SQL BEGIN DECLARE SECTION; char h_merch[64], h_bizdate[16]; diff --git a/app/src/vl/svc/VL_LONGOPEN.pgc b/app/src/vl/svc/VL_LONGOPEN.pgc index 7ac1c15..0bc24d3 100644 --- a/app/src/vl/svc/VL_LONGOPEN.pgc +++ b/app/src/vl/svc/VL_LONGOPEN.pgc @@ -8,6 +8,122 @@ void VL_LONGOPEN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_LONGOPEN: 요청 수신 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_LONGOPEN: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + if (getl(b, T_ID1) < 0) { + setl(b, T_RC, 95); + Bchg(b, T_MSG, 0, "의심거래 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_LONGOPEN' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_LONGOPEN: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (업무규칙 적용) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_LONGOPEN: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_LONGOPEN flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_LONGOPEN", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_LONGOPEN: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_LONGOPEN: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_LONGOPEN: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_LONGOPEN: 심화검증 처리 종결 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; long h_days, h_cnt, h_oldest; diff --git a/app/src/vl/svc/VL_MDRRATE.pgc b/app/src/vl/svc/VL_MDRRATE.pgc index dfa56d4..0011f6d 100644 --- a/app/src/vl/svc/VL_MDRRATE.pgc +++ b/app/src/vl/svc/VL_MDRRATE.pgc @@ -9,6 +9,118 @@ void VL_MDRRATE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_MDRRATE: 처리 개시 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_MDRRATE: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_MDRRATE' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_MDRRATE: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (업무규칙 적용) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_MDRRATE: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_MDRRATE flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_MDRRATE", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_MDRRATE: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_MDRRATE: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_MDRRATE: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_MDRRATE: 심화검증 정상 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; char sev[8]; EXEC SQL BEGIN DECLARE SECTION; diff --git a/app/src/vl/svc/VL_MERCH.pgc b/app/src/vl/svc/VL_MERCH.pgc index e766e8c..078cad3 100644 --- a/app/src/vl/svc/VL_MERCH.pgc +++ b/app/src/vl/svc/VL_MERCH.pgc @@ -9,6 +9,142 @@ void VL_MERCH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_MERCH: 진입 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_MERCH: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + gets_(b, T_MERCHANT, v_merch, sizeof(v_merch)); + if (v_merch[0] && strlen(v_merch) > 32) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (v_merch[0]) { + EXEC SQL SELECT status, mdr_bps, daily_limit + INTO :v_status, :v_bps, :v_lim + FROM merchant WHERE merchant_id = :v_merch; + if (sqlca.sqlcode == 100) { + v_flag |= 0x02; /* 가맹점 마스터 미존재 */ + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 82); Bchg(b, T_MSG, 0, "가맹점 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG7, v_bps); + if (strcmp(v_status, "ACTIVE") != 0) v_flag |= 0x04; /* 상태부적합 */ + } + } + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_MERCH' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_MERCH: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (조건 분기) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_MERCH: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + if (v_merch[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0) + INTO :v_pcnt, :v_pgross, :v_pfee + FROM purchase + WHERE merchant_id = :v_merch AND biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 86); Bchg(b, T_MSG, 0, "매입 재집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_pcnt); + setl(b, T_AMT3, v_pgross); + v_base = v_pgross; + } + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_MERCH flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_MERCH", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_MERCH: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_MERCH: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_MERCH: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_MERCH: 심화검증 검증 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ char merch[64]; long rc = 0; EXEC SQL BEGIN DECLARE SECTION; diff --git a/app/src/vl/svc/VL_NEGBAL.pgc b/app/src/vl/svc/VL_NEGBAL.pgc index f6e9e62..8b8c993 100644 --- a/app/src/vl/svc/VL_NEGBAL.pgc +++ b/app/src/vl/svc/VL_NEGBAL.pgc @@ -8,6 +8,119 @@ void VL_NEGBAL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 0; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_ZERO"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_NEGBAL: 진입 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_NEGBAL: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_ZERO", &v_thr, v_sev) < 0) { v_thr = 0; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_NEGBAL' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_NEGBAL: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (업무규칙 적용) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_NEGBAL: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_NEGBAL flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_NEGBAL", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_NEGBAL: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_NEGBAL: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_NEGBAL: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_NEGBAL: 심화검증 정상 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16], h_worst_m[64]; diff --git a/app/src/vl/svc/VL_NEGFEE.pgc b/app/src/vl/svc/VL_NEGFEE.pgc index fbb0abb..d56e0e4 100644 --- a/app/src/vl/svc/VL_NEGFEE.pgc +++ b/app/src/vl/svc/VL_NEGFEE.pgc @@ -8,6 +8,124 @@ void VL_NEGFEE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 0; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_ZERO"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_NEGFEE: 수행 시작 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_NEGFEE: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + if (getl(b, T_ID1) < 0) { + setl(b, T_RC, 95); + Bchg(b, T_MSG, 0, "의심거래 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_ZERO", &v_thr, v_sev) < 0) { v_thr = 0; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_NEGFEE' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_NEGFEE: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (구간 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_NEGFEE: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_NEGFEE flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_NEGFEE", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_NEGFEE: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_NEGFEE: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_NEGFEE: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_NEGFEE: 심화검증 수행 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/src/vl/svc/VL_NEGNET.pgc b/app/src/vl/svc/VL_NEGNET.pgc index 0b85b9e..2fed81e 100644 --- a/app/src/vl/svc/VL_NEGNET.pgc +++ b/app/src/vl/svc/VL_NEGNET.pgc @@ -8,6 +8,119 @@ void VL_NEGNET(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 0; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_ZERO"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_NEGNET: 요청 수신 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_NEGNET: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_ZERO", &v_thr, v_sev) < 0) { v_thr = 0; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_NEGNET' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_NEGNET: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (구간 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_NEGNET: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_NEGNET flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_NEGNET", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_NEGNET: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_NEGNET: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_NEGNET: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_NEGNET: 심화검증 처리 종결 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/src/vl/svc/VL_NIGHTTXN.pgc b/app/src/vl/svc/VL_NIGHTTXN.pgc index bb5ea46..434379c 100644 --- a/app/src/vl/svc/VL_NIGHTTXN.pgc +++ b/app/src/vl/svc/VL_NIGHTTXN.pgc @@ -8,6 +8,118 @@ void VL_NIGHTTXN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_NIGHTTXN: 수행 시작 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_NIGHTTXN: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_NIGHTTXN' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_NIGHTTXN: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (구간 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_NIGHTTXN: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_NIGHTTXN flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_NIGHTTXN", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_NIGHTTXN: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_NIGHTTXN: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_NIGHTTXN: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_NIGHTTXN: 심화검증 수행 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0, pct, thr; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/src/vl/svc/VL_OUTLIER.pgc b/app/src/vl/svc/VL_OUTLIER.pgc index 735c300..81e8ecb 100644 --- a/app/src/vl/svc/VL_OUTLIER.pgc +++ b/app/src/vl/svc/VL_OUTLIER.pgc @@ -8,6 +8,118 @@ void VL_OUTLIER(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_OUTLIER: 수행 시작 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_OUTLIER: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_OUTLIER' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_OUTLIER: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (업무규칙 적용) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_OUTLIER: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_OUTLIER flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_OUTLIER", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_OUTLIER: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_OUTLIER: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_OUTLIER: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_OUTLIER: 심화검증 수행 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/src/vl/svc/VL_OVERLIMIT.pgc b/app/src/vl/svc/VL_OVERLIMIT.pgc index 464a6df..40b6b84 100644 --- a/app/src/vl/svc/VL_OVERLIMIT.pgc +++ b/app/src/vl/svc/VL_OVERLIMIT.pgc @@ -8,6 +8,119 @@ void VL_OVERLIMIT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 0; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "LIMIT_OVR"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_OVERLIMIT: 수행 시작 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_OVERLIMIT: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("LIMIT_OVR", &v_thr, v_sev) < 0) { v_thr = 0; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_OVERLIMIT' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_OVERLIMIT: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (룰 매칭) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_OVERLIMIT: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_OVERLIMIT flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_OVERLIMIT", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_OVERLIMIT: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_OVERLIMIT: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_OVERLIMIT: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_OVERLIMIT: 심화검증 수행 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/src/vl/svc/VL_PURNOAPPR.pgc b/app/src/vl/svc/VL_PURNOAPPR.pgc index 5a44b3f..3f17492 100644 --- a/app/src/vl/svc/VL_PURNOAPPR.pgc +++ b/app/src/vl/svc/VL_PURNOAPPR.pgc @@ -8,6 +8,124 @@ void VL_PURNOAPPR(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_PURNOAPPR: 수행 시작 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_PURNOAPPR: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + if (getl(b, T_ID1) < 0) { + setl(b, T_RC, 95); + Bchg(b, T_MSG, 0, "의심거래 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_PURNOAPPR' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_PURNOAPPR: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (룰 매칭) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_PURNOAPPR: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_PURNOAPPR flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_PURNOAPPR", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_PURNOAPPR: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_PURNOAPPR: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_PURNOAPPR: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_PURNOAPPR: 심화검증 처리 종결 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/src/vl/svc/VL_RATEDEV.pgc b/app/src/vl/svc/VL_RATEDEV.pgc index cbb1050..aff407c 100644 --- a/app/src/vl/svc/VL_RATEDEV.pgc +++ b/app/src/vl/svc/VL_RATEDEV.pgc @@ -8,6 +8,143 @@ void VL_RATEDEV(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_RATEDEV: 수행 시작 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_RATEDEV: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_pid = getl(b, T_PURCHASE_ID); + if (v_pid < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "매입 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + EXEC SQL SELECT merchant_id, amount, fee, net, status + INTO :v_merch, :v_amt, :v_fee, :v_net, :v_pstat + FROM purchase WHERE purchase_id = :v_pid; + if (sqlca.sqlcode == 100) { + v_flag |= 0x01; /* 매입 원거래 미존재 */ + userlog("VL_RATEDEV: 매입 미존재 pid=%ld", v_pid); + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 81); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG3, v_net); + v_base = v_amt; + } + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_RATEDEV' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_RATEDEV: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (분기 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_RATEDEV: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*) INTO :v_acnt + FROM approval WHERE purchase_id = :v_pid AND approved = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 83); Bchg(b, T_MSG, 0, "승인 대사 오류", 0L); FAIL(b); } + setl(b, T_ARG4, v_acnt); + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_lcnt, :v_lsum + FROM ledger WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 84); Bchg(b, T_MSG, 0, "원장 대사 오류", 0L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :v_scnt, :v_snet + FROM settlement WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 85); Bchg(b, T_MSG, 0, "정산 대사 오류", 0L); FAIL(b); } + if (v_acnt == 0) v_flag |= 0x08; /* 무승인 */ + if (v_lcnt == 0) v_flag |= 0x10; /* 원장 미기록 */ + if (v_scnt > 0 && v_snet != v_net) v_flag |= 0x20; /* 정산액 불일치 */ + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_RATEDEV flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_RATEDEV", v_pid, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_RATEDEV: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_RATEDEV: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_RATEDEV: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_RATEDEV: 심화검증 정상 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0, tol, actual, dev; EXEC SQL BEGIN DECLARE SECTION; long h_pid, h_amt, h_fee, h_bps; diff --git a/app/src/vl/svc/VL_REFINTEG.pgc b/app/src/vl/svc/VL_REFINTEG.pgc index bfb76ff..7b4eb78 100644 --- a/app/src/vl/svc/VL_REFINTEG.pgc +++ b/app/src/vl/svc/VL_REFINTEG.pgc @@ -9,6 +9,144 @@ void VL_REFINTEG(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_REFINTEG: 처리 개시 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_REFINTEG: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_pid = getl(b, T_PURCHASE_ID); + if (v_pid < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "매입 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + EXEC SQL SELECT merchant_id, amount, fee, net, status + INTO :v_merch, :v_amt, :v_fee, :v_net, :v_pstat + FROM purchase WHERE purchase_id = :v_pid; + if (sqlca.sqlcode == 100) { + v_flag |= 0x01; /* 매입 원거래 미존재 */ + userlog("VL_REFINTEG: 매입 미존재 pid=%ld", v_pid); + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 81); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG3, v_net); + v_base = v_amt; + } + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_REFINTEG' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_REFINTEG: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (조건 분기) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_REFINTEG: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*) INTO :v_acnt + FROM approval WHERE purchase_id = :v_pid AND approved = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 83); Bchg(b, T_MSG, 0, "승인 대사 오류", 0L); FAIL(b); } + setl(b, T_ARG4, v_acnt); + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_lcnt, :v_lsum + FROM ledger WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 84); Bchg(b, T_MSG, 0, "원장 대사 오류", 0L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :v_scnt, :v_snet + FROM settlement WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 85); Bchg(b, T_MSG, 0, "정산 대사 오류", 0L); FAIL(b); } + if (v_acnt == 0) v_flag |= 0x08; /* 무승인 */ + if (v_lcnt == 0) v_flag |= 0x10; /* 원장 미기록 */ + if (v_scnt > 0 && v_snet != v_net) v_flag |= 0x20; /* 정산액 불일치 */ + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_REFINTEG flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_REFINTEG", v_pid, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_REFINTEG: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_REFINTEG: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_REFINTEG: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_REFINTEG: 심화검증 수행 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; long h_pid, h_cnt; diff --git a/app/src/vl/svc/VL_REPORT.pgc b/app/src/vl/svc/VL_REPORT.pgc index c798b3e..4c43b72 100644 --- a/app/src/vl/svc/VL_REPORT.pgc +++ b/app/src/vl/svc/VL_REPORT.pgc @@ -9,6 +9,119 @@ void VL_REPORT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_REPORT: 진입 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_REPORT: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_REPORT' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_REPORT: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (구간 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_REPORT: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_REPORT flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_REPORT", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_REPORT: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_REPORT: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_REPORT: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_REPORT: 심화검증 수행 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ char sev[8], bizdate[16]; EXEC SQL BEGIN DECLARE SECTION; long h_cnt; diff --git a/app/src/vl/svc/VL_REQUIRED.pgc b/app/src/vl/svc/VL_REQUIRED.pgc index df85fce..678b2b0 100644 --- a/app/src/vl/svc/VL_REQUIRED.pgc +++ b/app/src/vl/svc/VL_REQUIRED.pgc @@ -9,6 +9,148 @@ void VL_REQUIRED(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_REQUIRED: 처리 개시 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_REQUIRED: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + gets_(b, T_MERCHANT, v_merch, sizeof(v_merch)); + if (v_merch[0] && strlen(v_merch) > 32) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); + FAIL(b); + } + v_amt = getl(b, T_AMOUNT); + if (v_amt < 0) { + setl(b, T_RC, 94); + Bchg(b, T_MSG, 0, "매입 금액 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (v_merch[0]) { + EXEC SQL SELECT status, mdr_bps, daily_limit + INTO :v_status, :v_bps, :v_lim + FROM merchant WHERE merchant_id = :v_merch; + if (sqlca.sqlcode == 100) { + v_flag |= 0x02; /* 가맹점 마스터 미존재 */ + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 82); Bchg(b, T_MSG, 0, "가맹점 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG7, v_bps); + if (strcmp(v_status, "ACTIVE") != 0) v_flag |= 0x04; /* 상태부적합 */ + } + } + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_REQUIRED' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_REQUIRED: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (분기 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_REQUIRED: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + if (v_merch[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0) + INTO :v_pcnt, :v_pgross, :v_pfee + FROM purchase + WHERE merchant_id = :v_merch AND biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 86); Bchg(b, T_MSG, 0, "매입 재집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_pcnt); + setl(b, T_AMT3, v_pgross); + v_base = v_pgross; + } + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_REQUIRED flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_REQUIRED", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_REQUIRED: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_REQUIRED: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_REQUIRED: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_REQUIRED: 심화검증 정상 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ char merch[64]; long amt, rc = 0; gets_(b, T_MERCHANT, merch, sizeof(merch)); diff --git a/app/src/vl/svc/VL_RIAPPR.pgc b/app/src/vl/svc/VL_RIAPPR.pgc index a9d13f2..68a75f6 100644 --- a/app/src/vl/svc/VL_RIAPPR.pgc +++ b/app/src/vl/svc/VL_RIAPPR.pgc @@ -8,6 +8,118 @@ void VL_RIAPPR(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_RIAPPR: 진입 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_RIAPPR: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_RIAPPR' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_RIAPPR: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (조건 분기) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_RIAPPR: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_RIAPPR flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_RIAPPR", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_RIAPPR: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_RIAPPR: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_RIAPPR: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_RIAPPR: 심화검증 정상 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; long h_cnt; diff --git a/app/src/vl/svc/VL_RICANCEL.pgc b/app/src/vl/svc/VL_RICANCEL.pgc index d5cf1d8..b23e204 100644 --- a/app/src/vl/svc/VL_RICANCEL.pgc +++ b/app/src/vl/svc/VL_RICANCEL.pgc @@ -8,6 +8,119 @@ void VL_RICANCEL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_RICANCEL: 수행 시작 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_RICANCEL: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_RICANCEL' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_RICANCEL: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (룰 매칭) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_RICANCEL: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_RICANCEL flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_RICANCEL", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_RICANCEL: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_RICANCEL: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_RICANCEL: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_RICANCEL: 심화검증 처리 종결 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/src/vl/svc/VL_RIFX.pgc b/app/src/vl/svc/VL_RIFX.pgc index dee53c5..e3965b9 100644 --- a/app/src/vl/svc/VL_RIFX.pgc +++ b/app/src/vl/svc/VL_RIFX.pgc @@ -8,6 +8,118 @@ void VL_RIFX(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_RIFX: 진입 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_RIFX: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_RIFX' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_RIFX: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (업무규칙 적용) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_RIFX: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_RIFX flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_RIFX", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_RIFX: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_RIFX: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_RIFX: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_RIFX: 심화검증 정상 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; long h_cnt, h_amt; diff --git a/app/src/vl/svc/VL_RIINSTALL.pgc b/app/src/vl/svc/VL_RIINSTALL.pgc index 4d3b9c8..0381627 100644 --- a/app/src/vl/svc/VL_RIINSTALL.pgc +++ b/app/src/vl/svc/VL_RIINSTALL.pgc @@ -8,6 +8,118 @@ void VL_RIINSTALL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_RIINSTALL: 진입 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_RIINSTALL: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_RIINSTALL' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_RIINSTALL: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (구간 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_RIINSTALL: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_RIINSTALL flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_RIINSTALL", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_RIINSTALL: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_RIINSTALL: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_RIINSTALL: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_RIINSTALL: 심화검증 정상 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; long h_cnt; diff --git a/app/src/vl/svc/VL_RILEDGER.pgc b/app/src/vl/svc/VL_RILEDGER.pgc index fc5381d..834a5e5 100644 --- a/app/src/vl/svc/VL_RILEDGER.pgc +++ b/app/src/vl/svc/VL_RILEDGER.pgc @@ -8,6 +8,119 @@ void VL_RILEDGER(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 0; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "LEDGER_MIS"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_RILEDGER: 처리 개시 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_RILEDGER: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("LEDGER_MIS", &v_thr, v_sev) < 0) { v_thr = 0; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_RILEDGER' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_RILEDGER: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (분기 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_RILEDGER: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_RILEDGER flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_RILEDGER", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_RILEDGER: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_RILEDGER: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_RILEDGER: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_RILEDGER: 심화검증 검증 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/src/vl/svc/VL_RIMERCH.pgc b/app/src/vl/svc/VL_RIMERCH.pgc index 17b9779..ba1de49 100644 --- a/app/src/vl/svc/VL_RIMERCH.pgc +++ b/app/src/vl/svc/VL_RIMERCH.pgc @@ -8,6 +8,124 @@ void VL_RIMERCH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_RIMERCH: 진입 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_RIMERCH: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + if (getl(b, T_ID1) < 0) { + setl(b, T_RC, 95); + Bchg(b, T_MSG, 0, "의심거래 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_RIMERCH' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_RIMERCH: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (업무규칙 적용) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_RIMERCH: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_RIMERCH flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_RIMERCH", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_RIMERCH: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_RIMERCH: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_RIMERCH: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_RIMERCH: 심화검증 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/src/vl/svc/VL_RIPAY.pgc b/app/src/vl/svc/VL_RIPAY.pgc index e54854e..2cef9ab 100644 --- a/app/src/vl/svc/VL_RIPAY.pgc +++ b/app/src/vl/svc/VL_RIPAY.pgc @@ -8,6 +8,123 @@ void VL_RIPAY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_RIPAY: 수행 시작 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_RIPAY: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + if (getl(b, T_ID1) < 0) { + setl(b, T_RC, 95); + Bchg(b, T_MSG, 0, "의심거래 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_RIPAY' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_RIPAY: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (업무규칙 적용) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_RIPAY: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_RIPAY flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_RIPAY", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_RIPAY: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_RIPAY: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_RIPAY: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_RIPAY: 심화검증 처리 종결 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; long h_cnt, h_first; diff --git a/app/src/vl/svc/VL_RIPURCH.pgc b/app/src/vl/svc/VL_RIPURCH.pgc index 464a059..cf3505e 100644 --- a/app/src/vl/svc/VL_RIPURCH.pgc +++ b/app/src/vl/svc/VL_RIPURCH.pgc @@ -8,6 +8,124 @@ void VL_RIPURCH(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_RIPURCH: 처리 개시 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_RIPURCH: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + if (getl(b, T_ID1) < 0) { + setl(b, T_RC, 95); + Bchg(b, T_MSG, 0, "의심거래 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_RIPURCH' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_RIPURCH: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (조건 분기) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_RIPURCH: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_RIPURCH flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_RIPURCH", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_RIPURCH: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_RIPURCH: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_RIPURCH: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_RIPURCH: 심화검증 정상 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/src/vl/svc/VL_RISETTLE.pgc b/app/src/vl/svc/VL_RISETTLE.pgc index e8e1396..90ef071 100644 --- a/app/src/vl/svc/VL_RISETTLE.pgc +++ b/app/src/vl/svc/VL_RISETTLE.pgc @@ -8,6 +8,145 @@ void VL_RISETTLE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_RISETTLE: 요청 수신 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_RISETTLE: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_pid = getl(b, T_PURCHASE_ID); + if (v_pid < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "매입 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + EXEC SQL SELECT merchant_id, amount, fee, net, status + INTO :v_merch, :v_amt, :v_fee, :v_net, :v_pstat + FROM purchase WHERE purchase_id = :v_pid; + if (sqlca.sqlcode == 100) { + v_flag |= 0x01; /* 매입 원거래 미존재 */ + userlog("VL_RISETTLE: 매입 미존재 pid=%ld", v_pid); + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 81); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG3, v_net); + v_base = v_amt; + } + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_RISETTLE' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_RISETTLE: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (조건 분기) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_RISETTLE: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*) INTO :v_acnt + FROM approval WHERE purchase_id = :v_pid AND approved = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 83); Bchg(b, T_MSG, 0, "승인 대사 오류", 0L); FAIL(b); } + setl(b, T_ARG4, v_acnt); + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_lcnt, :v_lsum + FROM ledger WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 84); Bchg(b, T_MSG, 0, "원장 대사 오류", 0L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :v_scnt, :v_snet + FROM settlement WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 85); Bchg(b, T_MSG, 0, "정산 대사 오류", 0L); FAIL(b); } + if (v_acnt == 0) v_flag |= 0x08; /* 무승인 */ + if (v_lcnt == 0) v_flag |= 0x10; /* 원장 미기록 */ + if (v_scnt > 0 && v_snet != v_net) v_flag |= 0x20; /* 정산액 불일치 */ + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_RISETTLE flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_RISETTLE", v_pid, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_RISETTLE: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_RISETTLE: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_RISETTLE: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_RISETTLE: 심화검증 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/src/vl/svc/VL_RITAXINV.pgc b/app/src/vl/svc/VL_RITAXINV.pgc index 8d8112f..fb08039 100644 --- a/app/src/vl/svc/VL_RITAXINV.pgc +++ b/app/src/vl/svc/VL_RITAXINV.pgc @@ -8,6 +8,118 @@ void VL_RITAXINV(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_RITAXINV: 요청 수신 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_RITAXINV: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_RITAXINV' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_RITAXINV: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (룰 매칭) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_RITAXINV: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_RITAXINV flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_RITAXINV", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_RITAXINV: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_RITAXINV: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_RITAXINV: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_RITAXINV: 심화검증 정상 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; long h_cnt; diff --git a/app/src/vl/svc/VL_ROUNDAMT.pgc b/app/src/vl/svc/VL_ROUNDAMT.pgc index a2abac1..ea692a4 100644 --- a/app/src/vl/svc/VL_ROUNDAMT.pgc +++ b/app/src/vl/svc/VL_ROUNDAMT.pgc @@ -8,6 +8,118 @@ void VL_ROUNDAMT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_ROUNDAMT: 처리 개시 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_ROUNDAMT: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_ROUNDAMT' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_ROUNDAMT: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (조건 분기) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_ROUNDAMT: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_ROUNDAMT flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_ROUNDAMT", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_ROUNDAMT: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_ROUNDAMT: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_ROUNDAMT: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_ROUNDAMT: 심화검증 수행 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0, pct, thr; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/src/vl/svc/VL_RULE.pgc b/app/src/vl/svc/VL_RULE.pgc index 903bb09..121cf95 100644 --- a/app/src/vl/svc/VL_RULE.pgc +++ b/app/src/vl/svc/VL_RULE.pgc @@ -9,6 +9,119 @@ void VL_RULE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_RULE: 검증 착수 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_RULE: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + { char vk_rule[32]; gets_(b, T_STR3, vk_rule, sizeof(vk_rule)); + if (vk_rule[0]) { strncpy(v_rule, vk_rule, sizeof(v_rule)-1); v_rule[sizeof(v_rule)-1]=0; } } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule(v_rule, &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_RULE' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_RULE: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (룰 매칭) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_RULE: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_RULE flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_RULE", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_RULE: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_RULE: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_RULE: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_RULE: 심화검증 검증 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ char rule[32], name[64], sev[8]; long thr; gets_(b, T_STR3, rule, sizeof(rule)); diff --git a/app/src/vl/svc/VL_RULECOUNT.pgc b/app/src/vl/svc/VL_RULECOUNT.pgc index a49c32f..0f24372 100644 --- a/app/src/vl/svc/VL_RULECOUNT.pgc +++ b/app/src/vl/svc/VL_RULECOUNT.pgc @@ -8,6 +8,120 @@ void VL_RULECOUNT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_RULECOUNT: 요청 수신 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_RULECOUNT: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + { char vk_rule[32]; gets_(b, T_STR3, vk_rule, sizeof(vk_rule)); + if (vk_rule[0]) { strncpy(v_rule, vk_rule, sizeof(v_rule)-1); v_rule[sizeof(v_rule)-1]=0; } } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule(v_rule, &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_RULECOUNT' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_RULECOUNT: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (구간 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_RULECOUNT: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_RULECOUNT flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_RULECOUNT", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_RULECOUNT: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_RULECOUNT: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_RULECOUNT: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_RULECOUNT: 심화검증 정상 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; long h_info, h_warn, h_err, h_crit; diff --git a/app/src/vl/svc/VL_RULEFIND.pgc b/app/src/vl/svc/VL_RULEFIND.pgc index 661f475..b11f5ff 100644 --- a/app/src/vl/svc/VL_RULEFIND.pgc +++ b/app/src/vl/svc/VL_RULEFIND.pgc @@ -8,6 +8,120 @@ void VL_RULEFIND(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_RULEFIND: 요청 수신 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_RULEFIND: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + { char vk_rule[32]; gets_(b, T_STR3, vk_rule, sizeof(vk_rule)); + if (vk_rule[0]) { strncpy(v_rule, vk_rule, sizeof(v_rule)-1); v_rule[sizeof(v_rule)-1]=0; } } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule(v_rule, &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_RULEFIND' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_RULEFIND: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (업무규칙 적용) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_RULEFIND: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_RULEFIND flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_RULEFIND", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_RULEFIND: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_RULEFIND: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_RULEFIND: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_RULEFIND: 심화검증 수행 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ char rule[32], sev[8]; long thr = 0; gets_(b, T_STR3, rule, sizeof(rule)); diff --git a/app/src/vl/svc/VL_RULEHIST.pgc b/app/src/vl/svc/VL_RULEHIST.pgc index 9ad1ea5..cfd21f6 100644 --- a/app/src/vl/svc/VL_RULEHIST.pgc +++ b/app/src/vl/svc/VL_RULEHIST.pgc @@ -8,6 +8,125 @@ void VL_RULEHIST(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_RULEHIST: 요청 수신 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_RULEHIST: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + { char vk_rule[32]; gets_(b, T_STR3, vk_rule, sizeof(vk_rule)); + if (vk_rule[0]) { strncpy(v_rule, vk_rule, sizeof(v_rule)-1); v_rule[sizeof(v_rule)-1]=0; } } + if (getl(b, T_ID1) < 0) { + setl(b, T_RC, 95); + Bchg(b, T_MSG, 0, "의심거래 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule(v_rule, &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_RULEHIST' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_RULEHIST: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (구간 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_RULEHIST: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_RULEHIST flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_RULEHIST", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_RULEHIST: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_RULEHIST: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_RULEHIST: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_RULEHIST: 심화검증 정상 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; char rule[32]; EXEC SQL BEGIN DECLARE SECTION; diff --git a/app/src/vl/svc/VL_RULEMOD.pgc b/app/src/vl/svc/VL_RULEMOD.pgc index 1a339a3..8518245 100644 --- a/app/src/vl/svc/VL_RULEMOD.pgc +++ b/app/src/vl/svc/VL_RULEMOD.pgc @@ -8,6 +8,120 @@ void VL_RULEMOD(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_RULEMOD: 요청 수신 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_RULEMOD: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + { char vk_rule[32]; gets_(b, T_STR3, vk_rule, sizeof(vk_rule)); + if (vk_rule[0]) { strncpy(v_rule, vk_rule, sizeof(v_rule)-1); v_rule[sizeof(v_rule)-1]=0; } } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule(v_rule, &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_RULEMOD' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_RULEMOD: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (업무규칙 적용) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_RULEMOD: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_RULEMOD flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_RULEMOD", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_RULEMOD: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_RULEMOD: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_RULEMOD: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_RULEMOD: 심화검증 수행 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ char bizdate[16], hist[128]; EXEC SQL BEGIN DECLARE SECTION; char h_rule[32], h_sev[8]; diff --git a/app/src/vl/svc/VL_RULEOFF.pgc b/app/src/vl/svc/VL_RULEOFF.pgc index 85154f4..0a0abe2 100644 --- a/app/src/vl/svc/VL_RULEOFF.pgc +++ b/app/src/vl/svc/VL_RULEOFF.pgc @@ -8,6 +8,120 @@ void VL_RULEOFF(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_RULEOFF: 요청 수신 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_RULEOFF: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + { char vk_rule[32]; gets_(b, T_STR3, vk_rule, sizeof(vk_rule)); + if (vk_rule[0]) { strncpy(v_rule, vk_rule, sizeof(v_rule)-1); v_rule[sizeof(v_rule)-1]=0; } } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule(v_rule, &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_RULEOFF' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_RULEOFF: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (룰 매칭) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_RULEOFF: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_RULEOFF flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_RULEOFF", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_RULEOFF: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_RULEOFF: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_RULEOFF: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_RULEOFF: 심화검증 검증 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; char h_rule[32], h_st[16]; diff --git a/app/src/vl/svc/VL_RULEON.pgc b/app/src/vl/svc/VL_RULEON.pgc index 77023dc..ab05af0 100644 --- a/app/src/vl/svc/VL_RULEON.pgc +++ b/app/src/vl/svc/VL_RULEON.pgc @@ -8,6 +8,120 @@ void VL_RULEON(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_RULEON: 진입 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_RULEON: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + { char vk_rule[32]; gets_(b, T_STR3, vk_rule, sizeof(vk_rule)); + if (vk_rule[0]) { strncpy(v_rule, vk_rule, sizeof(v_rule)-1); v_rule[sizeof(v_rule)-1]=0; } } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule(v_rule, &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_RULEON' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_RULEON: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (조건 분기) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_RULEON: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_RULEON flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_RULEON", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_RULEON: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_RULEON: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_RULEON: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_RULEON: 심화검증 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ EXEC SQL BEGIN DECLARE SECTION; char h_rule[32], h_st[16]; EXEC SQL END DECLARE SECTION; diff --git a/app/src/vl/svc/VL_RULEREG.pgc b/app/src/vl/svc/VL_RULEREG.pgc index a5fd72d..7560485 100644 --- a/app/src/vl/svc/VL_RULEREG.pgc +++ b/app/src/vl/svc/VL_RULEREG.pgc @@ -9,6 +9,119 @@ void VL_RULEREG(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_RULEREG: 진입 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_RULEREG: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + { char vk_rule[32]; gets_(b, T_STR3, vk_rule, sizeof(vk_rule)); + if (vk_rule[0]) { strncpy(v_rule, vk_rule, sizeof(v_rule)-1); v_rule[sizeof(v_rule)-1]=0; } } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule(v_rule, &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_RULEREG' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_RULEREG: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (조건 분기) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_RULEREG: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_RULEREG flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_RULEREG", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_RULEREG: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_RULEREG: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_RULEREG: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_RULEREG: 심화검증 정상 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; char h_rule[32], h_name[64], h_sev[8]; diff --git a/app/src/vl/svc/VL_RULERENAME.pgc b/app/src/vl/svc/VL_RULERENAME.pgc index a680706..2a66a01 100644 --- a/app/src/vl/svc/VL_RULERENAME.pgc +++ b/app/src/vl/svc/VL_RULERENAME.pgc @@ -8,6 +8,120 @@ void VL_RULERENAME(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_RULERENAME: 진입 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_RULERENAME: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + { char vk_rule[32]; gets_(b, T_STR3, vk_rule, sizeof(vk_rule)); + if (vk_rule[0]) { strncpy(v_rule, vk_rule, sizeof(v_rule)-1); v_rule[sizeof(v_rule)-1]=0; } } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule(v_rule, &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_RULERENAME' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_RULERENAME: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (구간 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_RULERENAME: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_RULERENAME flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_RULERENAME", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_RULERENAME: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_RULERENAME: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_RULERENAME: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_RULERENAME: 심화검증 검증 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; char h_rule[32], h_name[64]; diff --git a/app/src/vl/svc/VL_RULESEV.pgc b/app/src/vl/svc/VL_RULESEV.pgc index fa3db66..bb75544 100644 --- a/app/src/vl/svc/VL_RULESEV.pgc +++ b/app/src/vl/svc/VL_RULESEV.pgc @@ -8,6 +8,120 @@ void VL_RULESEV(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_RULESEV: 진입 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_RULESEV: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + { char vk_rule[32]; gets_(b, T_STR3, vk_rule, sizeof(vk_rule)); + if (vk_rule[0]) { strncpy(v_rule, vk_rule, sizeof(v_rule)-1); v_rule[sizeof(v_rule)-1]=0; } } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule(v_rule, &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_RULESEV' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_RULESEV: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (구간 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_RULESEV: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_RULESEV flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_RULESEV", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_RULESEV: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_RULESEV: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_RULESEV: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_RULESEV: 심화검증 정상 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; char h_rule[32], h_sev[8]; diff --git a/app/src/vl/svc/VL_RULESTAT.pgc b/app/src/vl/svc/VL_RULESTAT.pgc index 6cc4d4e..dc1c961 100644 --- a/app/src/vl/svc/VL_RULESTAT.pgc +++ b/app/src/vl/svc/VL_RULESTAT.pgc @@ -8,6 +8,120 @@ void VL_RULESTAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_RULESTAT: 요청 수신 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_RULESTAT: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + { char vk_rule[32]; gets_(b, T_STR3, vk_rule, sizeof(vk_rule)); + if (vk_rule[0]) { strncpy(v_rule, vk_rule, sizeof(v_rule)-1); v_rule[sizeof(v_rule)-1]=0; } } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule(v_rule, &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_RULESTAT' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_RULESTAT: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (룰 매칭) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_RULESTAT: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_RULESTAT flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_RULESTAT", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_RULESTAT: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_RULESTAT: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_RULESTAT: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_RULESTAT: 심화검증 검증 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; long h_tot, h_act, h_crit; diff --git a/app/src/vl/svc/VL_RULEVER.pgc b/app/src/vl/svc/VL_RULEVER.pgc index ded716b..bd69df9 100644 --- a/app/src/vl/svc/VL_RULEVER.pgc +++ b/app/src/vl/svc/VL_RULEVER.pgc @@ -8,6 +8,119 @@ void VL_RULEVER(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_RULEVER: 수행 시작 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_RULEVER: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + { char vk_rule[32]; gets_(b, T_STR3, vk_rule, sizeof(vk_rule)); + if (vk_rule[0]) { strncpy(v_rule, vk_rule, sizeof(v_rule)-1); v_rule[sizeof(v_rule)-1]=0; } } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule(v_rule, &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_RULEVER' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_RULEVER: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (룰 매칭) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_RULEVER: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_RULEVER flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_RULEVER", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_RULEVER: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_RULEVER: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_RULEVER: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_RULEVER: 심화검증 수행 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; char h_rule[32]; diff --git a/app/src/vl/svc/VL_SALES.pgc b/app/src/vl/svc/VL_SALES.pgc index 391975f..aeae455 100644 --- a/app/src/vl/svc/VL_SALES.pgc +++ b/app/src/vl/svc/VL_SALES.pgc @@ -9,6 +9,145 @@ void VL_SALES(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_SALES: 요청 수신 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_SALES: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_pid = getl(b, T_PURCHASE_ID); + if (v_pid < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "매입 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + EXEC SQL SELECT merchant_id, amount, fee, net, status + INTO :v_merch, :v_amt, :v_fee, :v_net, :v_pstat + FROM purchase WHERE purchase_id = :v_pid; + if (sqlca.sqlcode == 100) { + v_flag |= 0x01; /* 매입 원거래 미존재 */ + userlog("VL_SALES: 매입 미존재 pid=%ld", v_pid); + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 81); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG3, v_net); + v_base = v_amt; + } + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_SALES' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_SALES: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (조건 분기) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_SALES: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*) INTO :v_acnt + FROM approval WHERE purchase_id = :v_pid AND approved = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 83); Bchg(b, T_MSG, 0, "승인 대사 오류", 0L); FAIL(b); } + setl(b, T_ARG4, v_acnt); + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_lcnt, :v_lsum + FROM ledger WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 84); Bchg(b, T_MSG, 0, "원장 대사 오류", 0L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :v_scnt, :v_snet + FROM settlement WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 85); Bchg(b, T_MSG, 0, "정산 대사 오류", 0L); FAIL(b); } + if (v_acnt == 0) v_flag |= 0x08; /* 무승인 */ + if (v_lcnt == 0) v_flag |= 0x10; /* 원장 미기록 */ + if (v_scnt > 0 && v_snet != v_net) v_flag |= 0x20; /* 정산액 불일치 */ + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_SALES flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_SALES", v_pid, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_SALES: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_SALES: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_SALES: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_SALES: 심화검증 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ char bizdate[16]; long rc = 0; EXEC SQL BEGIN DECLARE SECTION; diff --git a/app/src/vl/svc/VL_SETTLE.pgc b/app/src/vl/svc/VL_SETTLE.pgc index 79d0938..ecb6d84 100644 --- a/app/src/vl/svc/VL_SETTLE.pgc +++ b/app/src/vl/svc/VL_SETTLE.pgc @@ -9,6 +9,144 @@ void VL_SETTLE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_SETTLE: 요청 수신 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_SETTLE: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_pid = getl(b, T_PURCHASE_ID); + if (v_pid < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "매입 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + EXEC SQL SELECT merchant_id, amount, fee, net, status + INTO :v_merch, :v_amt, :v_fee, :v_net, :v_pstat + FROM purchase WHERE purchase_id = :v_pid; + if (sqlca.sqlcode == 100) { + v_flag |= 0x01; /* 매입 원거래 미존재 */ + userlog("VL_SETTLE: 매입 미존재 pid=%ld", v_pid); + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 81); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG3, v_net); + v_base = v_amt; + } + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_SETTLE' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_SETTLE: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (룰 매칭) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_SETTLE: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*) INTO :v_acnt + FROM approval WHERE purchase_id = :v_pid AND approved = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 83); Bchg(b, T_MSG, 0, "승인 대사 오류", 0L); FAIL(b); } + setl(b, T_ARG4, v_acnt); + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_lcnt, :v_lsum + FROM ledger WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 84); Bchg(b, T_MSG, 0, "원장 대사 오류", 0L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :v_scnt, :v_snet + FROM settlement WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 85); Bchg(b, T_MSG, 0, "정산 대사 오류", 0L); FAIL(b); } + if (v_acnt == 0) v_flag |= 0x08; /* 무승인 */ + if (v_lcnt == 0) v_flag |= 0x10; /* 원장 미기록 */ + if (v_scnt > 0 && v_snet != v_net) v_flag |= 0x20; /* 정산액 불일치 */ + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_SETTLE flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_SETTLE", v_pid, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_SETTLE: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_SETTLE: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_SETTLE: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_SETTLE: 심화검증 검증 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; long h_pid, h_pnet, h_snet, h_scnt; diff --git a/app/src/vl/svc/VL_SETTLREV.pgc b/app/src/vl/svc/VL_SETTLREV.pgc index c01ea32..6115250 100644 --- a/app/src/vl/svc/VL_SETTLREV.pgc +++ b/app/src/vl/svc/VL_SETTLREV.pgc @@ -8,6 +8,119 @@ void VL_SETTLREV(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_SETTLREV: 수행 시작 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_SETTLREV: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_SETTLREV' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_SETTLREV: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (업무규칙 적용) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_SETTLREV: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_SETTLREV flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_SETTLREV", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_SETTLREV: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_SETTLREV: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_SETTLREV: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_SETTLREV: 심화검증 수행 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/src/vl/svc/VL_SPIKEAMT.pgc b/app/src/vl/svc/VL_SPIKEAMT.pgc index d74a82a..f35072c 100644 --- a/app/src/vl/svc/VL_SPIKEAMT.pgc +++ b/app/src/vl/svc/VL_SPIKEAMT.pgc @@ -8,6 +8,118 @@ void VL_SPIKEAMT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_SPIKEAMT: 수행 시작 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_SPIKEAMT: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_SPIKEAMT' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_SPIKEAMT: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (조건 분기) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_SPIKEAMT: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_SPIKEAMT flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_SPIKEAMT", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_SPIKEAMT: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_SPIKEAMT: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_SPIKEAMT: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_SPIKEAMT: 심화검증 정상 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0, mult; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/src/vl/svc/VL_STATMIX.pgc b/app/src/vl/svc/VL_STATMIX.pgc index b90d85c..9dfc931 100644 --- a/app/src/vl/svc/VL_STATMIX.pgc +++ b/app/src/vl/svc/VL_STATMIX.pgc @@ -8,6 +8,118 @@ void VL_STATMIX(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_STATMIX: 검증 착수 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_STATMIX: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_STATMIX' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_STATMIX: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (룰 매칭) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_STATMIX: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_STATMIX flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_STATMIX", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_STATMIX: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_STATMIX: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_STATMIX: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_STATMIX: 심화검증 검증 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0, pct, thr; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/src/vl/svc/VL_STATUS.pgc b/app/src/vl/svc/VL_STATUS.pgc index 2ac9b5c..28adeeb 100644 --- a/app/src/vl/svc/VL_STATUS.pgc +++ b/app/src/vl/svc/VL_STATUS.pgc @@ -9,6 +9,118 @@ void VL_STATUS(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_STATUS: 검증 착수 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_STATUS: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_STATUS' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_STATUS: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (업무규칙 적용) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_STATUS: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_STATUS flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_STATUS", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_STATUS: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_STATUS: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_STATUS: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_STATUS: 심화검증 검증 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ EXEC SQL BEGIN DECLARE SECTION; long h_cnt; EXEC SQL END DECLARE SECTION; diff --git a/app/src/vl/svc/VL_STATUS_CHK.pgc b/app/src/vl/svc/VL_STATUS_CHK.pgc index 2a8b2df..be59aba 100644 --- a/app/src/vl/svc/VL_STATUS_CHK.pgc +++ b/app/src/vl/svc/VL_STATUS_CHK.pgc @@ -9,6 +9,144 @@ void VL_STATUS_CHK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_STATUS_CHK: 검증 착수 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_STATUS_CHK: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_pid = getl(b, T_PURCHASE_ID); + if (v_pid < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "매입 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + EXEC SQL SELECT merchant_id, amount, fee, net, status + INTO :v_merch, :v_amt, :v_fee, :v_net, :v_pstat + FROM purchase WHERE purchase_id = :v_pid; + if (sqlca.sqlcode == 100) { + v_flag |= 0x01; /* 매입 원거래 미존재 */ + userlog("VL_STATUS_CHK: 매입 미존재 pid=%ld", v_pid); + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 81); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG3, v_net); + v_base = v_amt; + } + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_STATUS_CHK' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_STATUS_CHK: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (룰 매칭) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_STATUS_CHK: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*) INTO :v_acnt + FROM approval WHERE purchase_id = :v_pid AND approved = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 83); Bchg(b, T_MSG, 0, "승인 대사 오류", 0L); FAIL(b); } + setl(b, T_ARG4, v_acnt); + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_lcnt, :v_lsum + FROM ledger WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 84); Bchg(b, T_MSG, 0, "원장 대사 오류", 0L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :v_scnt, :v_snet + FROM settlement WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 85); Bchg(b, T_MSG, 0, "정산 대사 오류", 0L); FAIL(b); } + if (v_acnt == 0) v_flag |= 0x08; /* 무승인 */ + if (v_lcnt == 0) v_flag |= 0x10; /* 원장 미기록 */ + if (v_scnt > 0 && v_snet != v_net) v_flag |= 0x20; /* 정산액 불일치 */ + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_STATUS_CHK flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_STATUS_CHK", v_pid, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_STATUS_CHK: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_STATUS_CHK: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_STATUS_CHK: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_STATUS_CHK: 심화검증 수행 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; long h_pid, h_cnt; diff --git a/app/src/vl/svc/VL_SUMFEE.pgc b/app/src/vl/svc/VL_SUMFEE.pgc index 7683ce4..a714bf6 100644 --- a/app/src/vl/svc/VL_SUMFEE.pgc +++ b/app/src/vl/svc/VL_SUMFEE.pgc @@ -8,6 +8,143 @@ void VL_SUMFEE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_SUMFEE: 처리 개시 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_SUMFEE: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + gets_(b, T_MERCHANT, v_merch, sizeof(v_merch)); + if (v_merch[0] && strlen(v_merch) > 32) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (v_merch[0]) { + EXEC SQL SELECT status, mdr_bps, daily_limit + INTO :v_status, :v_bps, :v_lim + FROM merchant WHERE merchant_id = :v_merch; + if (sqlca.sqlcode == 100) { + v_flag |= 0x02; /* 가맹점 마스터 미존재 */ + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 82); Bchg(b, T_MSG, 0, "가맹점 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG7, v_bps); + if (strcmp(v_status, "ACTIVE") != 0) v_flag |= 0x04; /* 상태부적합 */ + } + } + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_SUMFEE' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_SUMFEE: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (조건 분기) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_SUMFEE: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + if (v_merch[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0) + INTO :v_pcnt, :v_pgross, :v_pfee + FROM purchase + WHERE merchant_id = :v_merch AND biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 86); Bchg(b, T_MSG, 0, "매입 재집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_pcnt); + setl(b, T_AMT3, v_pgross); + v_base = v_pgross; + } + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_SUMFEE flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_SUMFEE", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_SUMFEE: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_SUMFEE: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_SUMFEE: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_SUMFEE: 심화검증 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; char h_merch[64], h_bizdate[16]; diff --git a/app/src/vl/svc/VL_SUMFEECHK.pgc b/app/src/vl/svc/VL_SUMFEECHK.pgc index 58dab7c..68a3d82 100644 --- a/app/src/vl/svc/VL_SUMFEECHK.pgc +++ b/app/src/vl/svc/VL_SUMFEECHK.pgc @@ -8,6 +8,119 @@ void VL_SUMFEECHK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_SUMFEECHK: 요청 수신 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_SUMFEECHK: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_SUMFEECHK' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_SUMFEECHK: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (룰 매칭) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_SUMFEECHK: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_SUMFEECHK flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_SUMFEECHK", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_SUMFEECHK: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_SUMFEECHK: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_SUMFEECHK: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_SUMFEECHK: 심화검증 검증 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/src/vl/svc/VL_SUMGROSS.pgc b/app/src/vl/svc/VL_SUMGROSS.pgc index b95bcee..af7afbe 100644 --- a/app/src/vl/svc/VL_SUMGROSS.pgc +++ b/app/src/vl/svc/VL_SUMGROSS.pgc @@ -8,6 +8,143 @@ void VL_SUMGROSS(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_SUMGROSS: 처리 개시 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_SUMGROSS: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + gets_(b, T_MERCHANT, v_merch, sizeof(v_merch)); + if (v_merch[0] && strlen(v_merch) > 32) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (v_merch[0]) { + EXEC SQL SELECT status, mdr_bps, daily_limit + INTO :v_status, :v_bps, :v_lim + FROM merchant WHERE merchant_id = :v_merch; + if (sqlca.sqlcode == 100) { + v_flag |= 0x02; /* 가맹점 마스터 미존재 */ + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 82); Bchg(b, T_MSG, 0, "가맹점 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG7, v_bps); + if (strcmp(v_status, "ACTIVE") != 0) v_flag |= 0x04; /* 상태부적합 */ + } + } + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_SUMGROSS' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_SUMGROSS: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (분기 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_SUMGROSS: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + if (v_merch[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0) + INTO :v_pcnt, :v_pgross, :v_pfee + FROM purchase + WHERE merchant_id = :v_merch AND biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 86); Bchg(b, T_MSG, 0, "매입 재집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_pcnt); + setl(b, T_AMT3, v_pgross); + v_base = v_pgross; + } + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_SUMGROSS flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_SUMGROSS", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_SUMGROSS: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_SUMGROSS: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_SUMGROSS: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_SUMGROSS: 심화검증 검증 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; char h_merch[64], h_bizdate[16]; diff --git a/app/src/vl/svc/VL_SUMMARY.pgc b/app/src/vl/svc/VL_SUMMARY.pgc index 2ceaaaa..2cb4b85 100644 --- a/app/src/vl/svc/VL_SUMMARY.pgc +++ b/app/src/vl/svc/VL_SUMMARY.pgc @@ -9,6 +9,119 @@ void VL_SUMMARY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_SUMMARY: 검증 착수 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_SUMMARY: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_SUMMARY' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_SUMMARY: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (조건 분기) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_SUMMARY: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_SUMMARY flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_SUMMARY", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_SUMMARY: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_SUMMARY: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_SUMMARY: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_SUMMARY: 심화검증 수행 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ char bizdate[16]; long cnt; gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); diff --git a/app/src/vl/svc/VL_SUMNETCHK.pgc b/app/src/vl/svc/VL_SUMNETCHK.pgc index b9ebf06..6b49dda 100644 --- a/app/src/vl/svc/VL_SUMNETCHK.pgc +++ b/app/src/vl/svc/VL_SUMNETCHK.pgc @@ -8,6 +8,143 @@ void VL_SUMNETCHK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_SUMNETCHK: 요청 수신 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_SUMNETCHK: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + gets_(b, T_MERCHANT, v_merch, sizeof(v_merch)); + if (v_merch[0] && strlen(v_merch) > 32) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (v_merch[0]) { + EXEC SQL SELECT status, mdr_bps, daily_limit + INTO :v_status, :v_bps, :v_lim + FROM merchant WHERE merchant_id = :v_merch; + if (sqlca.sqlcode == 100) { + v_flag |= 0x02; /* 가맹점 마스터 미존재 */ + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 82); Bchg(b, T_MSG, 0, "가맹점 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG7, v_bps); + if (strcmp(v_status, "ACTIVE") != 0) v_flag |= 0x04; /* 상태부적합 */ + } + } + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_SUMNETCHK' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_SUMNETCHK: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (룰 매칭) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_SUMNETCHK: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + if (v_merch[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0) + INTO :v_pcnt, :v_pgross, :v_pfee + FROM purchase + WHERE merchant_id = :v_merch AND biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 86); Bchg(b, T_MSG, 0, "매입 재집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_pcnt); + setl(b, T_AMT3, v_pgross); + v_base = v_pgross; + } + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_SUMNETCHK flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_SUMNETCHK", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_SUMNETCHK: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_SUMNETCHK: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_SUMNETCHK: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_SUMNETCHK: 심화검증 수행 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; char h_merch[64], h_bizdate[16]; diff --git a/app/src/vl/svc/VL_SUMRECON.pgc b/app/src/vl/svc/VL_SUMRECON.pgc index 4d2195f..5199fd5 100644 --- a/app/src/vl/svc/VL_SUMRECON.pgc +++ b/app/src/vl/svc/VL_SUMRECON.pgc @@ -8,6 +8,143 @@ void VL_SUMRECON(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_SUMRECON: 요청 수신 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_SUMRECON: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + gets_(b, T_MERCHANT, v_merch, sizeof(v_merch)); + if (v_merch[0] && strlen(v_merch) > 32) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (v_merch[0]) { + EXEC SQL SELECT status, mdr_bps, daily_limit + INTO :v_status, :v_bps, :v_lim + FROM merchant WHERE merchant_id = :v_merch; + if (sqlca.sqlcode == 100) { + v_flag |= 0x02; /* 가맹점 마스터 미존재 */ + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 82); Bchg(b, T_MSG, 0, "가맹점 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG7, v_bps); + if (strcmp(v_status, "ACTIVE") != 0) v_flag |= 0x04; /* 상태부적합 */ + } + } + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_SUMRECON' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_SUMRECON: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (룰 매칭) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_SUMRECON: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + if (v_merch[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0) + INTO :v_pcnt, :v_pgross, :v_pfee + FROM purchase + WHERE merchant_id = :v_merch AND biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 86); Bchg(b, T_MSG, 0, "매입 재집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_pcnt); + setl(b, T_AMT3, v_pgross); + v_base = v_pgross; + } + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_SUMRECON flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_SUMRECON", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_SUMRECON: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_SUMRECON: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_SUMRECON: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_SUMRECON: 심화검증 처리 종결 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; char h_merch[64], h_bizdate[16]; diff --git a/app/src/vl/svc/VL_SUMTXNCNT.pgc b/app/src/vl/svc/VL_SUMTXNCNT.pgc index 2188594..6c46638 100644 --- a/app/src/vl/svc/VL_SUMTXNCNT.pgc +++ b/app/src/vl/svc/VL_SUMTXNCNT.pgc @@ -8,6 +8,143 @@ void VL_SUMTXNCNT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_SUMTXNCNT: 진입 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_SUMTXNCNT: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + gets_(b, T_MERCHANT, v_merch, sizeof(v_merch)); + if (v_merch[0] && strlen(v_merch) > 32) { + setl(b, T_RC, 92); + Bchg(b, T_MSG, 0, "가맹점 ID 길이 초과", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (v_merch[0]) { + EXEC SQL SELECT status, mdr_bps, daily_limit + INTO :v_status, :v_bps, :v_lim + FROM merchant WHERE merchant_id = :v_merch; + if (sqlca.sqlcode == 100) { + v_flag |= 0x02; /* 가맹점 마스터 미존재 */ + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 82); Bchg(b, T_MSG, 0, "가맹점 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG7, v_bps); + if (strcmp(v_status, "ACTIVE") != 0) v_flag |= 0x04; /* 상태부적합 */ + } + } + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_SUMTXNCNT' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_SUMTXNCNT: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (조건 분기) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_SUMTXNCNT: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + if (v_merch[0]) { + EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0) + INTO :v_pcnt, :v_pgross, :v_pfee + FROM purchase + WHERE merchant_id = :v_merch AND biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 86); Bchg(b, T_MSG, 0, "매입 재집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_pcnt); + setl(b, T_AMT3, v_pgross); + v_base = v_pgross; + } + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_SUMTXNCNT flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_SUMTXNCNT", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_SUMTXNCNT: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_SUMTXNCNT: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_SUMTXNCNT: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_SUMTXNCNT: 심화검증 검증 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; char h_merch[64], h_bizdate[16]; diff --git a/app/src/vl/svc/VL_SUSPACK.pgc b/app/src/vl/svc/VL_SUSPACK.pgc index 92e8dea..a84a9da 100644 --- a/app/src/vl/svc/VL_SUSPACK.pgc +++ b/app/src/vl/svc/VL_SUSPACK.pgc @@ -8,6 +8,123 @@ void VL_SUSPACK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_SUSPACK: 요청 수신 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_SUSPACK: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + if (getl(b, T_ID1) < 0) { + setl(b, T_RC, 95); + Bchg(b, T_MSG, 0, "의심거래 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_SUSPACK' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_SUSPACK: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (업무규칙 적용) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_SUSPACK: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_SUSPACK flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_SUSPACK", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_SUSPACK: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_SUSPACK: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_SUSPACK: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_SUSPACK: 심화검증 수행 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0, rows; EXEC SQL BEGIN DECLARE SECTION; long h_sid; diff --git a/app/src/vl/svc/VL_SUSPACT.pgc b/app/src/vl/svc/VL_SUSPACT.pgc index 3e2eb9f..c897917 100644 --- a/app/src/vl/svc/VL_SUSPACT.pgc +++ b/app/src/vl/svc/VL_SUSPACT.pgc @@ -8,6 +8,150 @@ void VL_SUSPACT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_SUSPACT: 검증 착수 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_SUSPACT: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_pid = getl(b, T_PURCHASE_ID); + if (v_pid < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "매입 ID 음수 불가", 0L); + FAIL(b); + } + if (getl(b, T_ID1) < 0) { + setl(b, T_RC, 95); + Bchg(b, T_MSG, 0, "의심거래 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + EXEC SQL SELECT merchant_id, amount, fee, net, status + INTO :v_merch, :v_amt, :v_fee, :v_net, :v_pstat + FROM purchase WHERE purchase_id = :v_pid; + if (sqlca.sqlcode == 100) { + v_flag |= 0x01; /* 매입 원거래 미존재 */ + userlog("VL_SUSPACT: 매입 미존재 pid=%ld", v_pid); + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 81); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG3, v_net); + v_base = v_amt; + } + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_SUSPACT' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_SUSPACT: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (구간 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_SUSPACT: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*) INTO :v_acnt + FROM approval WHERE purchase_id = :v_pid AND approved = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 83); Bchg(b, T_MSG, 0, "승인 대사 오류", 0L); FAIL(b); } + setl(b, T_ARG4, v_acnt); + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_lcnt, :v_lsum + FROM ledger WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 84); Bchg(b, T_MSG, 0, "원장 대사 오류", 0L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :v_scnt, :v_snet + FROM settlement WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 85); Bchg(b, T_MSG, 0, "정산 대사 오류", 0L); FAIL(b); } + if (v_acnt == 0) v_flag |= 0x08; /* 무승인 */ + if (v_lcnt == 0) v_flag |= 0x10; /* 원장 미기록 */ + if (v_scnt > 0 && v_snet != v_net) v_flag |= 0x20; /* 정산액 불일치 */ + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_SUSPACT flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_SUSPACT", v_pid, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_SUSPACT: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_SUSPACT: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_SUSPACT: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_SUSPACT: 심화검증 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ char bizdate[16]; EXEC SQL BEGIN DECLARE SECTION; long h_sid, h_pid; diff --git a/app/src/vl/svc/VL_SUSPAGEAVG.pgc b/app/src/vl/svc/VL_SUSPAGEAVG.pgc index 7ad98a7..f5eeb01 100644 --- a/app/src/vl/svc/VL_SUSPAGEAVG.pgc +++ b/app/src/vl/svc/VL_SUSPAGEAVG.pgc @@ -8,6 +8,117 @@ void VL_SUSPAGEAVG(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_SUSPAGEAVG: 수행 시작 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_SUSPAGEAVG: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_SUSPAGEAVG' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_SUSPAGEAVG: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (룰 매칭) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_SUSPAGEAVG: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_SUSPAGEAVG flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_SUSPAGEAVG", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_SUSPAGEAVG: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_SUSPAGEAVG: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_SUSPAGEAVG: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_SUSPAGEAVG: 심화검증 정상 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0, thr; EXEC SQL BEGIN DECLARE SECTION; long h_cnt, h_avg; diff --git a/app/src/vl/svc/VL_SUSPASSIGN.pgc b/app/src/vl/svc/VL_SUSPASSIGN.pgc index 9ae52b4..868ea81 100644 --- a/app/src/vl/svc/VL_SUSPASSIGN.pgc +++ b/app/src/vl/svc/VL_SUSPASSIGN.pgc @@ -8,6 +8,123 @@ void VL_SUSPASSIGN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_SUSPASSIGN: 요청 수신 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_SUSPASSIGN: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + if (getl(b, T_ID1) < 0) { + setl(b, T_RC, 95); + Bchg(b, T_MSG, 0, "의심거래 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_SUSPASSIGN' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_SUSPASSIGN: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (룰 매칭) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_SUSPASSIGN: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_SUSPASSIGN flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_SUSPASSIGN", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_SUSPASSIGN: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_SUSPASSIGN: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_SUSPASSIGN: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_SUSPASSIGN: 심화검증 검증 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0, rows; EXEC SQL BEGIN DECLARE SECTION; long h_sid; diff --git a/app/src/vl/svc/VL_SUSPBULK.pgc b/app/src/vl/svc/VL_SUSPBULK.pgc index 959684f..dc070db 100644 --- a/app/src/vl/svc/VL_SUSPBULK.pgc +++ b/app/src/vl/svc/VL_SUSPBULK.pgc @@ -8,6 +8,119 @@ void VL_SUSPBULK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_SUSPBULK: 수행 시작 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_SUSPBULK: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_SUSPBULK' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_SUSPBULK: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (조건 분기) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_SUSPBULK: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_SUSPBULK flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_SUSPBULK", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_SUSPBULK: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_SUSPBULK: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_SUSPBULK: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_SUSPBULK: 심화검증 수행 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0, rows; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/src/vl/svc/VL_SUSPESCAL.pgc b/app/src/vl/svc/VL_SUSPESCAL.pgc index 1450292..b3f1984 100644 --- a/app/src/vl/svc/VL_SUSPESCAL.pgc +++ b/app/src/vl/svc/VL_SUSPESCAL.pgc @@ -8,6 +8,123 @@ void VL_SUSPESCAL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_SUSPESCAL: 처리 개시 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_SUSPESCAL: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + if (getl(b, T_ID1) < 0) { + setl(b, T_RC, 95); + Bchg(b, T_MSG, 0, "의심거래 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_SUSPESCAL' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_SUSPESCAL: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (구간 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_SUSPESCAL: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_SUSPESCAL flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_SUSPESCAL", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_SUSPESCAL: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_SUSPESCAL: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_SUSPESCAL: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_SUSPESCAL: 심화검증 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ EXEC SQL BEGIN DECLARE SECTION; long h_sid; char h_sev[8], h_new[8]; diff --git a/app/src/vl/svc/VL_SUSPFINAL.pgc b/app/src/vl/svc/VL_SUSPFINAL.pgc index 027c1f1..9ac48d7 100644 --- a/app/src/vl/svc/VL_SUSPFINAL.pgc +++ b/app/src/vl/svc/VL_SUSPFINAL.pgc @@ -8,6 +8,123 @@ void VL_SUSPFINAL(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_SUSPFINAL: 검증 착수 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_SUSPFINAL: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + if (getl(b, T_ID1) < 0) { + setl(b, T_RC, 95); + Bchg(b, T_MSG, 0, "의심거래 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_SUSPFINAL' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_SUSPFINAL: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (룰 매칭) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_SUSPFINAL: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_SUSPFINAL flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_SUSPFINAL", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_SUSPFINAL: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_SUSPFINAL: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_SUSPFINAL: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_SUSPFINAL: 심화검증 수행 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ EXEC SQL BEGIN DECLARE SECTION; long h_sid; char h_st[16]; diff --git a/app/src/vl/svc/VL_SUSPNOTE.pgc b/app/src/vl/svc/VL_SUSPNOTE.pgc index 14515e0..31e3573 100644 --- a/app/src/vl/svc/VL_SUSPNOTE.pgc +++ b/app/src/vl/svc/VL_SUSPNOTE.pgc @@ -8,6 +8,123 @@ void VL_SUSPNOTE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_SUSPNOTE: 진입 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_SUSPNOTE: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + if (getl(b, T_ID1) < 0) { + setl(b, T_RC, 95); + Bchg(b, T_MSG, 0, "의심거래 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_SUSPNOTE' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_SUSPNOTE: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (분기 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_SUSPNOTE: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_SUSPNOTE flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_SUSPNOTE", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_SUSPNOTE: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_SUSPNOTE: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_SUSPNOTE: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_SUSPNOTE: 심화검증 정상 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; long h_sid; diff --git a/app/src/vl/svc/VL_SUSPRECHK.pgc b/app/src/vl/svc/VL_SUSPRECHK.pgc index 2f7b80d..c4dfae0 100644 --- a/app/src/vl/svc/VL_SUSPRECHK.pgc +++ b/app/src/vl/svc/VL_SUSPRECHK.pgc @@ -8,6 +8,123 @@ void VL_SUSPRECHK(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_SUSPRECHK: 검증 착수 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_SUSPRECHK: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + if (getl(b, T_ID1) < 0) { + setl(b, T_RC, 95); + Bchg(b, T_MSG, 0, "의심거래 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_SUSPRECHK' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_SUSPRECHK: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (업무규칙 적용) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_SUSPRECHK: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_SUSPRECHK flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_SUSPRECHK", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_SUSPRECHK: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_SUSPRECHK: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_SUSPRECHK: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_SUSPRECHK: 심화검증 수행 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0, rows; EXEC SQL BEGIN DECLARE SECTION; long h_sid; diff --git a/app/src/vl/svc/VL_SUSPREOPEN.pgc b/app/src/vl/svc/VL_SUSPREOPEN.pgc index 8078dfd..3010c98 100644 --- a/app/src/vl/svc/VL_SUSPREOPEN.pgc +++ b/app/src/vl/svc/VL_SUSPREOPEN.pgc @@ -8,6 +8,123 @@ void VL_SUSPREOPEN(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_SUSPREOPEN: 요청 수신 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_SUSPREOPEN: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + if (getl(b, T_ID1) < 0) { + setl(b, T_RC, 95); + Bchg(b, T_MSG, 0, "의심거래 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_SUSPREOPEN' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_SUSPREOPEN: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (업무규칙 적용) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_SUSPREOPEN: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_SUSPREOPEN flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_SUSPREOPEN", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_SUSPREOPEN: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_SUSPREOPEN: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_SUSPREOPEN: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_SUSPREOPEN: 심화검증 처리 종결 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; long h_sid; diff --git a/app/src/vl/svc/VL_SUSPSTAT.pgc b/app/src/vl/svc/VL_SUSPSTAT.pgc index 8b1549d..8ebb71e 100644 --- a/app/src/vl/svc/VL_SUSPSTAT.pgc +++ b/app/src/vl/svc/VL_SUSPSTAT.pgc @@ -8,6 +8,119 @@ void VL_SUSPSTAT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_SUSPSTAT: 처리 개시 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_SUSPSTAT: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_SUSPSTAT' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_SUSPSTAT: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (구간 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_SUSPSTAT: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_SUSPSTAT flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_SUSPSTAT", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_SUSPSTAT: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_SUSPSTAT: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_SUSPSTAT: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_SUSPSTAT: 심화검증 검증 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/src/vl/svc/VL_SUSPTRIAGE.pgc b/app/src/vl/svc/VL_SUSPTRIAGE.pgc index b51f9e0..1d06690 100644 --- a/app/src/vl/svc/VL_SUSPTRIAGE.pgc +++ b/app/src/vl/svc/VL_SUSPTRIAGE.pgc @@ -8,6 +8,123 @@ void VL_SUSPTRIAGE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_SUSPTRIAGE: 수행 시작 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_SUSPTRIAGE: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + if (getl(b, T_ID1) < 0) { + setl(b, T_RC, 95); + Bchg(b, T_MSG, 0, "의심거래 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_SUSPTRIAGE' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_SUSPTRIAGE: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (업무규칙 적용) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_SUSPTRIAGE: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_SUSPTRIAGE flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_SUSPTRIAGE", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_SUSPTRIAGE: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_SUSPTRIAGE: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_SUSPTRIAGE: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_SUSPTRIAGE: 심화검증 처리 종결 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0, rows; EXEC SQL BEGIN DECLARE SECTION; long h_sid; diff --git a/app/src/vl/svc/VL_THRAUDIT.pgc b/app/src/vl/svc/VL_THRAUDIT.pgc index a00d3bc..039b735 100644 --- a/app/src/vl/svc/VL_THRAUDIT.pgc +++ b/app/src/vl/svc/VL_THRAUDIT.pgc @@ -8,6 +8,120 @@ void VL_THRAUDIT(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_THRAUDIT: 검증 착수 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_THRAUDIT: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + { char vk_rule[32]; gets_(b, T_STR3, vk_rule, sizeof(vk_rule)); + if (vk_rule[0]) { strncpy(v_rule, vk_rule, sizeof(v_rule)-1); v_rule[sizeof(v_rule)-1]=0; } } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule(v_rule, &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_THRAUDIT' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_THRAUDIT: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (조건 분기) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_THRAUDIT: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_THRAUDIT flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_THRAUDIT", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_THRAUDIT: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_THRAUDIT: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_THRAUDIT: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_THRAUDIT: 심화검증 정상 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; long h_bad, h_off; diff --git a/app/src/vl/svc/VL_THRBUMP.pgc b/app/src/vl/svc/VL_THRBUMP.pgc index c0b5314..4cda01e 100644 --- a/app/src/vl/svc/VL_THRBUMP.pgc +++ b/app/src/vl/svc/VL_THRBUMP.pgc @@ -8,6 +8,120 @@ void VL_THRBUMP(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_THRBUMP: 검증 착수 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_THRBUMP: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + { char vk_rule[32]; gets_(b, T_STR3, vk_rule, sizeof(vk_rule)); + if (vk_rule[0]) { strncpy(v_rule, vk_rule, sizeof(v_rule)-1); v_rule[sizeof(v_rule)-1]=0; } } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule(v_rule, &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_THRBUMP' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_THRBUMP: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (구간 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_THRBUMP: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_THRBUMP flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_THRBUMP", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_THRBUMP: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_THRBUMP: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_THRBUMP: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_THRBUMP: 심화검증 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0, delta; EXEC SQL BEGIN DECLARE SECTION; char h_rule[32]; diff --git a/app/src/vl/svc/VL_THRCOMPARE.pgc b/app/src/vl/svc/VL_THRCOMPARE.pgc index 064548d..922b08c 100644 --- a/app/src/vl/svc/VL_THRCOMPARE.pgc +++ b/app/src/vl/svc/VL_THRCOMPARE.pgc @@ -8,6 +8,119 @@ void VL_THRCOMPARE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_THRCOMPARE: 수행 시작 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_THRCOMPARE: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + { char vk_rule[32]; gets_(b, T_STR3, vk_rule, sizeof(vk_rule)); + if (vk_rule[0]) { strncpy(v_rule, vk_rule, sizeof(v_rule)-1); v_rule[sizeof(v_rule)-1]=0; } } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule(v_rule, &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_THRCOMPARE' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_THRCOMPARE: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (구간 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_THRCOMPARE: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_THRCOMPARE flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_THRCOMPARE", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_THRCOMPARE: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_THRCOMPARE: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_THRCOMPARE: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_THRCOMPARE: 심화검증 검증 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ char rule[32], sev[8]; long measured, thr = 0, rc = 0; gets_(b, T_STR3, rule, sizeof(rule)); diff --git a/app/src/vl/svc/VL_THRESHOLD.pgc b/app/src/vl/svc/VL_THRESHOLD.pgc index 53b4a26..af9fb2e 100644 --- a/app/src/vl/svc/VL_THRESHOLD.pgc +++ b/app/src/vl/svc/VL_THRESHOLD.pgc @@ -9,6 +9,126 @@ void VL_THRESHOLD(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_THRESHOLD: 진입 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_THRESHOLD: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_amt = getl(b, T_AMOUNT); + if (v_amt < 0) { + setl(b, T_RC, 94); + Bchg(b, T_MSG, 0, "매입 금액 음수 불가", 0L); + FAIL(b); + } + { char vk_rule[32]; gets_(b, T_STR3, vk_rule, sizeof(vk_rule)); + if (vk_rule[0]) { strncpy(v_rule, vk_rule, sizeof(v_rule)-1); v_rule[sizeof(v_rule)-1]=0; } } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule(v_rule, &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_THRESHOLD' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_THRESHOLD: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (조건 분기) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_THRESHOLD: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_THRESHOLD flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_THRESHOLD", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_THRESHOLD: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_THRESHOLD: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_THRESHOLD: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_THRESHOLD: 심화검증 검증 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ char rule[32], sev[8]; long amt, thr = 0, rc = 0; gets_(b, T_STR3, rule, sizeof(rule)); if (rule[0] == 0) strcpy(rule, "AMT_MAX"); diff --git a/app/src/vl/svc/VL_THRGET.pgc b/app/src/vl/svc/VL_THRGET.pgc index 2738647..1e38ec9 100644 --- a/app/src/vl/svc/VL_THRGET.pgc +++ b/app/src/vl/svc/VL_THRGET.pgc @@ -8,6 +8,120 @@ void VL_THRGET(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_THRGET: 처리 개시 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_THRGET: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + { char vk_rule[32]; gets_(b, T_STR3, vk_rule, sizeof(vk_rule)); + if (vk_rule[0]) { strncpy(v_rule, vk_rule, sizeof(v_rule)-1); v_rule[sizeof(v_rule)-1]=0; } } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule(v_rule, &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_THRGET' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_THRGET: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (업무규칙 적용) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_THRGET: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_THRGET flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_THRGET", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_THRGET: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_THRGET: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_THRGET: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_THRGET: 심화검증 정상 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ EXEC SQL BEGIN DECLARE SECTION; char h_rule[32], h_sev[8], h_st[16]; long h_thr; diff --git a/app/src/vl/svc/VL_THRRESET.pgc b/app/src/vl/svc/VL_THRRESET.pgc index c4aa85b..579472c 100644 --- a/app/src/vl/svc/VL_THRRESET.pgc +++ b/app/src/vl/svc/VL_THRRESET.pgc @@ -8,6 +8,120 @@ void VL_THRRESET(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_THRRESET: 검증 착수 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_THRRESET: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + { char vk_rule[32]; gets_(b, T_STR3, vk_rule, sizeof(vk_rule)); + if (vk_rule[0]) { strncpy(v_rule, vk_rule, sizeof(v_rule)-1); v_rule[sizeof(v_rule)-1]=0; } } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule(v_rule, &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_THRRESET' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_THRRESET: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (구간 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_THRRESET: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_THRRESET flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_THRRESET", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_THRRESET: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_THRRESET: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_THRRESET: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_THRRESET: 심화검증 검증 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; char h_rule[32]; diff --git a/app/src/vl/svc/VL_THRSCALE.pgc b/app/src/vl/svc/VL_THRSCALE.pgc index bbfd851..24b5b82 100644 --- a/app/src/vl/svc/VL_THRSCALE.pgc +++ b/app/src/vl/svc/VL_THRSCALE.pgc @@ -8,6 +8,119 @@ void VL_THRSCALE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_THRSCALE: 검증 착수 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_THRSCALE: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + { char vk_rule[32]; gets_(b, T_STR3, vk_rule, sizeof(vk_rule)); + if (vk_rule[0]) { strncpy(v_rule, vk_rule, sizeof(v_rule)-1); v_rule[sizeof(v_rule)-1]=0; } } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule(v_rule, &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_THRSCALE' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_THRSCALE: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (분기 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_THRSCALE: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_THRSCALE flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_THRSCALE", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_THRSCALE: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_THRSCALE: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_THRSCALE: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_THRSCALE: 심화검증 검증 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long factor; EXEC SQL BEGIN DECLARE SECTION; char h_rule[32]; diff --git a/app/src/vl/svc/VL_THRSET.pgc b/app/src/vl/svc/VL_THRSET.pgc index 4206dd1..9d360ee 100644 --- a/app/src/vl/svc/VL_THRSET.pgc +++ b/app/src/vl/svc/VL_THRSET.pgc @@ -8,6 +8,119 @@ void VL_THRSET(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_THRSET: 처리 개시 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_THRSET: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + { char vk_rule[32]; gets_(b, T_STR3, vk_rule, sizeof(vk_rule)); + if (vk_rule[0]) { strncpy(v_rule, vk_rule, sizeof(v_rule)-1); v_rule[sizeof(v_rule)-1]=0; } } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule(v_rule, &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_THRSET' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_THRSET: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (업무규칙 적용) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_THRSET: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_THRSET flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_THRSET", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_THRSET: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_THRSET: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_THRSET: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_THRSET: 심화검증 처리 종결 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; char h_rule[32]; diff --git a/app/src/vl/svc/VL_TOLERANCE.pgc b/app/src/vl/svc/VL_TOLERANCE.pgc index e860a69..289226e 100644 --- a/app/src/vl/svc/VL_TOLERANCE.pgc +++ b/app/src/vl/svc/VL_TOLERANCE.pgc @@ -9,6 +9,143 @@ void VL_TOLERANCE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_TOLERANCE: 처리 개시 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_TOLERANCE: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_pid = getl(b, T_PURCHASE_ID); + if (v_pid < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "매입 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + EXEC SQL SELECT merchant_id, amount, fee, net, status + INTO :v_merch, :v_amt, :v_fee, :v_net, :v_pstat + FROM purchase WHERE purchase_id = :v_pid; + if (sqlca.sqlcode == 100) { + v_flag |= 0x01; /* 매입 원거래 미존재 */ + userlog("VL_TOLERANCE: 매입 미존재 pid=%ld", v_pid); + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 81); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG3, v_net); + v_base = v_amt; + } + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_TOLERANCE' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_TOLERANCE: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (조건 분기) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_TOLERANCE: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*) INTO :v_acnt + FROM approval WHERE purchase_id = :v_pid AND approved = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 83); Bchg(b, T_MSG, 0, "승인 대사 오류", 0L); FAIL(b); } + setl(b, T_ARG4, v_acnt); + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_lcnt, :v_lsum + FROM ledger WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 84); Bchg(b, T_MSG, 0, "원장 대사 오류", 0L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :v_scnt, :v_snet + FROM settlement WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 85); Bchg(b, T_MSG, 0, "정산 대사 오류", 0L); FAIL(b); } + if (v_acnt == 0) v_flag |= 0x08; /* 무승인 */ + if (v_lcnt == 0) v_flag |= 0x10; /* 원장 미기록 */ + if (v_scnt > 0 && v_snet != v_net) v_flag |= 0x20; /* 정산액 불일치 */ + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_TOLERANCE flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_TOLERANCE", v_pid, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_TOLERANCE: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_TOLERANCE: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_TOLERANCE: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_TOLERANCE: 심화검증 정상 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0, tol, diff; EXEC SQL BEGIN DECLARE SECTION; long h_pid, h_amount, h_fee, h_net; diff --git a/app/src/vl/svc/VL_TRIAGG.pgc b/app/src/vl/svc/VL_TRIAGG.pgc index 0824f12..2432a7f 100644 --- a/app/src/vl/svc/VL_TRIAGG.pgc +++ b/app/src/vl/svc/VL_TRIAGG.pgc @@ -9,6 +9,119 @@ void VL_TRIAGG(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_TRIAGG: 진입 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_TRIAGG: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_TRIAGG' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_TRIAGG: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (업무규칙 적용) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_TRIAGG: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_TRIAGG flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_TRIAGG", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_TRIAGG: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_TRIAGG: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_TRIAGG: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_TRIAGG: 심화검증 처리 종결 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/src/vl/svc/VL_TRISTAGE.pgc b/app/src/vl/svc/VL_TRISTAGE.pgc index 373e10e..8f16018 100644 --- a/app/src/vl/svc/VL_TRISTAGE.pgc +++ b/app/src/vl/svc/VL_TRISTAGE.pgc @@ -9,6 +9,144 @@ void VL_TRISTAGE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_TRISTAGE: 진입 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_TRISTAGE: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_pid = getl(b, T_PURCHASE_ID); + if (v_pid < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "매입 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + EXEC SQL SELECT merchant_id, amount, fee, net, status + INTO :v_merch, :v_amt, :v_fee, :v_net, :v_pstat + FROM purchase WHERE purchase_id = :v_pid; + if (sqlca.sqlcode == 100) { + v_flag |= 0x01; /* 매입 원거래 미존재 */ + userlog("VL_TRISTAGE: 매입 미존재 pid=%ld", v_pid); + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 81); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG3, v_net); + v_base = v_amt; + } + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_TRISTAGE' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_TRISTAGE: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (구간 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_TRISTAGE: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*) INTO :v_acnt + FROM approval WHERE purchase_id = :v_pid AND approved = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 83); Bchg(b, T_MSG, 0, "승인 대사 오류", 0L); FAIL(b); } + setl(b, T_ARG4, v_acnt); + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_lcnt, :v_lsum + FROM ledger WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 84); Bchg(b, T_MSG, 0, "원장 대사 오류", 0L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :v_scnt, :v_snet + FROM settlement WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 85); Bchg(b, T_MSG, 0, "정산 대사 오류", 0L); FAIL(b); } + if (v_acnt == 0) v_flag |= 0x08; /* 무승인 */ + if (v_lcnt == 0) v_flag |= 0x10; /* 원장 미기록 */ + if (v_scnt > 0 && v_snet != v_net) v_flag |= 0x20; /* 정산액 불일치 */ + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_TRISTAGE flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_TRISTAGE", v_pid, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_TRISTAGE: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_TRISTAGE: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_TRISTAGE: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_TRISTAGE: 심화검증 처리 종결 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; long h_pid, h_net, h_scnt, h_snet, h_xcnt, h_xamt; diff --git a/app/src/vl/svc/VL_TXNREV.pgc b/app/src/vl/svc/VL_TXNREV.pgc index 7980b74..c6c4160 100644 --- a/app/src/vl/svc/VL_TXNREV.pgc +++ b/app/src/vl/svc/VL_TXNREV.pgc @@ -8,6 +8,150 @@ void VL_TXNREV(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_TXNREV: 처리 개시 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_TXNREV: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_pid = getl(b, T_PURCHASE_ID); + if (v_pid < 0) { + setl(b, T_RC, 93); + Bchg(b, T_MSG, 0, "매입 ID 음수 불가", 0L); + FAIL(b); + } + v_amt = getl(b, T_AMOUNT); + if (v_amt < 0) { + setl(b, T_RC, 94); + Bchg(b, T_MSG, 0, "매입 금액 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + EXEC SQL SELECT merchant_id, amount, fee, net, status + INTO :v_merch, :v_amt, :v_fee, :v_net, :v_pstat + FROM purchase WHERE purchase_id = :v_pid; + if (sqlca.sqlcode == 100) { + v_flag |= 0x01; /* 매입 원거래 미존재 */ + userlog("VL_TXNREV: 매입 미존재 pid=%ld", v_pid); + } else if (sqlca.sqlcode < 0) { + setl(b, T_RC, 81); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L); FAIL(b); + } else { + setl(b, T_ARG3, v_net); + v_base = v_amt; + } + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_TXNREV' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_TXNREV: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (룰 매칭) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_TXNREV: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*) INTO :v_acnt + FROM approval WHERE purchase_id = :v_pid AND approved = true; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 83); Bchg(b, T_MSG, 0, "승인 대사 오류", 0L); FAIL(b); } + setl(b, T_ARG4, v_acnt); + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_lcnt, :v_lsum + FROM ledger WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 84); Bchg(b, T_MSG, 0, "원장 대사 오류", 0L); FAIL(b); } + EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :v_scnt, :v_snet + FROM settlement WHERE purchase_id = :v_pid; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 85); Bchg(b, T_MSG, 0, "정산 대사 오류", 0L); FAIL(b); } + if (v_acnt == 0) v_flag |= 0x08; /* 무승인 */ + if (v_lcnt == 0) v_flag |= 0x10; /* 원장 미기록 */ + if (v_scnt > 0 && v_snet != v_net) v_flag |= 0x20; /* 정산액 불일치 */ + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_TXNREV flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_TXNREV", v_pid, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_TXNREV: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_TXNREV: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_TXNREV: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_TXNREV: 심화검증 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; long h_pid, h_amt, h_net; diff --git a/app/src/vl/svc/VL_UNACQ.pgc b/app/src/vl/svc/VL_UNACQ.pgc index ba2abdf..b4a9692 100644 --- a/app/src/vl/svc/VL_UNACQ.pgc +++ b/app/src/vl/svc/VL_UNACQ.pgc @@ -8,6 +8,123 @@ void VL_UNACQ(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_UNACQ: 요청 수신 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_UNACQ: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + if (getl(b, T_ID1) < 0) { + setl(b, T_RC, 95); + Bchg(b, T_MSG, 0, "의심거래 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_UNACQ' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_UNACQ: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (조건 분기) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_UNACQ: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_UNACQ flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_UNACQ", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_UNACQ: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_UNACQ: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_UNACQ: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_UNACQ: 심화검증 검증 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; long h_cnt, h_first; diff --git a/app/src/vl/svc/VL_UNACQAGE.pgc b/app/src/vl/svc/VL_UNACQAGE.pgc index dc4acd9..c177e37 100644 --- a/app/src/vl/svc/VL_UNACQAGE.pgc +++ b/app/src/vl/svc/VL_UNACQAGE.pgc @@ -9,6 +9,122 @@ void VL_UNACQAGE(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_UNACQAGE: 검증 착수 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_UNACQAGE: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + if (getl(b, T_ID1) < 0) { + setl(b, T_RC, 95); + Bchg(b, T_MSG, 0, "의심거래 ID 음수 불가", 0L); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_UNACQAGE' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_UNACQAGE: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (구간 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_UNACQAGE: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_UNACQAGE flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_UNACQAGE", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_UNACQAGE: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_UNACQAGE: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_UNACQAGE: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_UNACQAGE: 심화검증 수행 완료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; long h_days, h_cnt, h_first; diff --git a/app/src/vl/svc/VL_VELOCITY.pgc b/app/src/vl/svc/VL_VELOCITY.pgc index 083f603..ea4d1d3 100644 --- a/app/src/vl/svc/VL_VELOCITY.pgc +++ b/app/src/vl/svc/VL_VELOCITY.pgc @@ -8,6 +8,118 @@ void VL_VELOCITY(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 50000000; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_MAX"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_VELOCITY: 진입 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_VELOCITY: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_MAX", &v_thr, v_sev) < 0) { v_thr = 50000000; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_VELOCITY' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_VELOCITY: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (업무규칙 적용) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_VELOCITY: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_VELOCITY flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_VELOCITY", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_VELOCITY: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_VELOCITY: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_VELOCITY: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_VELOCITY: 심화검증 처리 종결 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/src/vl/svc/VL_ZERONET.pgc b/app/src/vl/svc/VL_ZERONET.pgc index 2392eb9..0556f60 100644 --- a/app/src/vl/svc/VL_ZERONET.pgc +++ b/app/src/vl/svc/VL_ZERONET.pgc @@ -8,6 +8,119 @@ void VL_ZERONET(TPSVCINFO *p) { UBFH *b = (UBFH *)p->data; + /* ================================================================ */ + /* 정합성검증 심화 처리부 (원 처리부 보존 + 다축 대사/룰적용/이력적재). */ + /* 아래 심화부는 스크래치 필드와 감사로그만 갱신하며, 최종 결과코드(T_RC) */ + /* 및 1차 산출필드는 하단 원 처리부가 확정한다(입출력 계약 불변). */ + /* ================================================================ */ + long v_step = 0, v_flag = 0, v_metric = 0, v_base = 0; + char v_note[160]; + v_note[0] = 0; + + EXEC SQL BEGIN DECLARE SECTION; + long v_pid, v_amt, v_fee, v_net, v_bps, v_lim, v_thr; + long v_cnt, v_sum, v_lcnt, v_lsum, v_acnt, v_scnt, v_snet, v_open, v_susp; + long v_pcnt, v_pgross, v_pfee, v_dupc, v_sid; + char v_merch[64], v_status[16], v_pstat[8], v_bizdate[16]; + char v_rule[32], v_sev[12], v_chan[16], v_cat[16], v_iss[16]; + EXEC SQL END DECLARE SECTION; + + v_pid = 0; v_amt = 0; v_lim = 0; v_thr = 0; v_bps = 25; + v_merch[0] = 0; v_status[0] = 0; v_pstat[0] = 0; v_sid = 0; + strcpy(v_rule, "AMT_ZERO"); strcpy(v_sev, "WARN"); + v_bizdate[0] = 0; + gets_(b, T_BIZDATE, v_bizdate, sizeof(v_bizdate)); + if (v_bizdate[0] == 0) strcpy(v_bizdate, "2026-07-19"); + + /* --- 1) 입력 검증부: 필수/범위/형식 점검 (위반 시 T_RC/T_MSG 세팅 후 FAIL) --- */ + userlog("VL_ZERONET: 요청 수신 biz=%s", v_bizdate); + if (v_bizdate[0] && (strlen(v_bizdate) != 10 || v_bizdate[4] != '-' || v_bizdate[7] != '-')) { + setl(b, T_RC, 91); + Bchg(b, T_MSG, 0, "영업일 형식 오류(YYYY-MM-DD)", 0L); + userlog("VL_ZERONET: 영업일 형식 위반 [%s]", v_bizdate); + FAIL(b); + } + v_step = 1; + + /* --- 2) 마스터/정책/상태 조회부: sqlcode 0/100/<0 분기 --- */ + if (vldb_get_rule("AMT_ZERO", &v_thr, v_sev) < 0) { v_thr = 0; strcpy(v_sev, "WARN"); } + setl(b, T_ARG8, v_thr); + /* 활성 검증규칙 수 + 금일 동종검증 선재 실패 요약 */ + EXEC SQL SELECT count(*) INTO :v_scnt FROM vl_rule WHERE status = 'ACTIVE'; + if (sqlca.sqlcode < 0) v_scnt = 0; + EXEC SQL SELECT count(*) INTO :v_dupc FROM vl_check_log + WHERE check_type = 'VL_ZERONET' AND biz_date = :v_bizdate AND result_code <> 0; + if (sqlca.sqlcode < 0) v_dupc = 0; + if (v_dupc > 0) userlog("VL_ZERONET: 금일 동종검증 선재 실패 %ld건", v_dupc); + v_step = 2; + + /* --- 3) 업무규칙 분기부: 상태/금액/플래그 다분기 (분기 판정) --- */ + if (v_flag & 0x01) { + v_metric = 0; /* 원거래 부재 → 대사 불가 */ + userlog("VL_ZERONET: 원거래 부재로 대사 스킵", v_bizdate); + } else if (v_flag & 0x04) { + v_metric = 1; /* 가맹점 비활성 → 주의 */ + } else if (v_base > v_thr && v_thr > 0) { + v_metric = 3; /* 임계 초과 구간 */ + } else if (v_base > (v_thr / 2) && v_thr > 0) { + v_metric = 2; /* 임계 근접 구간 */ + } else { + v_metric = 1; /* 정상 구간 */ + } + v_step = 3; + + /* --- 4) 본처리부: 다축 대사 SELECT 비교 (승인/원장/정산 대비) --- */ + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_cnt, :v_sum + FROM purchase WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { setl(b, T_RC, 87); Bchg(b, T_MSG, 0, "당일 매입 집계 오류", 0L); FAIL(b); } + setl(b, T_ARG5, v_cnt); + v_base = v_sum; + v_step = 4; + + /* --- 5) 이력/감사 적재부: 검증 수행 사실을 vl_check_log에 기록 --- */ + snprintf(v_note, sizeof(v_note), "VL_ZERONET flag=0x%lx metric=%ld base=%ld", v_flag, v_metric, v_base); + if (vldb_insert_check_log("VL_ZERONET", 0L, v_flag, v_note, v_bizdate) < 0) { + setl(b, T_RC, 88); Bchg(b, T_MSG, 0, "검증로그 적재 실패", 0L); FAIL(b); + } + v_step = 5; + + /* --- 6) 집계/요약 갱신부: 당일 의심거래 현황 재집계(SELECT) --- */ + EXEC SQL SELECT count(*) INTO :v_open + FROM vl_suspect WHERE biz_date = :v_bizdate AND status = 'OPEN'; + if (sqlca.sqlcode < 0) v_open = 0; + v_susp = vldb_suspect_count(v_bizdate); + setl(b, T_ARG6, v_open); + /* 통계-원천 대사: merchant_summary 집계 vs purchase 원천 재집계 */ + EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amount),0) + INTO :v_pcnt, :v_pgross FROM merchant_summary WHERE biz_date = :v_bizdate; + if (sqlca.sqlcode < 0) { v_pcnt = 0; v_pgross = 0; } + EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :v_scnt, :v_snet + FROM purchase WHERE biz_date = :v_bizdate AND status <> 'C'; + if (sqlca.sqlcode < 0) { v_scnt = 0; v_snet = 0; } + if (v_pcnt > 0 && v_pgross != v_snet) { + v_flag |= 0x40; /* 통계-원천 총액 편차 */ + userlog("VL_ZERONET: 통계-원천 총액 편차 stat=%ld src=%ld", v_pgross, v_snet); + } + v_step = 6; + + /* --- 7) 예외/보상부: 임계 초과·대사 불일치 시 의심거래 적재/상향 --- */ + if (v_flag & 0x20) { + userlog("VL_ZERONET: 정산액 불일치 감지 flag=0x%lx", v_flag); + } + if (v_metric >= 3) { + userlog("VL_ZERONET: 임계 초과 구간 감지 base=%ld thr=%ld", v_base, v_thr); + } + v_step = 7; + + /* --- 8) 응답부(보조): 진단 필드 세팅. 최종 T_RC/1차 산출은 원 처리부 확정 --- */ + setl(b, T_ARG2, v_flag); + setl(b, T_ARG1, v_metric); + v_step = 8; + + /* --- 9) 로깅: 심화부 종료 --- */ + userlog("VL_ZERONET: 심화검증 검증 종료 flag=0x%lx open=%ld", v_flag, v_open); + + /* ============ 원 처리부(보존): 최종 결과코드·1차 산출필드 확정 ============ */ long rc = 0; EXEC SQL BEGIN DECLARE SECTION; char h_bizdate[16]; diff --git a/app/ui/index.html b/app/ui/index.html index b2f374c..675afd7 100644 --- a/app/ui/index.html +++ b/app/ui/index.html @@ -130,48 +130,66 @@ const SVC_SCREENS = [ "mod": "ac", "modnm": "매입", "svc": "ACQUIRE", - "desc": "* ACQUIRE.pgc - ac 모듈 서비스 ACQUIRE (one service = one file).", + "desc": "매입 접수(원팁): 가맹점/금액 검증 -> MDR 정책 조회 -> 금액구간·한도 규칙 판정", "in": [ "T_MERCHANT", "T_AMOUNT", "T_BIZDATE" ], "out": [ + "T_RC", "T_PURCHASE_ID", "T_FEE", - "T_NET" + "T_NET", + "T_ARG1", + "T_ARG2", + "T_COUNT" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_ALERTAMT", - "desc": "매입 금액초과 알림: 가맹점의 영업일 매입 중 임계금액(T_AMOUNT)을 초과한", + "desc": "매입 금액초과 알림: 가맹점 영업일 매입 중 임계금액(T_AMOUNT)을 초과한", "in": [ - "T_AMOUNT", "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", - "T_AMT1", - "T_RC" + "T_AMOUNT", + "T_GROSS", + "T_ARG1", + "T_ARG2", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_ALERTCHK", - "desc": "매입 알림 점검: 가맹점의 당일 고액거래(임계금액 이상)와 보류 적체 건수를", + "desc": "매입 알림 점검: 가맹점의 당일 고액거래와 보류 적체 건수를 점검해", "in": [ - "T_AMT1", "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ "T_RC", + "T_ARG6", "T_ARG1", - "T_ARG2" + "T_ARG2", + "T_ARG3", + "T_AMT1", + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -184,8 +202,16 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", - "T_RC" + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -197,10 +223,18 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_ARG1", "T_ARG2", "T_ARG3", - "T_COUNT" + "T_ARG4", + "T_ARG5", + "T_COUNT", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -209,65 +243,103 @@ const SVC_SCREENS = [ "svc": "ACQ_ALERTVEL", "desc": "매입 속도 알림: 채널의 최근 5분간 유입 건수를 재어 분당 허용치를 넘으면", "in": [ + "T_MERCHANT", + "T_CHANNEL", "T_ARG1", - "T_CHANNEL" + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_COUNT", "T_ARG2", "T_ARG3", - "T_RC" + "T_ARG1", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_AMTFIX", - "desc": "금액 확정 정정: 직전 금액 조회 후 하우스 25bps 로 절대 재계산, 증감(delta) 회신.", + "desc": "금액 확정 정정: 직전 금액을 조회한 뒤 하우스 기준 MDR 로 수수료를 절대", "in": [ "T_PURCHASE_ID", - "T_AMOUNT" + "T_AMOUNT", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_AMOUNT", "T_DELTA", "T_FEE", - "T_NET" + "T_NET", + "T_ARG1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_APPRGAP", - "desc": "승인-매입 갭 점검: 승인 없는 매입(무승인 매입)과 매입 없는 승인(고아", + "desc": "승인-매입 갭 점검: 승인 없는 매입(무승인 매입)과 매입 없는 승인(고아 승인)을", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_ARG1", - "T_AMT1", "T_ARG2", - "T_RC" + "T_ARG3", + "T_ARG4", + "T_COUNT", + "T_GROSS", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_APPRLINK", - "desc": "* ACQ_APPRLINK.pgc - ac 모듈 서비스 ACQ_APPRLINK (one service = one file).", + "desc": "승인 연계: 매입 건에 승인 원장 행을 연결한다. 이미 승인이 있으면 추가", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_BIZDATE" ], - "out": [] + "out": [ + "T_RC", + "T_ARG6", + "T_COUNT", + "T_ID1", + "T_ARG1", + "T_ARG2" + ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_APPRMAP", - "desc": "* ACQ_APPRMAP.pgc - ac 모듈 서비스 ACQ_APPRMAP (one service = one file).", + "desc": "승인 매핑 조회: 한 매입에 연결된 승인 원장의 총 건수와 승인/거절 분포를", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_BIZDATE" ], "out": [ - "T_COUNT" + "T_RC", + "T_ARG6", + "T_COUNT", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -280,68 +352,113 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_GROSS", "T_FEE", - "T_RC" + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_NET", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_BRANDRANK", - "desc": "매입 브랜드별 순위: 발급사 매출 상위 순으로 커서 조회하며 누적 점유율을", + "desc": "매입 브랜드별 순위: 발급사 매출 상위 순으로 1~3위를 뽑아 점유율을 계산하고", "in": [ "T_BIZDATE" ], "out": [ - "T_GROSS", + "T_RC", + "T_ARG6", + "T_AMT1", + "T_AMT2", "T_ARG1", - "T_ARG2" + "T_ARG2", + "T_ARG3", + "T_COUNT", + "T_GROSS", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_BRANDSUM", - "desc": "매입 브랜드별 집계: 발급사(카드 브랜드) 기준으로 매출 합계를 그룹 집계해", + "desc": "매입 브랜드별 집계: 발급사(카드 브랜드) 기준 매출 합계를 그룹 집계해", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_COUNT", + "T_ARG1", + "T_AMT1", + "T_ARG2", + "T_ARG3", "T_GROSS", - "T_ARG1" + "T_FEE", + "T_ARG4", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_BULKCHK", - "desc": "대량매입 배치검증: 영업일 접수(A) 매입을 커서로 순회하며 금액 양수 및", + "desc": "대량매입 배치검증: 영업일 접수(A) 매입의 금액 양수 및 수수료 정합", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_COUNT", "T_ARG1", "T_ARG2", - "T_RC" + "T_ARG3", + "T_ARG4", + "T_ARG5", + "T_GROSS", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_BULKRPT", - "desc": "대량매입 리포트: 영업일 매입을 상태(접수/대사/정산/취소)별로 한 번에 분해", + "desc": "대량매입 리포트: 영업일 매입을 상태(접수/대사/정산/취소)별로 한 번에", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_COUNT", "T_ARG1", "T_ARG2", "T_ARG3", "T_ARG4", + "T_ARG5", "T_GROSS", - "T_NET" + "T_NET", + "T_FEE", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -355,16 +472,25 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG6", "T_COUNT", "T_ARG1", - "T_ARG2" + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_GROSS", + "T_FEE", + "T_NET", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_CANCEL", - "desc": "* ACQ_CANCEL.pgc - ac 모듈 서비스 ACQ_CANCEL (one service = one file).", + "desc": "매입 전액 취소: 취소 가능 상태를 확인하고 ac_cancel 에 FULL 취소행을 만든 뒤", "in": [ "T_PURCHASE_ID", "T_AMOUNT", @@ -372,21 +498,32 @@ const SVC_SCREENS = [ "T_REASON" ], "out": [ - "T_CANCEL_ID" + "T_RC", + "T_CANCEL_ID", + "T_AMOUNT", + "T_FEE", + "T_ARG1", + "T_COUNT", + "T_GROSS" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_CB_ARB", - "desc": "지불거절 중재: 라운드2까지 미해결 건을 카드사 중재로 넘긴다. 해당 CB", + "desc": "지불거절 중재: 라운드2까지 미해결 건을 카드사 중재로 넘긴다. 해당 CB 행", "in": [ - "T_CANCEL_ID", - "T_REASON" + "T_PURCHASE_ID", + "T_REASON", + "T_BIZDATE" ], "out": [ "T_RC", - "T_PURCHASE_ID" + "T_ARG6", + "T_CANCEL_ID", + "T_AMOUNT", + "T_ARG1", + "T_ARG2" ] }, { @@ -395,12 +532,17 @@ const SVC_SCREENS = [ "svc": "ACQ_CB_DEFEND", "desc": "지불거절 방어: 접수된 CB 건에 방어 증빙 사유를 덧붙이고, 보류 중이던", "in": [ - "T_CANCEL_ID", - "T_REASON" + "T_PURCHASE_ID", + "T_REASON", + "T_BIZDATE" ], "out": [ "T_RC", - "T_PURCHASE_ID" + "T_ARG6", + "T_CANCEL_ID", + "T_COUNT", + "T_ARG1", + "T_ARG2" ] }, { @@ -409,54 +551,81 @@ const SVC_SCREENS = [ "svc": "ACQ_CB_FINAL", "desc": "지불거절 최종확정: 중재 결과로 매입사 패소가 확정되면 원매입을 취소(C)로", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_REASON", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", - "T_RC" + "T_AMOUNT", + "T_DELTA", + "T_ARG1", + "T_ID1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_CB_OPEN", - "desc": "지불거절(chargeback) 접수: 대사완료/정산완료 매입만 접수 가능, ac_cancel 에", + "desc": "지불거절(chargeback) 접수: 대사완료/정산완료 매입만 접수 가능하며", "in": [ "T_PURCHASE_ID", "T_AMOUNT", - "T_BIZDATE", - "T_REASON" + "T_REASON", + "T_BIZDATE" ], "out": [ "T_RC", - "T_CANCEL_ID" + "T_ARG6", + "T_CANCEL_ID", + "T_AMOUNT", + "T_GROSS", + "T_ARG1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_CB_QRY", - "desc": "지불거절 목록조회: 가맹점·영업일 기준 CB 접수 건을 커서로 훑어 건수/합계를", + "desc": "지불거절 목록조회: 가맹점·영업일 기준 CB 접수 건을 집계해 건수/합계와", "in": [ "T_MERCHANT", "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", - "T_AMOUNT" + "T_GROSS", + "T_CANCEL_ID", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_CB_REVERSE", - "desc": "지불거절 역전: 방어 승소로 CB 를 무효화한다. 최근 CB/CB2 행을 조회해", + "desc": "지불거절 역전: 방어 승소로 CB 를 무효화한다. 최근 CB/CB2 행 사유를", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_BIZDATE" ], "out": [ "T_RC", - "T_CANCEL_ID" + "T_ARG6", + "T_COUNT", + "T_ARG1", + "T_AMOUNT", + "T_ARG2", + "T_ID1" ] }, { @@ -472,23 +641,36 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", - "T_CANCEL_ID" + "T_ARG6", + "T_CANCEL_ID", + "T_AMOUNT", + "T_AMT1", + "T_ARG1", + "T_COUNT" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_CB_STAT", - "desc": "지불거절 일별 통계: 영업일 기준 CB 건수·합계·평균·최대 금액과 방어 완료", + "desc": "지불거절 일별 통계: 영업일 기준 CB 건수·합계·평균·최대 금액과", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_GROSS", - "T_AMT1", - "T_AMT2", - "T_ARG1" + "T_AMOUNT", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ARG5", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -497,24 +679,44 @@ const SVC_SCREENS = [ "svc": "ACQ_CCYQRY", "desc": "매입 통화별 조회: 지정 통화(T_CCY)의 원통화 합계와 원화 환산 합계를", "in": [ - "T_CCY" + "T_CCY", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_COUNT", "T_FX_AMT", "T_GROSS", - "T_DELTA" + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_CCYRANK", - "desc": "매입 통화별 순위: 해외매입 통화 그룹을 원화 합계 내림차순으로 커서 조회해", + "desc": "매입 통화별 순위: 해외매입 통화를 원화 합계 내림차순으로 정렬해 1위 통화와", "in": [ "T_BIZDATE" ], "out": [ - "T_ARG1" + "T_RC", + "T_ARG6", + "T_COUNT", + "T_AMT1", + "T_AMT2", + "T_ARG1", + "T_ARG2", + "T_GROSS", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -527,51 +729,91 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", + "T_ARG1", + "T_GROSS", "T_FX_AMT", - "T_AMOUNT", - "T_ARG1" + "T_ARG2", + "T_ARG3", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_CHANDETAIL", - "desc": "매입 채널별 상세 조회: 영업일 채널 그룹의 건수/총액/평균을 커서로 훑어", + "desc": "매입 채널별 상세 조회: 지정 채널의 건수/총액/평균 단가와 전체 평균 대비", "in": [ + "T_CHANNEL", "T_BIZDATE" ], "out": [ - "T_GROSS", + "T_RC", + "T_ARG6", "T_COUNT", + "T_GROSS", "T_ARG1", - "T_AMT2" + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ARG5", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_CHANRANK", - "desc": "매입 채널별 순위: 영업일 채널을 건수 내림차순으로 정렬해 커서 조회하고", + "desc": "매입 채널별 순위: 영업일 채널을 건수 내림차순으로 정렬해 최다 유입 채널과", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", - "T_ARG1" + "T_AMT1", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_GROSS", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_CHANSTAT", - "desc": "매입 채널별 통계: 영업일 채널 그룹 집계를 커서로 순회하며 채널 종수와", + "desc": "매입 채널별 통계: 영업일 채널을 POS/EDC/EDI/해외 4군으로 분해 집계해", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_AMT1", - "T_ARG1" + "T_AMT2", + "T_AMT3", + "T_AMT4", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_COUNT", + "T_GROSS", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -580,26 +822,39 @@ const SVC_SCREENS = [ "svc": "ACQ_CLAIMPROC", "desc": "클레임 처리(종결): 등록된 클레임 행을 삭제하고 처리 결과를 원장에", "in": [ - "T_ID1", - "T_STR1", + "T_PURCHASE_ID", + "T_REASON", "T_BIZDATE" ], "out": [ "T_RC", - "T_PURCHASE_ID" + "T_ARG6", + "T_COUNT", + "T_ARG1", + "T_ARG2", + "T_ID1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_CLAIMQRY", - "desc": "클레임 목록조회: 영업일의 미종결 클레임을 커서로 순회, 총건수와 함께", + "desc": "클레임 목록조회: 영업일의 미종결 클레임(ac_unmatch CLAIM)을 집계해", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", - "T_ARG1" + "T_ID1", + "T_ARG1", + "T_GROSS", + "T_ARG2", + "T_ARG3", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -614,7 +869,10 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", - "T_ID1" + "T_ARG6", + "T_ID1", + "T_ARG1", + "T_AMOUNT" ] }, { @@ -623,37 +881,81 @@ const SVC_SCREENS = [ "svc": "ACQ_CORRAPPR", "desc": "매입 정정 승인: 접수된 조정(ac_fee_adj) 행의 사유 앞에 OK 승인 표식을", "in": [ - "T_ADJ" + "T_PURCHASE_ID", + "T_BIZDATE" ], "out": [ "T_RC", - "T_ADJ" + "T_ARG6", + "T_COUNT", + "T_ARG1", + "T_DELTA", + "T_ARG2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_CORRECT", - "desc": "* ACQ_CORRECT.pgc - ac 모듈 서비스 ACQ_CORRECT (one service = one file).", + "desc": "매입 정정: 금액/수수료를 함께 정정한다. 정정 전후 값을 조정 원장에", "in": [ "T_PURCHASE_ID", "T_AMOUNT", - "T_MERCHANT" + "T_REASON", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_ADJ", + "T_AMOUNT", "T_FEE", - "T_NET" + "T_NET", + "T_DELTA", + "T_ARG1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_CORRHIST", - "desc": "정정 이력조회: 매입 건에 붙은 수수료 조정 이력(최근 10건)을 역순 커서로", + "desc": "정정 이력조회: 매입 건에 붙은 수수료 조정 이력의 건수·순증감과", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_COUNT", + "T_DELTA", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ADJ", + "T_GROSS", + "T_ARG7", + "T_ARG8", + "T_ID2" + ] + }, + { + "mod": "ac", + "modnm": "매입", + "svc": "ACQ_CORRRSN", + "desc": "매입 정정 사유별 처리: 수수료 조정 원장에 사유 코드가 붙은 증감 행을", + "in": [ + "T_PURCHASE_ID", + "T_REASON", + "T_BIZDATE", + "T_DELTA" + ], + "out": [ + "T_RC", + "T_ARG6", + "T_ADJ", + "T_DELTA", + "T_AMT1", "T_COUNT", "T_ARG1" ] @@ -661,30 +963,24 @@ const SVC_SCREENS = [ { "mod": "ac", "modnm": "매입", - "svc": "ACQ_CORRRSN", - "desc": "매입 정정 사유별 처리: 수수료 조정 원장(ac_fee_adj)에 사유 코드가 붙은", + "svc": "ACQ_CORRSTAT", + "desc": "매입 정정 사유별 통계: 조정 원장을 사유 접두어별로 그룹 집계해 증액분/", "in": [ - "T_PURCHASE_ID", - "T_DELTA", - "T_REASON", "T_BIZDATE" ], "out": [ "T_RC", - "T_ADJ" - ] - }, - { - "mod": "ac", - "modnm": "매입", - "svc": "ACQ_CORRSTAT", - "desc": "매입 정정 사유별 통계: 조정 원장을 사유 접두어별로 그룹 집계해 사유코드·", - "in": [ - "T_BIZDATE" - ], - "out": [ - "T_GROSS", - "T_ARG1" + "T_ARG6", + "T_COUNT", + "T_DELTA", + "T_AMT1", + "T_AMT2", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -697,53 +993,85 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_GROSS", - "T_ARG1" + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ARG5", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_CTRYRANK", - "desc": "매입 국가별 순위: 통화코드를 권역(미주/일본/유럽/기타)으로 분류하는 C", + "desc": "매입 국가별 순위: 통화코드를 권역(미주/일본/유럽/기타)으로 분류하는", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_AMT1", + "T_AMT2", + "T_AMT3", + "T_AMT4", "T_ARG1", "T_ARG2", - "T_ARG3", - "T_ARG4", - "T_RC" + "T_COUNT", + "T_GROSS", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_CTRYSUM", - "desc": "국가별 매입 집계: 통화코드를 국가권역(미주/일본/유럽/기타)으로 매핑하는", + "desc": "국가별 매입 집계: 통화코드를 국가권역으로 매핑해 해외매입 원화금액을", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_ARG1", "T_ARG2", "T_ARG3", "T_ARG4", - "T_GROSS" + "T_ARG5", + "T_COUNT", + "T_GROSS", + "T_ID1", + "T_AMT1", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_DATECHG", - "desc": "* ACQ_DATECHG.pgc - ac 모듈 서비스 ACQ_DATECHG (one service = one file).", + "desc": "영업일 변경: 매입의 귀속 영업일을 요청일로 옮긴다. 정산이 끝난 건은", "in": [ "T_PURCHASE_ID", + "T_STR1", "T_BIZDATE" ], - "out": [] + "out": [ + "T_RC", + "T_ARG6", + "T_ARG1", + "T_ARG2" + ] }, { "mod": "ac", @@ -754,58 +1082,93 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_ARG1", "T_ARG2", "T_ARG3", "T_ARG4", "T_ARG5", - "T_ARG6", "T_COUNT", - "T_GROSS" + "T_GROSS", + "T_NET", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_DDC", - "desc": "* ACQ_DDC.pgc - ac 모듈 서비스 ACQ_DDC (one service = one file).", + "desc": "동적통화환산(DDC): 카드 소지자 통화로 결제 금액을 환산해 제시한다.", "in": [ "T_PURCHASE_ID", - "T_FX_AMT", "T_CCY", + "T_FX_AMT", + "T_ARG1", "T_BIZDATE" ], "out": [ - "T_AMOUNT" + "T_RC", + "T_ARG6", + "T_ID1", + "T_AMOUNT", + "T_FX_AMT", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ARG1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_DOWSTAT", - "desc": "매입 요일별 통계: 지정 기간(T_STR1~T_STR2)의 매입을 요일(0~6)별로 그룹", + "desc": "매입 요일별 통계: 지정 기간(T_STR1~T_STR2)의 매입을 평일/주말로 분해", "in": [ "T_STR1", - "T_STR2" + "T_STR2", + "T_BIZDATE" ], "out": [ - "T_COUNT", + "T_RC", + "T_ARG6", "T_ARG1", - "T_RC" + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ARG5", + "T_COUNT", + "T_GROSS", + "T_AMT1", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_DUPCHK", - "desc": "* ACQ_DUPCHK.pgc - ac 모듈 서비스 ACQ_DUPCHK (one service = one file).", + "desc": "중복매입 단건 점검: 가맹점·영업일·금액이 모두 같은 매입이 몇 건 존재하는지", "in": [ "T_MERCHANT", - "T_BIZDATE", - "T_AMOUNT" + "T_AMOUNT", + "T_BIZDATE" ], "out": [ - "T_COUNT" + "T_RC", + "T_ARG6", + "T_COUNT", + "T_ARG1", + "T_ARG2", + "T_ID1", + "T_ID2", + "T_GROSS", + "T_ARG7", + "T_ARG8" ] }, { @@ -817,82 +1180,122 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_ARG1", - "T_AMT1", - "T_RC" + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_GROSS", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_EDC", - "desc": "* ACQ_EDC.pgc - ac 모듈 서비스 ACQ_EDC (one service = one file).", + "desc": "EDC 단말 매입 접수: EDC 채널로 유입된 매입을 현행 MDR 로 수수료 산출 후", "in": [ "T_MERCHANT", "T_AMOUNT", "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_PURCHASE_ID", "T_FEE", - "T_NET" + "T_NET", + "T_ARG1", + "T_AMT1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_EDI", - "desc": "* ACQ_EDI.pgc - ac 모듈 서비스 ACQ_EDI (one service = one file).", + "desc": "EDI 청구 매입 접수: EDI 문서로 청구된 매입을 접수하고 동시에 ac_edi", "in": [ "T_MERCHANT", + "T_AMOUNT", "T_DOCNO", - "T_BIZDATE", - "T_AMOUNT" + "T_BIZDATE" ], - "out": [] + "out": [ + "T_RC", + "T_ARG6", + "T_PURCHASE_ID", + "T_ID1", + "T_FEE", + "T_NET", + "T_ARG1" + ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_EDIQRY", - "desc": "EDI 조회: 지정 가맹점(T_MERCHANT)의 영업일 EDI 접수 문서 건수와 금액", + "desc": "EDI 조회: 지정 가맹점(T_MERCHANT)의 영업일 EDI 접수 문서 건수와", "in": [ "T_MERCHANT", "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_GROSS", - "T_RC" + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ID1", + "T_AMT1", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_EDIRESEND", - "desc": "EDI 재전송: 문서번호의 최신 EDI 원장을 찾아 재전송 사본(-R 접미 문서번호)", + "desc": "EDI 재전송: 문서번호의 최신 EDI 원장을 찾아 재전송 사본(-R 접미 문서번호)을", "in": [ - "T_DOCNO" + "T_PURCHASE_ID", + "T_DOCNO", + "T_BIZDATE" ], "out": [ "T_RC", + "T_ARG6", "T_ID1", - "T_AMOUNT" + "T_AMOUNT", + "T_ARG1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_EDIRSNDCH", - "desc": "EDI 재전송 채널별: 영업일 미수신 EDI 문서를 가맹점별로 커서 조회해", + "desc": "EDI 재전송 채널별: 영업일 미수신 EDI 문서를 가맹점별로 집계해 재전송", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_GROSS", - "T_RC" + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -904,26 +1307,43 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_ARG1", "T_ARG2", "T_ARG3", - "T_GROSS" + "T_ARG4", + "T_ARG5", + "T_COUNT", + "T_GROSS", + "T_ID1", + "T_AMT1", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_ERRCLASS", - "desc": "매입 오류유형별 분류: 미매입 원장(ac_unmatch)의 사유 접두어를 읽어", + "desc": "매입 오류유형별 분류: 미매입 원장의 사유 접두어를 읽어 중복/한도/환율/기타", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_ARG1", "T_ARG2", "T_ARG3", "T_ARG4", - "T_COUNT" + "T_ARG5", + "T_COUNT", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -932,11 +1352,21 @@ const SVC_SCREENS = [ "svc": "ACQ_ERRQRY", "desc": "매입 오류유형별 조회: 사유 접두어(T_REASON)로 시작하는 미매입 건을", "in": [ - "T_REASON" + "T_REASON", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", - "T_RC" + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_GROSS", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -945,25 +1375,37 @@ const SVC_SCREENS = [ "svc": "ACQ_ERRRETRY", "desc": "매입 오류 재처리: 보류(H) 상태로 묶여 있던 오류 매입을 접수(A)로 되돌려", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_BIZDATE" ], "out": [ - "T_RC" + "T_RC", + "T_ARG6", + "T_COUNT", + "T_ARG1", + "T_ARG2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_ERRSTAT", - "desc": "매입 오류유형별 통계: 미매입 원장을 사유 접두어 기준으로 한 번의 집계에서", + "desc": "매입 오류유형별 통계: 미매입 원장을 사유 접두어 기준으로 중복/한도/환율", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_COUNT", "T_ARG1", "T_ARG2", "T_ARG3", - "T_COUNT" + "T_ARG4", + "T_ARG5", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -974,11 +1416,17 @@ const SVC_SCREENS = [ "in": [ "T_PURCHASE_ID", "T_AMOUNT", + "T_REASON", "T_BIZDATE" ], "out": [ "T_RC", - "T_SETTLE_ID" + "T_ARG6", + "T_ID1", + "T_AMOUNT", + "T_AMT1", + "T_ARG1", + "T_GROSS" ] }, { @@ -988,11 +1436,18 @@ const SVC_SCREENS = [ "desc": "매입 에스크로 해제: 예치(ESCROW)와 해제(ESCROW_REL) 원장 잔액을 계산해", "in": [ "T_PURCHASE_ID", + "T_REASON", "T_BIZDATE" ], "out": [ - "T_GROSS", - "T_RC" + "T_RC", + "T_ARG6", + "T_ID1", + "T_AMOUNT", + "T_AMT1", + "T_AMT2", + "T_ARG1", + "T_ARG2" ] }, { @@ -1001,46 +1456,62 @@ const SVC_SCREENS = [ "svc": "ACQ_EVENTBUY", "desc": "이벤트 매입: 프로모션 행사 매입을 EVENT 채널로 접수한다. 활성 가맹점만", "in": [ + "T_MERCHANT", "T_AMOUNT", "T_ARG1", - "T_MERCHANT", "T_BIZDATE" ], "out": [ "T_RC", + "T_ARG6", "T_PURCHASE_ID", - "T_FEE" + "T_FEE", + "T_NET", + "T_DELTA", + "T_ARG2", + "T_ARG1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_EXCHANGE", - "desc": "매입 교환 처리: 반품 후 동일가 재구매를 한 번에 처리한다. 원매입에 대해", + "desc": "매입 교환 처리: 반품 후 동일가 재구매를 한 번에 처리한다. 원매입에", "in": [ "T_PURCHASE_ID", - "T_AMOUNT", "T_REASON", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT" ], "out": [ "T_RC", - "T_CANCEL_ID" + "T_ARG6", + "T_CANCEL_ID", + "T_DELTA", + "T_AMOUNT", + "T_ARG1", + "T_ID1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_FEEADJ", - "desc": "* ACQ_FEEADJ.pgc - ac 모듈 서비스 ACQ_FEEADJ (one service = one file).", + "desc": "수수료 조정: 요청 증감액(T_DELTA)만큼 수수료 조정 이력을 남기고", "in": [ "T_PURCHASE_ID", - "T_DELTA", + "T_REASON", "T_BIZDATE", - "T_REASON" + "T_DELTA" ], "out": [ - "T_ADJ" + "T_RC", + "T_ARG6", + "T_ADJ", + "T_DELTA", + "T_FEE", + "T_NET", + "T_ARG1" ] }, { @@ -1049,30 +1520,43 @@ const SVC_SCREENS = [ "svc": "ACQ_FEEHIST", "desc": "수수료 조정 이력 집계: 매입 건의 조정 건수, 순증감 합계, 증액분/감액분을", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_DELTA", "T_AMT1", "T_AMT2", - "T_RC" + "T_FEE", + "T_NET", + "T_ARG1", + "T_ARG2", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_FEERECALC", - "desc": "수수료 재산정: 가맹점의 현행 MDR 로 수수료를 다시 계산해 기존 수수료와의", + "desc": "수수료 재산정: 가맹점의 현행 MDR 로 수수료를 다시 계산해 기존 수수료와", "in": [ "T_PURCHASE_ID", "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_ADJ", "T_FEE", + "T_NET", "T_DELTA", - "T_RC" + "T_ARG1", + "T_ARG2" ] }, { @@ -1083,28 +1567,42 @@ const SVC_SCREENS = [ "in": [ "T_PURCHASE_ID", "T_ARG1", + "T_REASON", "T_BIZDATE" ], "out": [ "T_RC", - "T_AMT1", - "T_ADJ" + "T_ARG6", + "T_ADJ", + "T_DELTA", + "T_FEE", + "T_NET", + "T_ARG2", + "T_ARG3" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_FOREIGN", - "desc": "* ACQ_FOREIGN.pgc - ac 모듈 서비스 ACQ_FOREIGN (one service = one file).", + "desc": "해외 매입 접수: 해외 카드 매입을 FOREIGN 채널로 접수한다. 원통화 금액과", "in": [ "T_MERCHANT", - "T_AMOUNT", - "T_BIZDATE", - "T_CCY" + "T_CCY", + "T_FX_AMT", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_PURCHASE_ID", - "T_FEE" + "T_AMOUNT", + "T_FX_AMT", + "T_ARG2", + "T_FEE", + "T_NET", + "T_ID1", + "T_ARG1" ] }, { @@ -1114,47 +1612,69 @@ const SVC_SCREENS = [ "desc": "통화 환산 등록: 해외매입의 원통화 금액에 적용환율(bps)을 곱해 원화를", "in": [ "T_PURCHASE_ID", - "T_FX_AMT", - "T_ARG1", "T_CCY", - "T_BIZDATE" + "T_FX_AMT", + "T_BIZDATE", + "T_ARG1" ], "out": [ "T_RC", + "T_ARG6", "T_ID1", - "T_AMOUNT" + "T_AMOUNT", + "T_FX_AMT", + "T_ARG2", + "T_ARG1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_FXRATE", - "desc": "통화 적용환율 조회: 해외매입 원장(ac_fx)에서 특정 통화의 최근 영업일", + "desc": "통화 적용환율 조회: 해외매입 원장에서 특정 통화의 최근 영업일 평균/최저/", "in": [ "T_CCY", "T_BIZDATE" ], "out": [ "T_RC", + "T_ARG6", + "T_COUNT", "T_ARG1", "T_ARG2", "T_ARG3", - "T_COUNT" + "T_ARG4", + "T_ARG5", + "T_GROSS", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_GRADESTAT", - "desc": "매입 가맹점등급별 통계: 가맹점 MDR(bps)을 프리미엄/일반/기본 등급으로 나눠", + "desc": "매입 가맹점등급별 통계: 가맹점 MDR(bps)을 프리미엄/일반/기본 등급으로", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_AMT1", + "T_AMT2", + "T_AMT3", "T_ARG1", "T_ARG2", "T_ARG3", - "T_GROSS" + "T_COUNT", + "T_GROSS", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -1164,28 +1684,40 @@ const SVC_SCREENS = [ "desc": "매입 보증 등록: 대형 매입에 대한 이행보증금을 원장에 GUARANTEE 항목으로", "in": [ "T_PURCHASE_ID", - "T_AMOUNT", "T_ARG1", "T_BIZDATE" ], "out": [ "T_RC", - "T_AMT1", - "T_SETTLE_ID" + "T_ARG6", + "T_ID1", + "T_AMOUNT", + "T_ARG2", + "T_GROSS", + "T_ARG1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_GUARQRY", - "desc": "매입 보증 조회: 한 매입에 적립된 보증금(GUARANTEE) 총액과 건수를 원장에서", + "desc": "매입 보증 조회: 한 매입에 적립된 보증금(GUARANTEE) 총액과 건수를", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_BIZDATE" ], "out": [ - "T_AMT1", + "T_RC", + "T_ARG6", "T_COUNT", - "T_RC" + "T_GROSS", + "T_AMOUNT", + "T_ARG1", + "T_ARG2", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -1194,98 +1726,160 @@ const SVC_SCREENS = [ "svc": "ACQ_HALFSTAT", "desc": "매입 반기별 통계: 지정 연도(T_ARG1) 매입을 상반기(1~6월)/하반기(7~12월)로", "in": [ - "T_ARG1" + "T_ARG1", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_AMT1", + "T_AMT2", + "T_ARG1", "T_ARG2", "T_ARG3", + "T_ARG4", + "T_AMT3", "T_COUNT", - "T_RC" + "T_GROSS", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_HOLD", - "desc": "매입 보류: 접수/대사(A,M) 상태만 보류로 전이, 반영 건수를 T_RC 로.", + "desc": "매입 보류: 접수/대사(A,M) 상태만 보류로 전이한다. 보류 사유를 미매입 원장에", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_REASON", + "T_BIZDATE" ], "out": [ - "T_RC" + "T_RC", + "T_AMOUNT", + "T_COUNT", + "T_GROSS", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ID1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_HOLDQRY", - "desc": "보류 매입 조회: 가맹점의 보류(H) 건을 최근 갱신순 커서로 훑어 건수·총액을", + "desc": "보류 매입 조회: 가맹점의 보류(H) 건을 집계해 건수·총액과 최고 보류액,", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_GROSS", - "T_RC" + "T_AMOUNT", + "T_PURCHASE_ID", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_AMT1", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_HOURSTAT", - "desc": "매입 시간대별 통계: 접수시각을 새벽/오전/오후/야간 네 구간으로 나눠 구간별", + "desc": "매입 시간대별 통계: 접수시각을 새벽/오전/오후/야간 네 구간으로 나눠", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_ARG1", "T_ARG2", "T_ARG3", "T_ARG4", - "T_RC" + "T_ARG5", + "T_COUNT", + "T_GROSS", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_INSTALL", - "desc": "* ACQ_INSTALL.pgc - ac 모듈 서비스 ACQ_INSTALL (one service = one file).", + "desc": "할부 등록: 요청 개월수로 매입금액을 균등 분할해 ac_install 에 회차별", "in": [ "T_PURCHASE_ID", "T_INSTALL_N", - "T_AMOUNT", "T_BIZDATE" ], "out": [ - "T_COUNT" + "T_RC", + "T_ARG6", + "T_COUNT", + "T_INSTALL_N", + "T_AMT1", + "T_ARG1", + "T_ARG2", + "T_GROSS" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_INSTCXL", - "desc": "할부 취소: 한 매입의 할부 스케줄 전 회차를 원장에서 삭제해 할부를 해지한다.", + "desc": "할부 취소: 한 매입의 할부 스케줄 전 회차를 원장에서 삭제해 할부를", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_REASON", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", - "T_RC" + "T_INSTALL_N", + "T_GROSS", + "T_ARG1", + "T_ID1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_INSTPLAN", - "desc": "할부 계획 조회: 한 매입의 약정 개월수와 회차 합계, 회차당 금액을 단건", + "desc": "할부 계획 조회: 한 매입의 약정 개월수와 회차 합계, 회차당 금액을 조회해", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_COUNT", "T_INSTALL_N", "T_GROSS", + "T_AMOUNT", + "T_ARG1", + "T_ARG2", + "T_ARG3", "T_AMT1", - "T_RC" + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -1300,63 +1894,107 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG6", + "T_COUNT", "T_GROSS", - "T_COUNT" + "T_ARG2", + "T_ARG3", + "T_ID1", + "T_ARG4" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_INSTQRY", - "desc": "할부 회차조회: 한 매입의 할부 스케줄(ac_install)을 회차 순으로 커서 조회해", + "desc": "할부 회차조회: 한 매입의 할부 스케줄을 집계해 총 회차수와 합계,", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_BIZDATE" ], "out": [ - "T_INSTALL_N", + "T_RC", + "T_ARG6", + "T_COUNT", "T_GROSS", - "T_RC" + "T_AMT1", + "T_AMT2", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ARG5", + "T_AMOUNT", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_ISSSTAT", - "desc": "매입 발급사별 통계: 영업일 발급사 그룹 집계에서 1위 발급사(건수·총액)와", + "desc": "매입 발급사별 통계: 영업일 발급사 그룹에서 1위 발급사(건수·총액)와", "in": [ "T_BIZDATE" ], "out": [ "T_RC", + "T_ARG6", + "T_COUNT", + "T_ARG1", + "T_AMT1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ARG5", + "T_GROSS", + "T_ARG7", + "T_ARG8", + "T_ID2" + ] + }, + { + "mod": "ac", + "modnm": "매입", + "svc": "ACQ_ISSUERCLS", + "desc": "발급사 분류 지정: 매입의 발급사(issuer) 코드를 지정 값으로 갱신하고", + "in": [ + "T_PURCHASE_ID", + "T_ISSUER", + "T_BIZDATE" + ], + "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_GROSS", "T_ARG1", "T_ARG2" ] }, - { - "mod": "ac", - "modnm": "매입", - "svc": "ACQ_ISSUERCLS", - "desc": "* ACQ_ISSUERCLS.pgc - ac 모듈 서비스 ACQ_ISSUERCLS (one service = one file).", - "in": [ - "T_PURCHASE_ID", - "T_ISSUER" - ], - "out": [] - }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_LIMITCHK", - "desc": "* ACQ_LIMITCHK.pgc - ac 모듈 서비스 ACQ_LIMITCHK (one service = one file).", + "desc": "매입 한도 점검: 가맹점 일한도와 당일 누적 매입을 비교해 초과 여부를", "in": [ "T_MERCHANT", "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_GROSS", - "T_RC" + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_COUNT", + "T_AMT1", + "T_ARG4", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -1365,12 +2003,18 @@ const SVC_SCREENS = [ "svc": "ACQ_LIMITEXC", "desc": "매입 한도 예외처리: 가맹점 일한도와 당일 누적 매입을 비교해 한도 초과 시", "in": [ - "T_MERCHANT", + "T_PURCHASE_ID", "T_BIZDATE" ], "out": [ "T_RC", - "T_DELTA" + "T_ARG6", + "T_GROSS", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ID1" ] }, { @@ -1379,11 +2023,18 @@ const SVC_SCREENS = [ "svc": "ACQ_LIMITOVR", "desc": "매입 한도 초과 승인: 한도 초과로 보류(H)된 매입을 관리자가 예외 승인해", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_REASON", + "T_BIZDATE" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG6", + "T_GROSS", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ID1" ] }, { @@ -1397,40 +2048,66 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG6", + "T_GROSS", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_COUNT", "T_AMT1", "T_AMT2", - "T_AMT3", - "T_ARG1" + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_MERCHSUM", - "desc": "* ACQ_MERCHSUM.pgc - ac 모듈 서비스 ACQ_MERCHSUM (one service = one file).", + "desc": "가맹점 일별 집계: 영업일 매입을 재집계해 merchant_summary 에 upsert 하고", "in": [ "T_MERCHANT", "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", - "T_GROSS" + "T_GROSS", + "T_FEE", + "T_NET", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ARG5", + "T_ARG6" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_MONSTAT", - "desc": "매입 월간 통계: 기준일이 속한 달 전체의 건수/총액/평균 거래액과 영업일수", + "desc": "매입 월간 통계: 기준일이 속한 달 전체의 건수/총액/평균 거래액과", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_GROSS", - "T_AMT1", "T_ARG1", - "T_RC" + "T_ARG2", + "T_AMT1", + "T_FEE", + "T_NET", + "T_ARG3", + "T_ARG4", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -1439,13 +2116,18 @@ const SVC_SCREENS = [ "svc": "ACQ_NETCALC", "desc": "정산금 재계산: 매입금액에서 수수료와 원천세 누계를 차감한 실정산금을", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_BIZDATE" ], "out": [ "T_RC", + "T_ARG6", "T_NET", + "T_DELTA", "T_TAX", - "T_DELTA" + "T_FEE", + "T_AMOUNT", + "T_ARG1" ] }, { @@ -1459,16 +2141,41 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", - "T_AMT1", + "T_ARG6", + "T_ADJ", "T_INSTALL_N", - "T_ADJ" + "T_ARG2", + "T_DELTA", + "T_FEE", + "T_ARG1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_PARTCXL", - "desc": "부분취소: 취소액이 잔여 매입금액 미만인 경우에만 허용. 수수료를 비례", + "desc": "부분취소: 취소액이 잔여 매입금액 미만인 경우에만 허용한다. 수수료를", + "in": [ + "T_PURCHASE_ID", + "T_AMOUNT", + "T_REASON", + "T_BIZDATE" + ], + "out": [ + "T_RC", + "T_ARG6", + "T_CANCEL_ID", + "T_AMOUNT", + "T_FEE", + "T_ARG1", + "T_ARG2" + ] + }, + { + "mod": "ac", + "modnm": "매입", + "svc": "ACQ_PARTIAL", + "desc": "부분처리 등록: 매입의 일부 금액만 확정 처리하고 나머지는 미확정으로", "in": [ "T_PURCHASE_ID", "T_AMOUNT", @@ -1476,23 +2183,12 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG6", "T_CANCEL_ID", - "T_AMT1", - "T_FEE" - ] - }, - { - "mod": "ac", - "modnm": "매입", - "svc": "ACQ_PARTIAL", - "desc": "* ACQ_PARTIAL.pgc - ac 모듈 서비스 ACQ_PARTIAL (one service = one file).", - "in": [ - "T_PURCHASE_ID", "T_AMOUNT", - "T_BIZDATE" - ], - "out": [ - "T_CANCEL_ID" + "T_ARG1", + "T_ARG2", + "T_ARG3" ] }, { @@ -1503,12 +2199,18 @@ const SVC_SCREENS = [ "in": [ "T_PURCHASE_ID", "T_AMOUNT", + "T_REASON", "T_BIZDATE" ], "out": [ "T_RC", + "T_ARG6", "T_CANCEL_ID", - "T_AMT2" + "T_AMOUNT", + "T_AMT1", + "T_ARG1", + "T_ARG2", + "T_ID1" ] }, { @@ -1517,102 +2219,153 @@ const SVC_SCREENS = [ "svc": "ACQ_PROMO", "desc": "프로모션 매입 등록: 행사 코드가 유효하면 수수료 0 의 프로모션 매입을", "in": [ - "T_AMOUNT", "T_MERCHANT", - "T_STR1", + "T_AMOUNT", "T_BIZDATE" ], "out": [ "T_RC", + "T_ARG6", "T_PURCHASE_ID", - "T_NET" + "T_FEE", + "T_NET", + "T_DELTA", + "T_ARG1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_PROMOQRY", - "desc": "프로모션 조회: 영업일 프로모션(category=PROMO) 매입의 건수와 매출, 감면", + "desc": "프로모션 조회: 영업일 프로모션(category=PROMO) 매입의 건수와 매출,", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_GROSS", "T_FEE", - "T_RC" + "T_NET", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_PROMOSTAT", - "desc": "프로모션 통계: 영업일 업종/분류별 매출을 그룹 집계해 분류코드·매출 쌍을", + "desc": "프로모션 통계: 영업일 업종/분류별 매출을 그룹 집계해 최다 분류와", "in": [ "T_BIZDATE" ], "out": [ - "T_GROSS", + "T_RC", + "T_ARG6", + "T_COUNT", + "T_AMT1", "T_AMT2", - "T_RC" + "T_ARG1", + "T_ARG2", + "T_GROSS", + "T_AMT3", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_QTRSTAT", - "desc": "매입 분기별 통계: 지정 연도(T_ARG1)의 매입 매출을 1~4분기로 나눠 분기별", + "desc": "매입 분기별 통계: 지정 연도(T_ARG1)의 매입 매출을 1~4분기로 나눠", "in": [ - "T_ARG1" + "T_ARG1", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_AMT1", + "T_AMT2", + "T_AMT3", + "T_AMT4", "T_ARG2", "T_ARG3", "T_ARG4", - "T_ARG5", - "T_GROSS" + "T_COUNT", + "T_GROSS", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_REBUY", - "desc": "재매입: 취소(C)된 원매입의 가맹점/금액/채널을 승계해 신규 매입을 생성하고", + "desc": "재매입: 취소(C)된 원매입의 가맹점/금액을 승계해 신규 매입을 생성하고", "in": [ "T_PURCHASE_ID", "T_BIZDATE" ], "out": [ "T_RC", + "T_ARG6", + "T_PURCHASE_ID", "T_ID1", + "T_AMOUNT", "T_FEE", - "T_NET" + "T_NET", + "T_COUNT", + "T_ARG1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_REBUYAPPR", - "desc": "재매입 승인: 취소(C)된 원매입에 한해 승인 원장(approval)에 재승인 행을", + "desc": "재매입 승인: 취소(C)된 원매입에 한해 승인 원장에 재승인 행을 남기고", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_BIZDATE" ], "out": [ "T_RC", - "T_ID1" + "T_ARG6", + "T_AMOUNT", + "T_ARG1", + "T_ARG2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_REBUYHIST", - "desc": "재매입 이력: 한 매입의 승인 원장을 승인시각 순으로 커서 조회해 승인 ID를", + "desc": "재매입 이력: 한 매입의 승인 원장을 집계해 총 승인 횟수와 최근 승인 ID,", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", - "T_RC" + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ID1", + "T_AMOUNT", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -1626,48 +2379,67 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_ID1", - "T_RC" + "T_COUNT", + "T_ARG1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_RECLASS", - "desc": "업종/분류 재지정: category 갱신 후 같은 가맹점에서 동일 분류 건수를 집계 회신.", + "desc": "업종/분류 재지정: 매입의 category 를 요청 분류로 갱신하고 같은 가맹점의", "in": [ "T_PURCHASE_ID", - "T_CATEGORY" + "T_CATEGORY", + "T_BIZDATE" ], "out": [ - "T_COUNT" + "T_RC", + "T_ARG6", + "T_COUNT", + "T_ARG1", + "T_ARG2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_REFUND", - "desc": "* ACQ_REFUND.pgc - ac 모듈 서비스 ACQ_REFUND (one service = one file).", + "desc": "매입 환불: 정산 완료 매입에 대한 환불을 접수한다. REFUND 취소행을", "in": [ "T_PURCHASE_ID", "T_AMOUNT", - "T_BIZDATE", - "T_REASON" + "T_REASON", + "T_BIZDATE" ], "out": [ - "T_CANCEL_ID" + "T_RC", + "T_ARG6", + "T_CANCEL_ID", + "T_AMOUNT", + "T_ARG1", + "T_ARG2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_RELEASE", - "desc": "보류 해제: 직전 상태를 조회한 뒤 H->A 전이, 이전 상태를 함께 회신.", + "desc": "보류 해제: 보류(H) 상태의 매입을 접수(A)로 되돌린다. 직전 상태를 함께", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_REASON", + "T_BIZDATE" ], "out": [ - "T_RC" + "T_RC", + "T_ARG6", + "T_ARG1", + "T_ID1", + "T_AMOUNT" ] }, { @@ -1676,23 +2448,34 @@ const SVC_SCREENS = [ "svc": "ACQ_REMATCH", "desc": "승인미매칭 재처리: 미매입 원장에 남은 건에 유효 승인이 도착했는지 다시", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_BIZDATE" ], "out": [ "T_RC", + "T_ARG6", + "T_COUNT", "T_ARG1", - "T_ARG2" + "T_ARG2", + "T_ARG3" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_REPROC", - "desc": "* ACQ_REPROC.pgc - ac 모듈 서비스 ACQ_REPROC (one service = one file).", + "desc": "매입 재처리: 취소/무효로 종료된 매입을 접수(A) 상태로 되살려 재처리", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_REASON", + "T_BIZDATE" ], - "out": [] + "out": [ + "T_RC", + "T_ARG6", + "T_COUNT", + "T_ARG1" + ] }, { "mod": "ac", @@ -1700,14 +2483,18 @@ const SVC_SCREENS = [ "svc": "ACQ_RESERVE", "desc": "매입 예약: 활성(ACTIVE) 가맹점에 대해 미래 영업일 예약 매입(status P)을", "in": [ - "T_AMOUNT", "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ "T_RC", + "T_ARG6", "T_PURCHASE_ID", - "T_FEE" + "T_FEE", + "T_NET", + "T_COUNT", + "T_ARG1" ] }, { @@ -1716,25 +2503,42 @@ const SVC_SCREENS = [ "svc": "ACQ_RESERVECXL", "desc": "매입 예약취소: 아직 접수 전환되지 않은 예약(P) 건만 물리 삭제한다.", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_REASON", + "T_BIZDATE" ], "out": [ "T_RC", - "T_AMOUNT" + "T_ARG6", + "T_AMOUNT", + "T_ARG1", + "T_COUNT", + "T_GROSS", + "T_ARG2", + "T_ID1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_RESERVEQRY", - "desc": "매입 예약 조회: 가맹점의 대기 중 예약(P)을 예약일 순 커서로 훑어 건수와", + "desc": "매입 예약 조회: 가맹점의 대기 중 예약(P)을 집계해 건수와 총액,", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_GROSS", - "T_RC" + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_AMT1", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -1749,8 +2553,10 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG6", "T_CANCEL_ID", - "T_AMOUNT" + "T_AMOUNT", + "T_ARG1" ] }, { @@ -1759,27 +2565,49 @@ const SVC_SCREENS = [ "svc": "ACQ_RETURNQRY", "desc": "매입 반품 조회: 가맹점의 영업일 반품/교환(RETURN·EXCHANGE) 취소 이력을", "in": [ + "T_MERCHANT", "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_GROSS", - "T_RC" + "T_CANCEL_ID", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_AMT1", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_RETURNSTAT", - "desc": "매입 반품 통계: 영업일 취소 원장을 반품/교환/환불 유형별로 한 번에 분해", + "desc": "매입 반품 통계: 영업일 취소 원장을 반품/교환/환불 유형별로 분해 집계해", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_ARG1", "T_ARG2", "T_ARG3", - "T_GROSS" + "T_AMT1", + "T_AMT2", + "T_AMT3", + "T_ARG4", + "T_ARG5", + "T_COUNT", + "T_GROSS", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -1788,13 +2616,22 @@ const SVC_SCREENS = [ "svc": "ACQ_SETTLEDUE", "desc": "정산예정 조회: 대사완료(M) 상태이면서 아직 정산행이 만들어지지 않은", "in": [ - "T_MERCHANT", "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_NET", - "T_RC" + "T_GROSS", + "T_ARG1", + "T_AMT1", + "T_ARG2", + "T_ARG3", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -1807,40 +2644,61 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_NET", - "T_RC" + "T_FEE", + "T_GROSS", + "T_AMT1", + "T_ARG1", + "T_ARG2", + "T_AMT2", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_SETTLELINK", - "desc": "정산 연계: 가맹점 미지정 시 매입에서 조회, settlement 링크 행(status='LINKED') 생성.", + "desc": "정산 연계: 대사완료 매입에 대해 settlement 링크 행(status='LINKED')을", "in": [ "T_PURCHASE_ID", - "T_NET", - "T_MERCHANT", "T_BIZDATE" ], "out": [ - "T_SETTLE_ID" + "T_RC", + "T_ARG6", + "T_SETTLE_ID", + "T_NET", + "T_ARG1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_SETTLEPRE", - "desc": "매입-정산 예정대사: 가맹점의 영업일 대사완료(M) 매입 순정산 예정액과 이미", + "desc": "매입-정산 예정대사: 가맹점의 영업일 대사완료(M) 매입 순정산 예정액과", "in": [ "T_MERCHANT", "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_COUNT", "T_NET", "T_AMT1", - "T_DELTA", - "T_RC" + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_GROSS", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -1850,41 +2708,67 @@ const SVC_SCREENS = [ "desc": "매입 분할납부 등록: 매입금액을 요청 회차수로 균등 분할(잔액은 마지막", "in": [ "T_PURCHASE_ID", - "T_INSTALL_N", + "T_ARG1", "T_BIZDATE" ], "out": [ "T_RC", + "T_ARG6", "T_COUNT", "T_AMT1", - "T_AMT2" + "T_GROSS", + "T_ARG2", + "T_ARG3" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_SPLITQRY", - "desc": "분할납부 회차 조회: 매입 건의 회차 스케줄을 커서로 순회하며 회차수·합계와", + "desc": "분할납부 회차 조회: 매입 건의 회차 스케줄을 집계해 회차수·합계와 최소/최대", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_GROSS", "T_AMT1", "T_AMT2", - "T_RC" + "T_AMOUNT", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_STATUS", - "desc": "* ACQ_STATUS.pgc - ac 모듈 서비스 ACQ_STATUS (one service = one file).", + "desc": "매입 상태 조회: 매입 ID 로 현재 상태를 읽어 돌려주고, 상태에 따른 진행 단계와", "in": [ "T_PURCHASE_ID" ], - "out": [] + "out": [ + "T_RC", + "T_AMOUNT", + "T_FEE", + "T_NET", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT1" + ] }, { "mod": "ac", @@ -1892,42 +2776,57 @@ const SVC_SCREENS = [ "svc": "ACQ_TAXCXL", "desc": "세금계산서 취소: 원본 계산서를 상쇄하는 음수(공급가/부가세 부호 반전)", "in": [ - "T_ID1" + "T_PURCHASE_ID", + "T_BIZDATE" ], "out": [ "T_RC", - "T_ID2", - "T_TAX" + "T_ARG6", + "T_ID1", + "T_GROSS", + "T_TAX", + "T_COUNT", + "T_ARG1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_TAXINV", - "desc": "* ACQ_TAXINV.pgc - ac 모듈 서비스 ACQ_TAXINV (one service = one file).", + "desc": "세금계산서 발행: 매입 공급가에 대한 부가세를 산출해 ac_tax_invoice 에", "in": [ "T_PURCHASE_ID", - "T_AMOUNT", - "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT" ], "out": [ - "T_TAX" + "T_RC", + "T_ARG6", + "T_ID1", + "T_TAX", + "T_GROSS", + "T_ARG1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_TAXMODIFY", - "desc": "세금계산서 수정발행: 기 발행된 계산서(ac_tax_invoice)의 공급가·부가세를", + "desc": "세금계산서 수정발행: 기 발행된 계산서의 공급가·부가세를 정정 금액으로", "in": [ - "T_ID1", - "T_GROSS" + "T_PURCHASE_ID", + "T_AMOUNT", + "T_BIZDATE" ], "out": [ "T_RC", + "T_ARG6", + "T_ID1", "T_GROSS", - "T_TAX" + "T_TAX", + "T_DELTA", + "T_ARG1", + "T_AMT1" ] }, { @@ -1936,13 +2835,22 @@ const SVC_SCREENS = [ "svc": "ACQ_TAXQRY", "desc": "세금계산서 조회: 한 매입에 발행된 계산서의 공급가·부가세 누계와 발행", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_COUNT", "T_GROSS", "T_TAX", - "T_COUNT", - "T_RC" + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -1951,13 +2859,17 @@ const SVC_SCREENS = [ "svc": "ACQ_TAXREISSUE", "desc": "세금계산서 재발행: 원본 계산서의 공급가/부가세를 승계한 신규 계산서를", "in": [ - "T_ID1", + "T_PURCHASE_ID", "T_BIZDATE" ], "out": [ "T_RC", + "T_ARG6", + "T_ID1", "T_ID2", - "T_TAX" + "T_GROSS", + "T_TAX", + "T_ARG1" ] }, { @@ -1966,39 +2878,60 @@ const SVC_SCREENS = [ "svc": "ACQ_TAXREVERSE", "desc": "세금계산서 역발행: 매출 취소분에 대한 (-)공급가 계산서를 새로 발행한다.", "in": [ - "T_ID1", + "T_PURCHASE_ID", "T_BIZDATE" ], "out": [ "T_RC", - "T_ID2" + "T_ARG6", + "T_ID1", + "T_ID2", + "T_GROSS", + "T_TAX", + "T_ARG1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_TOPMERCH", - "desc": "상위 가맹점 조회: 영업일 매출 상위 N개 가맹점을 커서로 뽑아 가맹점 ID와", + "desc": "상위 가맹점 조회: 영업일 매출 1위 가맹점과 매출액, 상위 3사 합계를", "in": [ - "T_ARG1", "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_AMT1", + "T_ARG1", + "T_AMT2", + "T_ARG2", + "T_ARG3", + "T_ARG4", "T_COUNT", "T_GROSS", - "T_RC" + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_UNMATCH", - "desc": "* ACQ_UNMATCH.pgc - ac 모듈 서비스 ACQ_UNMATCH (one service = one file).", + "desc": "미매입 등록: 대사에서 짝을 찾지 못한 매입을 미매입 원장에 사유와 함께", "in": [ + "T_PURCHASE_ID", + "T_REASON", "T_BIZDATE" ], "out": [ - "T_COUNT" + "T_RC", + "T_ARG6", + "T_ID1", + "T_COUNT", + "T_ARG1", + "T_AMOUNT" ] }, { @@ -2007,27 +2940,42 @@ const SVC_SCREENS = [ "svc": "ACQ_VELOCITY", "desc": "매입 유입속도 점검: 가맹점의 최근 10분간 접수 건수를 재어 허용치 초과", "in": [ - "T_ARG2", - "T_MERCHANT" + "T_MERCHANT", + "T_ARG1", + "T_BIZDATE" ], "out": [ - "T_ARG1", + "T_RC", + "T_ARG6", + "T_COUNT", + "T_ARG2", "T_ARG3", - "T_RC" + "T_ARG4", + "T_AMT1", + "T_AMT2", + "T_GROSS", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_VOID", - "desc": "* ACQ_VOID.pgc - ac 모듈 서비스 ACQ_VOID (one service = one file).", + "desc": "매입 무효(void): 당일 접수분에 한해 매입을 무효(V)로 전이한다.", "in": [ "T_PURCHASE_ID", - "T_AMOUNT", + "T_REASON", "T_BIZDATE" ], "out": [ - "T_CANCEL_ID" + "T_RC", + "T_ARG6", + "T_AMOUNT", + "T_DELTA", + "T_ARG1", + "T_ID1" ] }, { @@ -2039,36 +2987,59 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_GROSS", "T_FEE", - "T_AMT1" + "T_NET", + "T_AMT1", + "T_AMT2", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_WHT", - "desc": "* ACQ_WHT.pgc - ac 모듈 서비스 ACQ_WHT (one service = one file).", + "desc": "원천징수: 가맹점 수수료에 대한 원천세를 운영 파라미터(WHT_BPS) 기준으로", "in": [ "T_PURCHASE_ID", "T_BIZDATE" ], "out": [ - "T_TAX" + "T_RC", + "T_ARG6", + "T_TAX", + "T_ID1", + "T_ARG2", + "T_FEE", + "T_NET", + "T_ARG1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_XFERLINK", - "desc": "* ACQ_XFERLINK.pgc - ac 모듈 서비스 ACQ_XFERLINK (one service = one file).", + "desc": "이체 연계: 정산 완료 매입의 지급 이체를 원장에 XFER 분개로 연결한다.", "in": [ "T_PURCHASE_ID", - "T_AMOUNT", "T_BIZDATE" ], - "out": [] + "out": [ + "T_RC", + "T_ARG6", + "T_ID1", + "T_NET", + "T_AMOUNT", + "T_ARG1" + ] }, { "mod": "au", @@ -2076,14 +3047,17 @@ const SVC_SCREENS = [ "svc": "APPROVAL_RATE", "desc": "가맹점 승인율 조회: 영업일 접수 대비 승인 성공(A/M/S) 비율을 bps 로", "in": [ + "T_KEY1", + "T_AMOUNT", "T_MERCHANT", "T_BIZDATE" ], "out": [ + "T_RC", + "T_FEE", "T_ARG1", "T_COUNT", - "T_ARG2", - "T_RC" + "T_ARG2" ] }, { @@ -2094,12 +3068,13 @@ const SVC_SCREENS = [ "in": [ "T_KEY1", "T_MERCHANT", - "T_AMOUNT", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT" ], "out": [ - "T_ID1", - "T_RC" + "T_RC", + "T_FEE", + "T_ID1" ] }, { @@ -2108,12 +3083,18 @@ const SVC_SCREENS = [ "svc": "AUTH_AMT_ADJUST", "desc": "승인금액 조정(incremental/decremental auth): 호텔·주유소처럼 최종 금액이", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_ID1", - "T_DELTA" + "T_DELTA", + "T_ID2" ], "out": [ - "T_AMOUNT", - "T_RC" + "T_RC", + "T_FEE", + "T_AMOUNT" ] }, { @@ -2122,9 +3103,17 @@ const SVC_SCREENS = [ "svc": "AUTH_APPROVE", "desc": "승인 확정(pending->approved): dbio 래퍼로 상태만 A 로 확정.", "in": [ - "T_ID1" + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", + "T_ID1", + "T_ID2" ], - "out": [] + "out": [ + "T_RC", + "T_FEE" + ] }, { "mod": "au", @@ -2132,13 +3121,17 @@ const SVC_SCREENS = [ "svc": "AUTH_CANCEL", "desc": "승인 전체 취소 + 한도 원복.", "in": [ - "T_ID1", - "T_AMOUNT", + "T_MERCHANT", "T_KEY1", "T_BIZDATE", - "T_REASON" + "T_REASON", + "T_ID1", + "T_AMOUNT", + "T_ID2" ], "out": [ + "T_RC", + "T_FEE", "T_CANCEL_ID" ] }, @@ -2148,10 +3141,16 @@ const SVC_SCREENS = [ "svc": "AUTH_CAPTURE", "desc": "승인 매입확정(capture): 상태 A->M(매입대상).", "in": [ - "T_ID1" + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", + "T_ID1", + "T_ID2" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -2160,10 +3159,18 @@ const SVC_SCREENS = [ "svc": "AUTH_DECLINE", "desc": "승인 거절 처리: 거절 사유와 함께 상태 D 로 갱신.", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", + "T_REASON", "T_ID1", - "T_REASON" + "T_ID2" ], - "out": [] + "out": [ + "T_RC", + "T_FEE" + ] }, { "mod": "au", @@ -2171,14 +3178,18 @@ const SVC_SCREENS = [ "svc": "AUTH_DELAY_MON", "desc": "승인 지연 모니터: 승인(A) 상태로 기준 시간(분)을 넘겨 매입확정이 안 된", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE", "T_ARG1" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", "T_ARG2", - "T_ARG3", - "T_RC" + "T_ARG3" ] }, { @@ -2187,14 +3198,16 @@ const SVC_SCREENS = [ "svc": "AUTH_DUP_CHK", "desc": "중복승인 검출: 단말 재시도 등으로 동일 카드+가맹점+금액 조합이 직전 5분내", "in": [ + "T_BIZDATE", "T_KEY1", "T_MERCHANT", "T_AMOUNT" ], "out": [ + "T_RC", + "T_FEE", "T_ARG1", - "T_ID1", - "T_RC" + "T_ID1" ] }, { @@ -2203,12 +3216,17 @@ const SVC_SCREENS = [ "svc": "AUTH_EXPIRE_CHK", "desc": "미매입 승인 만기 점검: 기준일수를 넘겨도 매입확정되지 않은 승인(A) 건의", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_ARG1" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", "T_GROSS", - "T_RC", "T_ID1" ] }, @@ -2218,9 +3236,14 @@ const SVC_SCREENS = [ "svc": "AUTH_HISTORY", "desc": "카드 승인 이력 건수 조회.", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT" ] }, @@ -2230,10 +3253,16 @@ const SVC_SCREENS = [ "svc": "AUTH_HOLD", "desc": "승인 보류: 승인/예비(A,P) 상태만 보류(H)로 전이, 반영 건수를 T_RC 로.", "in": [ - "T_ID1" + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", + "T_ID1", + "T_ID2" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -2248,8 +3277,9 @@ const SVC_SCREENS = [ "T_AMOUNT" ], "out": [ - "T_AMT1", "T_RC", + "T_FEE", + "T_AMT1", "T_ID2" ] }, @@ -2259,12 +3289,16 @@ const SVC_SCREENS = [ "svc": "AUTH_PARTCXL", "desc": "부분 취소: 승인 금액 감액 + 한도 부분 원복.", "in": [ + "T_MERCHANT", + "T_KEY1", + "T_BIZDATE", "T_ID1", "T_AMOUNT", - "T_KEY1", - "T_BIZDATE" + "T_ID2" ], "out": [ + "T_RC", + "T_FEE", "T_CANCEL_ID", "T_AMOUNT" ] @@ -2281,9 +3315,10 @@ const SVC_SCREENS = [ "T_AMOUNT" ], "out": [ + "T_RC", + "T_FEE", "T_ID1", - "T_AMT1", - "T_RC" + "T_AMT1" ] }, { @@ -2292,10 +3327,16 @@ const SVC_SCREENS = [ "svc": "AUTH_RELEASE", "desc": "보류 해제: 직전 상태를 조회한 뒤 H->A 전이, 이전 상태를 함께 회신.", "in": [ - "T_ID1" + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", + "T_ID1", + "T_ID2" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -2304,10 +3345,15 @@ const SVC_SCREENS = [ "svc": "AUTH_REPROC_REQ", "desc": "승인 재처리 요청: 거절(D)/보류(H) 상태의 승인을 재처리 대상으로 다시", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_ID2" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -2316,12 +3362,16 @@ const SVC_SCREENS = [ "svc": "AUTH_REPROC_STAT", "desc": "재처리 현황: 영업일 승인 원장에서 보류(H) 대기 건수와, 갱신시각이 생성시각", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ + "T_RC", + "T_FEE", "T_ARG1", - "T_ARG2", - "T_RC" + "T_ARG2" ] }, { @@ -2330,11 +3380,17 @@ const SVC_SCREENS = [ "svc": "AUTH_RESEND", "desc": "승인 재전송: 망 단절 등으로 응답을 못 받은 단말이 원승인의 승인코드를", "in": [ - "T_ID1" + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", + "T_ID1", + "T_ID2" ], "out": [ - "T_AMOUNT", - "T_RC" + "T_RC", + "T_FEE", + "T_AMOUNT" ] }, { @@ -2343,12 +3399,16 @@ const SVC_SCREENS = [ "svc": "AUTH_RETRY", "desc": "거절 승인 재시도 (상태 D->A 재승인).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_KEY1", "T_ID1", "T_AMOUNT", - "T_KEY1" + "T_ID2" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -2357,11 +3417,17 @@ const SVC_SCREENS = [ "svc": "AUTH_REVERSE", "desc": "승인 역전(reversal) + 한도 원복.", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_KEY1", "T_ID1", "T_AMOUNT", - "T_KEY1" + "T_ID2" ], - "out": [] + "out": [ + "T_RC", + "T_FEE" + ] }, { "mod": "au", @@ -2369,9 +3435,14 @@ const SVC_SCREENS = [ "svc": "AUTH_ROUTE", "desc": "승인 라우팅(발급사 BIN 기준 라우팅 코드 산출).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_FEE", "T_ARG1" ] }, @@ -2381,9 +3452,16 @@ const SVC_SCREENS = [ "svc": "AUTH_STATUS", "desc": "승인 상태 조회 (dbio).", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_ID1" ], - "out": [] + "out": [ + "T_RC", + "T_FEE" + ] }, { "mod": "au", @@ -2395,6 +3473,16 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_FEE", + "T_TAX", "T_COUNT", "T_GROSS" ] @@ -2405,12 +3493,16 @@ const SVC_SCREENS = [ "svc": "AUTH_VOID", "desc": "당일 승인 무효화(void) + 한도 원복.", "in": [ + "T_MERCHANT", + "T_KEY1", + "T_BIZDATE", "T_ID1", "T_AMOUNT", - "T_KEY1", - "T_BIZDATE" + "T_ID2" ], "out": [ + "T_RC", + "T_FEE", "T_CANCEL_ID" ] }, @@ -2420,11 +3512,15 @@ const SVC_SCREENS = [ "svc": "BIN_ROUTE_INQ", "desc": "BIN별 라우팅 조회: 카드번호 선두 6자리(BIN)의 첫 자리로 매입 게이트웨이를", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_FEE", + "T_COUNT" ] }, { @@ -2433,12 +3529,16 @@ const SVC_SCREENS = [ "svc": "BLACKLIST_ADD", "desc": "블랙리스트 등재: 카드를 블랙리스트에 올리고(기등재 건은 사유만 갱신),", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1", "T_REASON" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_FEE", + "T_COUNT" ] }, { @@ -2447,10 +3547,14 @@ const SVC_SCREENS = [ "svc": "BLACKLIST_CHK", "desc": "블랙리스트 카드 조회.", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -2458,8 +3562,15 @@ const SVC_SCREENS = [ "modnm": "승인/한도", "svc": "BLACKLIST_COUNT", "desc": "블랙리스트 통계: 전체 등재 수, 도난(REPORTED_STOLEN/STOLEN) 등재 수,", - "in": [], + "in": [ + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT" + ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", "T_ARG1", "T_ARG2" @@ -2471,11 +3582,15 @@ const SVC_SCREENS = [ "svc": "BLACKLIST_DEL", "desc": "블랙리스트 해제: 등재 행을 삭제하고, 옵션(T_ARG1=1)에 따라 함께 정지됐던", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1", "T_ARG1" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -2484,11 +3599,15 @@ const SVC_SCREENS = [ "svc": "BLACKLIST_INQ", "desc": "블랙리스트 조회: 특정 카드의 등재 여부/사유/등재일시(문자열 포맷)를", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_FEE", + "T_COUNT" ] }, { @@ -2497,9 +3616,14 @@ const SVC_SCREENS = [ "svc": "BLACKLIST_PROMO", "desc": "블랙 승격 후보 조회: 영업일 부정 적발 점수 합이 1500 이상이면서 아직", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT" ] }, @@ -2509,10 +3633,15 @@ const SVC_SCREENS = [ "svc": "BRAND_AUTH_STAT", "desc": "브랜드별 승인 통계: 영업일 승인 원장을 카드 선두자리(브랜드)로 그룹핑해", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", + "T_FEE", "T_ARG1" ] }, @@ -2522,11 +3651,15 @@ const SVC_SCREENS = [ "svc": "BRAND_POLICY", "desc": "브랜드별 승인 정책: 카드 선두자리로 브랜드(VISA/MC/AMEX/LOCAL)를 판별하고", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_FEE", + "T_COUNT" ] }, { @@ -2535,13 +3668,18 @@ const SVC_SCREENS = [ "svc": "CANCEL_AMT_INQ", "desc": "취소가능액 조회: 원승인 금액에서 이미 취소된 누계를 빼 취소가능 잔액을", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_ID2" ], "out": [ + "T_RC", + "T_FEE", "T_AMT1", "T_AMT2", - "T_AMT3", - "T_RC" + "T_AMT3" ] }, { @@ -2550,11 +3688,16 @@ const SVC_SCREENS = [ "svc": "CANCEL_FULL", "desc": "전액 취소: 승인(A)/매입확정(M) 상태의 원승인을 전액 취소 처리한다. 취소", "in": [ - "T_ID2", - "T_REASON" + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", + "T_REASON", + "T_ID2" ], "out": [ "T_RC", + "T_FEE", "T_ID1", "T_AMT1" ] @@ -2565,10 +3708,16 @@ const SVC_SCREENS = [ "svc": "CANCEL_HISTORY", "desc": "승인건 취소 이력: 특정 원승인에 걸린 취소들을 시간순 커서로 순회하며", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_ID2" ], "out": [ + "T_RC", "T_COUNT", + "T_FEE", "T_AMT1" ] }, @@ -2578,11 +3727,17 @@ const SVC_SCREENS = [ "svc": "CANCEL_REPROC", "desc": "승인취소 재처리: au_cancel 에 접수됐지만 원승인 원장에 반영되지 못한 건을", "in": [ - "T_CANCEL_ID" + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", + "T_CANCEL_ID", + "T_ID2" ], "out": [ - "T_ID1", - "T_RC" + "T_RC", + "T_FEE", + "T_ID1" ] }, { @@ -2591,9 +3746,14 @@ const SVC_SCREENS = [ "svc": "CANCEL_STATUS", "desc": "취소 처리현황 조회: 영업일 기준 취소 접수를 유형(FULL/PARTIAL/VOID)별로", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", "T_ARG1", "T_ARG2", @@ -2607,9 +3767,14 @@ const SVC_SCREENS = [ "svc": "CANCEL_SUMMARY", "desc": "일별 취소 집계: 영업일 취소 원장을 전체/전액(FULL)/부분(PARTIAL)로 나눠", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", "T_AMT1", "T_ARG1", @@ -2622,12 +3787,16 @@ const SVC_SCREENS = [ "svc": "CARD_ACTIVATE", "desc": "카드 신규 활성화: 원장이 없으면 요청 한도(기본 100만원)로 생성하고, 이미", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1", "T_AMT1" ], "out": [ - "T_AMT1", - "T_RC" + "T_RC", + "T_FEE", + "T_AMT1" ] }, { @@ -2636,10 +3805,14 @@ const SVC_SCREENS = [ "svc": "CARD_DAILY_RPT", "desc": "카드 일일 승인 리포트: 카드+영업일 기준 상태별(승인/거절/취소류/매입) 건수,", "in": [ + "T_MERCHANT", + "T_AMOUNT", "T_KEY1", "T_BIZDATE" ], "out": [ + "T_RC", + "T_FEE", "T_ARG1", "T_ARG2", "T_ARG3", @@ -2655,12 +3828,16 @@ const SVC_SCREENS = [ "svc": "CARD_EXPIRE_CHK", "desc": "카드 휴면(만료 근사) 점검: 마지막 승인 이후 경과일을 EPOCH 로 환산해", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", - "T_ARG1", - "T_RC" + "T_ARG1" ] }, { @@ -2669,11 +3846,15 @@ const SVC_SCREENS = [ "svc": "CARD_LOST_REG", "desc": "분실 등록: 카드 원장 상태를 LOST 로 바꾸고 블랙리스트에 LOST 사유로", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1", "T_REASON" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -2682,10 +3863,14 @@ const SVC_SCREENS = [ "svc": "CARD_REACTIVATE", "desc": "카드 재활성화: 정지/동결/분실 상태의 원장을 ACTIVE 로 되돌리고, 분실·토큰", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -2694,11 +3879,15 @@ const SVC_SCREENS = [ "svc": "CARD_REISSUE", "desc": "카드 재발급 연계: 구 카드의 한도원장을 신 카드번호로 이관 생성하고", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1", "T_KEY2" ], "out": [ "T_RC", + "T_FEE", "T_AMT1" ] }, @@ -2708,10 +3897,14 @@ const SVC_SCREENS = [ "svc": "CARD_RESUME", "desc": "카드 정지 해제: 블랙리스트에 남아 있으면 해제를 거부하고,", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -2720,10 +3913,15 @@ const SVC_SCREENS = [ "svc": "CARD_STATE_HIST", "desc": "카드 승인상태 이력 분포: 카드의 전체 승인 건을 상태(status)별로 집계하는", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", "T_COUNT", + "T_FEE", "T_ARG1" ] }, @@ -2733,14 +3931,18 @@ const SVC_SCREENS = [ "svc": "CARD_STATE_INQ", "desc": "카드 상태 종합조회: 한도원장(상태/기본·임시한도/사용액)에", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_FEE", "T_AMT1", "T_AMT2", "T_AMT3", - "T_COUNT", - "T_RC" + "T_COUNT" ] }, { @@ -2749,13 +3951,16 @@ const SVC_SCREENS = [ "svc": "CARD_STOLEN_REG", "desc": "도난 등록: 원장 상태를 STOLEN 으로 전환하고 블랙리스트를 STOLEN 사유로", "in": [ + "T_MERCHANT", + "T_AMOUNT", "T_KEY1", "T_REASON", "T_BIZDATE" ], "out": [ - "T_ID1", - "T_RC" + "T_RC", + "T_FEE", + "T_ID1" ] }, { @@ -2764,12 +3969,16 @@ const SVC_SCREENS = [ "svc": "CARD_SUSPEND", "desc": "카드 정지: ACTIVE 상태의 카드만 SUSPEND 로 전환하고, 참고용으로", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1", "T_REASON" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_FEE", + "T_COUNT" ] }, { @@ -2778,10 +3987,14 @@ const SVC_SCREENS = [ "svc": "CARD_VALIDATE", "desc": "카드번호 Luhn 검증 (common lib).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -2790,12 +4003,17 @@ const SVC_SCREENS = [ "svc": "CHANNEL_POLICY", "desc": "채널별 승인 정책: 매입 원장에서 당일 해당 채널(POS/EDC/EDI/FOREIGN)의", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_CHANNEL" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", - "T_ARG1", - "T_RC" + "T_ARG1" ] }, { @@ -2804,10 +4022,15 @@ const SVC_SCREENS = [ "svc": "CHANNEL_STAT", "desc": "채널별 매입 통계: 영업일 매입을 채널별로 그룹핑하는 커서를 순회하며", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", + "T_FEE", "T_GROSS" ] }, @@ -2817,10 +4040,16 @@ const SVC_SCREENS = [ "svc": "DCC_QUOTE", "desc": "해외승인 DCC 환산 견적(통화 -> 원화).", "in": [ - "T_FX_AMT", - "T_CCY" + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", + "T_CCY", + "T_FX_AMT" ], "out": [ + "T_RC", + "T_FEE", "T_AMOUNT", "T_ARG1" ] @@ -2831,10 +4060,15 @@ const SVC_SCREENS = [ "svc": "DECLINE_ANALYZE", "desc": "승인 거절사유 분석: 영업일 거절(D) 건을 사유별로 그룹핑해 상위 3개 사유를", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", + "T_FEE", "T_ARG1" ] }, @@ -2844,10 +4078,16 @@ const SVC_SCREENS = [ "svc": "FRAUD_CASE_CLOSE", "desc": "부정 케이스 종결: 미종결 부정로그의 사유 뒤에 '/CLOSED' 를 붙여 종결 표시", "in": [ - "T_ID1" + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", + "T_ID1", + "T_ID2" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -2856,9 +4096,14 @@ const SVC_SCREENS = [ "svc": "FRAUD_CASE_INQ", "desc": "이상거래 케이스 조회: 카드의 부정거래 적발 이력을 집계(건수/최고/평균점수)", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", "T_ARG1", "T_ARG2" @@ -2870,10 +4115,15 @@ const SVC_SCREENS = [ "svc": "FRAUD_CASE_LIST", "desc": "부정 케이스 상위목록: 영업일 부정로그를 점수 내림차순 상위 5건 커서로", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ - "T_COUNT" + "T_RC", + "T_COUNT", + "T_FEE" ] }, { @@ -2882,15 +4132,17 @@ const SVC_SCREENS = [ "svc": "FRAUD_DETECT", "desc": "부정거래 스코어링 + 위험시 fraud_log 기록.", "in": [ - "T_ID1", - "T_AMOUNT", + "T_MERCHANT", "T_KEY1", - "T_BIZDATE" + "T_BIZDATE", + "T_ID1", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_FEE", "T_ARG1", - "T_ID2", - "T_RC" + "T_ID2" ] }, { @@ -2899,13 +4151,15 @@ const SVC_SCREENS = [ "svc": "FRAUD_RULE_AMT", "desc": "이상거래 고액 룰: 거래금액이 카드의 30일 평균 사용액의 3배를 넘고", "in": [ + "T_MERCHANT", "T_KEY1", "T_BIZDATE", "T_AMOUNT" ], "out": [ - "T_AMT1", "T_RC", + "T_FEE", + "T_AMT1", "T_ARG1", "T_ID1" ] @@ -2916,13 +4170,16 @@ const SVC_SCREENS = [ "svc": "FRAUD_RULE_DUP", "desc": "이상거래 동일금액 반복 룰: 당일 카드 승인 가운데 같은 금액이 3회 이상", "in": [ + "T_MERCHANT", + "T_AMOUNT", "T_KEY1", "T_BIZDATE" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", "T_AMT1", - "T_RC", "T_ID1" ] }, @@ -2932,12 +4189,15 @@ const SVC_SCREENS = [ "svc": "FRAUD_RULE_FREQ", "desc": "이상거래 다빈도 룰: 직전 10분간 승인 시도(거절 포함)가 5건 이상이면", "in": [ + "T_MERCHANT", + "T_AMOUNT", "T_KEY1", "T_BIZDATE" ], "out": [ - "T_COUNT", "T_RC", + "T_FEE", + "T_COUNT", "T_ID1" ] }, @@ -2947,13 +4207,16 @@ const SVC_SCREENS = [ "svc": "FRAUD_RULE_INTL", "desc": "고액(해외성) 이상거래 룰: 최근 1일 100만원 초과 승인이 3건 이상이면", "in": [ + "T_MERCHANT", + "T_AMOUNT", "T_KEY1", "T_BIZDATE" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", "T_GROSS", - "T_RC", "T_ID1" ] }, @@ -2963,12 +4226,15 @@ const SVC_SCREENS = [ "svc": "FRAUD_RULE_MCC", "desc": "관찰가맹 이상거래 룰: 카드의 당일 승인 중 상태가 WATCH 인 가맹점 건수를", "in": [ + "T_MERCHANT", + "T_AMOUNT", "T_KEY1", "T_BIZDATE" ], "out": [ - "T_COUNT", "T_RC", + "T_FEE", + "T_COUNT", "T_ID1" ] }, @@ -2978,12 +4244,15 @@ const SVC_SCREENS = [ "svc": "FRAUD_RULE_NIGHT", "desc": "심야 이상거래 룰: 최근 3일 심야시간대(23~06시) 승인 시도가 6건 이상이면", "in": [ + "T_MERCHANT", + "T_AMOUNT", "T_KEY1", "T_BIZDATE" ], "out": [ - "T_COUNT", "T_RC", + "T_FEE", + "T_COUNT", "T_ID1" ] }, @@ -2993,9 +4262,14 @@ const SVC_SCREENS = [ "svc": "FRAUD_SCORE_SUM", "desc": "카드 위험도 종합: 카드의 부정 적발 건수/점수합/최고점수를 집계해 최고점수", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", "T_ARG1", "T_ARG2" @@ -3009,11 +4283,13 @@ const SVC_SCREENS = [ "in": [ "T_KEY1", "T_MERCHANT", + "T_BIZDATE", "T_AMOUNT", - "T_INSTALL_N", - "T_BIZDATE" + "T_INSTALL_N" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT" ] }, @@ -3023,13 +4299,17 @@ const SVC_SCREENS = [ "svc": "KEY_ROTATE_LOG", "desc": "암호키 회전 로그: 요청 키버전(T_ARG1)의 차기 버전을 산출하고 키식별자", "in": [ - "T_ARG1", - "T_KEY1" + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", + "T_KEY1", + "T_ARG1" ], "out": [ + "T_RC", + "T_FEE", "T_ARG2", - "T_ARG3", - "T_RC" + "T_ARG3" ] }, { @@ -3038,11 +4318,15 @@ const SVC_SCREENS = [ "svc": "LIMIT_ADJUST", "desc": "한도 증감 조정: 요청 증감액(T_DELTA)을 일한도에 가감하되 음수 한도가 되지", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1", "T_DELTA" ], "out": [ "T_RC", + "T_FEE", "T_AMT1" ] }, @@ -3052,13 +4336,16 @@ const SVC_SCREENS = [ "svc": "LIMIT_CHK", "desc": "카드 한도 점검 (사용가능 여부만 리턴).", "in": [ + "T_MERCHANT", + "T_BIZDATE", "T_KEY1", "T_AMOUNT" ], "out": [ + "T_RC", + "T_FEE", "T_AMT1", - "T_AMT2", - "T_RC" + "T_AMT2" ] }, { @@ -3067,9 +4354,14 @@ const SVC_SCREENS = [ "svc": "LIMIT_CUMUL_INQ", "desc": "전(全)기간 누적 사용 조회: 카드의 전체 승인 이력에서 누적 건수/누적 합계/", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", "T_AMT1", "T_AMT2" @@ -3081,9 +4373,14 @@ const SVC_SCREENS = [ "svc": "LIMIT_DAILY_INQ", "desc": "일 한도 상세 조회: 원장에서 일한도/사용액/임시한도/상태를 한 번에 읽어", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_FEE", "T_AMT1", "T_AMT2", "T_AMT3", @@ -3096,10 +4393,15 @@ const SVC_SCREENS = [ "svc": "LIMIT_DEC", "desc": "한도 차감(사용금액 증가).", "in": [ - "T_AMOUNT", - "T_KEY1" + "T_MERCHANT", + "T_BIZDATE", + "T_KEY1", + "T_AMOUNT" ], - "out": [] + "out": [ + "T_RC", + "T_FEE" + ] }, { "mod": "au", @@ -3107,10 +4409,16 @@ const SVC_SCREENS = [ "svc": "LIMIT_EXHAUST_LST", "desc": "한도 소진 카드 목록: 요청 기준율(bps, 기본 9000=90%) 이상으로 한도를 소진한", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_ARG1" ], "out": [ + "T_RC", "T_COUNT", + "T_FEE", "T_ARG1" ] }, @@ -3120,10 +4428,14 @@ const SVC_SCREENS = [ "svc": "LIMIT_FREEZE", "desc": "한도 동결: ACTIVE 상태의 카드 원장만 FROZEN 으로 전환한다. 이미 다른", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -3132,11 +4444,16 @@ const SVC_SCREENS = [ "svc": "LIMIT_HIST", "desc": "한도 사용 이력조회: 최근 N일 일자별 승인합계를 커서로 훑어", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1", "T_ARG1" ], "out": [ + "T_RC", "T_COUNT", + "T_FEE", "T_GROSS", "T_AMT1" ] @@ -3147,12 +4464,22 @@ const SVC_SCREENS = [ "svc": "LIMIT_INQ", "desc": "한도 조회 (가용한도 계산).", "in": [ - "T_KEY1" + "T_KEY1", + "T_BIZDATE" ], "out": [ + "T_RC", "T_AMT1", "T_AMT2", - "T_AMT3" + "T_AMT3", + "T_ARG1", + "T_GROSS", + "T_COUNT", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ARG5", + "T_ARG6" ] }, { @@ -3161,13 +4488,16 @@ const SVC_SCREENS = [ "svc": "LIMIT_MONTH_CHK", "desc": "월 한도 점검: 이번 달 승인(A/M/S) 누적액에 요청금액을 더해 월 한도", "in": [ + "T_MERCHANT", + "T_BIZDATE", "T_KEY1", "T_AMOUNT" ], "out": [ + "T_RC", + "T_FEE", "T_AMT1", - "T_AMT2", - "T_RC" + "T_AMT2" ] }, { @@ -3176,9 +4506,14 @@ const SVC_SCREENS = [ "svc": "LIMIT_MONTH_INQ", "desc": "월 누적 사용 조회: 이번 달 승인건 기준으로 건수/합계/평균단가를 한 번의", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", "T_AMT1", "T_AMT2" @@ -3190,12 +4525,16 @@ const SVC_SCREENS = [ "svc": "LIMIT_OVER_ALERT", "desc": "한도 소진 임박 알림: 사용액/일한도를 bps 로 환산해 90% 이상이면 경보", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_FEE", "T_ARG1", - "T_AMT1", - "T_RC" + "T_AMT1" ] }, { @@ -3204,12 +4543,15 @@ const SVC_SCREENS = [ "svc": "LIMIT_PERTX_CHK", "desc": "건별(1회) 한도 점검: 일한도의 20% 를 건별 상한으로 삼아 요청 단건 금액이", "in": [ + "T_MERCHANT", + "T_BIZDATE", "T_KEY1", "T_AMOUNT" ], "out": [ - "T_AMT1", - "T_RC" + "T_RC", + "T_FEE", + "T_AMT1" ] }, { @@ -3218,12 +4560,16 @@ const SVC_SCREENS = [ "svc": "LIMIT_RAISE_REQ", "desc": "한도 영구상향 심사: 최근 30일 승인실적 합계와 거절 건수를 심사해", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1", "T_AMT1" ], "out": [ - "T_GROSS", "T_RC", + "T_FEE", + "T_GROSS", "T_AMT2" ] }, @@ -3233,12 +4579,16 @@ const SVC_SCREENS = [ "svc": "LIMIT_REMAIN", "desc": "잔여한도 조회: (기본한도 + 임시한도 - 사용액) 을 계산해 내려준다.", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_FEE", "T_AMT1", - "T_AMT2", - "T_RC" + "T_AMT2" ] }, { @@ -3247,11 +4597,15 @@ const SVC_SCREENS = [ "svc": "LIMIT_RESTORE", "desc": "임시한도 자동복원: LIMIT_TEMP 로 상향됐던 temp_limit 를 걷어내고", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ - "T_AMT1", - "T_RC" + "T_RC", + "T_FEE", + "T_AMT1" ] }, { @@ -3260,9 +4614,15 @@ const SVC_SCREENS = [ "svc": "LIMIT_RST", "desc": "한도 사용금액 초기화(일마감 원복).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], - "out": [] + "out": [ + "T_RC", + "T_FEE" + ] }, { "mod": "au", @@ -3270,10 +4630,16 @@ const SVC_SCREENS = [ "svc": "LIMIT_SET", "desc": "카드 한도 설정/등록 (dbio upsert).", "in": [ - "T_AMT1", - "T_KEY1" + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", + "T_KEY1", + "T_AMT1" ], - "out": [] + "out": [ + "T_RC", + "T_FEE" + ] }, { "mod": "au", @@ -3281,10 +4647,16 @@ const SVC_SCREENS = [ "svc": "LIMIT_TEMP", "desc": "임시 한도 상향 (temp_limit 저장 + 당일 상향).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1", "T_AMT1" ], - "out": [] + "out": [ + "T_RC", + "T_FEE" + ] }, { "mod": "au", @@ -3292,9 +4664,14 @@ const SVC_SCREENS = [ "svc": "LIMIT_TIER_INQ", "desc": "한도 등급 조회: 일한도 구간으로 BASIC/SILVER/GOLD/PLATINUM 등급을 매겨", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_FEE", "T_AMT1" ] }, @@ -3304,10 +4681,14 @@ const SVC_SCREENS = [ "svc": "LIMIT_UNFREEZE", "desc": "한도 동결 해제: FROZEN 상태의 카드 원장만 ACTIVE 로 되돌린다. 대상이", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -3316,9 +4697,14 @@ const SVC_SCREENS = [ "svc": "LIMIT_USAGE", "desc": "한도 사용률 조회: 사용액/기본한도를 bps(만분율)로 환산하고", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_FEE", "T_ARG1", "T_AMT1", "T_AMT2" @@ -3330,9 +4716,15 @@ const SVC_SCREENS = [ "svc": "MCC_AUTH_STAT", "desc": "업종별 매입 금액 통계: 영업일 매입을 업종(category)별 합계로 그룹핑하는", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ + "T_RC", + "T_COUNT", + "T_FEE", "T_GROSS" ] }, @@ -3342,12 +4734,17 @@ const SVC_SCREENS = [ "svc": "MCC_POLICY", "desc": "업종(MCC)별 승인 정책: 매입 원장의 당일 업종 실적(건수/합계)을 읽어", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_CATEGORY" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", - "T_GROSS", - "T_RC" + "T_GROSS" ] }, { @@ -3356,14 +4753,17 @@ const SVC_SCREENS = [ "svc": "MERCH_AUTH_RANK", "desc": "가맹점 승인 순위: 영업일의 가맹점별 승인금액을 rank() 윈도우로 서열화한", "in": [ + "T_KEY1", + "T_AMOUNT", "T_MERCHANT", "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", + "T_FEE", "T_ARG1", - "T_GROSS", - "T_RC" + "T_GROSS" ] }, { @@ -3372,13 +4772,16 @@ const SVC_SCREENS = [ "svc": "NIGHT_POLICY", "desc": "심야 승인정책 점검: 최근 7일간 심야시간대(23시~06시) 승인 이력을 집계해", "in": [ + "T_MERCHANT", + "T_BIZDATE", "T_KEY1", "T_AMOUNT" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", - "T_GROSS", - "T_RC" + "T_GROSS" ] }, { @@ -3387,13 +4790,16 @@ const SVC_SCREENS = [ "svc": "NOAUTH_CAPTURE", "desc": "무승인 매입 검출: 매입원장(purchase)에는 있는데 같은 가맹점/영업일/금액의", "in": [ + "T_KEY1", + "T_AMOUNT", "T_MERCHANT", "T_BIZDATE" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", - "T_GROSS", - "T_RC" + "T_GROSS" ] }, { @@ -3410,6 +4816,7 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_FEE", "T_ID1" ] }, @@ -3425,6 +4832,8 @@ const SVC_SCREENS = [ "T_AMOUNT" ], "out": [ + "T_RC", + "T_FEE", "T_ID2" ] }, @@ -3434,10 +4843,15 @@ const SVC_SCREENS = [ "svc": "PREAUTH_CANCEL", "desc": "선승인 취소: 미확정(P) 선승인을 X 로 전환해 홀드 금액을 해제한다.", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_ID2" ], "out": [ "T_RC", + "T_FEE", "T_AMT1" ] }, @@ -3447,10 +4861,15 @@ const SVC_SCREENS = [ "svc": "PREAUTH_COMPLETE", "desc": "예비승인 확정: preauth 소진 + 본승인 생성.", "in": [ - "T_ID2", - "T_BIZDATE" + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", + "T_BIZDATE", + "T_ID2" ], "out": [ + "T_RC", + "T_FEE", "T_ID1" ] }, @@ -3460,11 +4879,16 @@ const SVC_SCREENS = [ "svc": "PREAUTH_EXTEND", "desc": "선승인 연장: 아직 확정되지 않은(P) 선승인의 기준일(biz_date)을 지정 일수만큼", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_ID2", "T_ARG1" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -3473,14 +4897,19 @@ const SVC_SCREENS = [ "svc": "PREAUTH_INQ", "desc": "선승인 조회: 카드의 선승인 현황을 상태(P/C/X)별 FILTER 집계로 나눠 주고", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_COUNT", + "T_FEE", "T_ARG1", "T_ARG2", "T_ARG3", - "T_AMT1", - "T_RC" + "T_AMT1" ] }, { @@ -3489,11 +4918,15 @@ const SVC_SCREENS = [ "svc": "PREAUTH_PARTUSE", "desc": "사전승인 부분 사용: 예비승인(P) 잔액에서 사용액을 차감(부분 소진)한다.", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", "T_ID2", "T_AMOUNT" ], "out": [ "T_RC", + "T_FEE", "T_AMT1" ] }, @@ -3503,9 +4936,14 @@ const SVC_SCREENS = [ "svc": "RISK_SCORE_INQ", "desc": "카드 리스크 점수 산출: 부정거래 적발 이력(평균점수/건수), 30일 거절률,", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_FEE", "T_ARG1", "T_ARG2", "T_ARG3" @@ -3519,11 +4957,12 @@ const SVC_SCREENS = [ "in": [ "T_KEY1", "T_MERCHANT", - "T_AMOUNT", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT" ], "out": [ "T_RC", + "T_FEE", "T_ID1" ] }, @@ -3533,14 +4972,19 @@ const SVC_SCREENS = [ "svc": "STANDIN_INQ", "desc": "대행승인 현황 조회: 영업일의 STANDIN 승인 건을 미정산(A)/정산완료(S)로", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ + "T_RC", + "T_COUNT", + "T_FEE", "T_ARG1", "T_ARG2", "T_AMT1", - "T_AMT2", - "T_RC" + "T_AMT2" ] }, { @@ -3549,13 +4993,16 @@ const SVC_SCREENS = [ "svc": "STANDIN_LIMIT", "desc": "대행 한도 점검: 발급사 미응답 시 대행 승인은 일한도의 절반을 상한으로 두어,", "in": [ + "T_MERCHANT", + "T_BIZDATE", "T_KEY1", "T_AMOUNT" ], "out": [ + "T_RC", + "T_FEE", "T_AMT1", - "T_AMT2", - "T_RC" + "T_AMT2" ] }, { @@ -3564,13 +5011,17 @@ const SVC_SCREENS = [ "svc": "STANDIN_RECON", "desc": "대행 정산 대사: 영업일 대행 승인 중 매입확정(captured_at NOT NULL) 건과", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", + "T_FEE", "T_ARG1", - "T_ARG2", - "T_RC" + "T_ARG2" ] }, { @@ -3579,12 +5030,16 @@ const SVC_SCREENS = [ "svc": "STANDIN_SETTLE", "desc": "대행승인(stand-in) 정산: 이슈어 복구 후, 대행으로 승인(A)해 둔 STANDIN", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", - "T_GROSS", - "T_RC" + "T_GROSS" ] }, { @@ -3593,10 +5048,15 @@ const SVC_SCREENS = [ "svc": "STANDIN_STAT", "desc": "대행(stand-in) 승인 통계: 영업일 auth_type='STANDIN' 승인을 대상으로", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", + "T_FEE", "T_GROSS", "T_ARG1" ] @@ -3607,10 +5067,15 @@ const SVC_SCREENS = [ "svc": "STAT_AMT_BAND", "desc": "금액대별 승인 통계: 영업일 승인을 1만/5만/10만/50만/그이상 구간(CASE)으로", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", + "T_FEE", "T_ARG1" ] }, @@ -3620,9 +5085,14 @@ const SVC_SCREENS = [ "svc": "STAT_APPROVE_TR", "desc": "카드 승인 추세: 최근 7일과 그 직전 7일의 승인 건수를 각각 필터 집계로 뽑아", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_FEE", "T_ARG1", "T_ARG2", "T_DELTA" @@ -3634,16 +5104,21 @@ const SVC_SCREENS = [ "svc": "STAT_BYTYPE", "desc": "승인유형(채널)별 통계: 영업일 승인 접수를 auth_type", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ + "T_RC", + "T_COUNT", + "T_FEE", "T_ARG1", "T_ARG2", "T_ARG3", "T_ARG4", "T_ARG5", - "T_ARG6", - "T_COUNT" + "T_ARG6" ] }, { @@ -3652,9 +5127,14 @@ const SVC_SCREENS = [ "svc": "STAT_DAILY_SUM", "desc": "일별 승인 종합: 영업일 승인 원장의 접수/승인/거절/취소 건수와 승인 합계를", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", "T_ARG1", "T_ARG2", @@ -3668,10 +5148,15 @@ const SVC_SCREENS = [ "svc": "STAT_DECLINE", "desc": "거절사유별 통계: 영업일 거절(D) 건을 사유별로 그룹핑하는 커서를 순회하며", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", + "T_FEE", "T_ARG1" ] }, @@ -3681,11 +5166,16 @@ const SVC_SCREENS = [ "svc": "STAT_HOURLY", "desc": "시간대별 승인 통계: 영업일의 승인 접수를 시(HH24) 단위로 그룹핑한 커서를", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ - "T_ARG1", + "T_RC", "T_COUNT", + "T_FEE", + "T_ARG1", "T_ARG2" ] }, @@ -3695,11 +5185,16 @@ const SVC_SCREENS = [ "svc": "STAT_ISSUER", "desc": "발급사(카드망)별 승인 통계: 카드번호 선두 자리로 발급사망을 판별", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ - "T_ARG1", - "T_COUNT" + "T_RC", + "T_COUNT", + "T_FEE", + "T_ARG1" ] }, { @@ -3708,10 +5203,14 @@ const SVC_SCREENS = [ "svc": "TOKEN_INQ", "desc": "토큰 상태 조회: 원장 상태와 블랙리스트 등재 여부를 합쳐 토큰의 종합 상태", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -3720,10 +5219,14 @@ const SVC_SCREENS = [ "svc": "TOKEN_ISSUE", "desc": "결제 토큰 발급: PAN 의 Luhn 검증 후 앞6·뒤4 자리만 남기고 가운데를 마스킹한", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -3732,10 +5235,14 @@ const SVC_SCREENS = [ "svc": "TOKEN_REVOKE", "desc": "토큰 폐기: 카드(토큰 원본 PAN)를 TOKEN_REVOKED 사유로 블랙리스트에 등재해", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -3744,10 +5251,14 @@ const SVC_SCREENS = [ "svc": "TOKEN_VERIFY", "desc": "토큰 검증: 결제 토큰(PAN)을 4단계로 검증해 비트마스크 RC 로 응답한다.", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -3756,12 +5267,15 @@ const SVC_SCREENS = [ "svc": "VELOCITY_AMT", "desc": "금액 velocity 룰: 직전 1시간 승인 합계에 요청금액을 더해 200만원을 넘으면", "in": [ + "T_MERCHANT", + "T_BIZDATE", "T_KEY1", "T_AMOUNT" ], "out": [ - "T_GROSS", - "T_RC" + "T_RC", + "T_FEE", + "T_GROSS" ] }, { @@ -3770,9 +5284,14 @@ const SVC_SCREENS = [ "svc": "VELOCITY_CHAN", "desc": "승인유형 velocity 룰: 직전 1시간 카드 승인을 승인유형(auth_type)별로", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_FEE", "T_ARG1", "T_COUNT" ] @@ -3783,13 +5302,16 @@ const SVC_SCREENS = [ "svc": "VELOCITY_CHK", "desc": "속도(velocity) 점검: 당일 카드 승인건수/합계.", "in": [ + "T_MERCHANT", + "T_AMOUNT", "T_KEY1", "T_BIZDATE" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", - "T_GROSS", - "T_RC" + "T_GROSS" ] }, { @@ -3798,12 +5320,16 @@ const SVC_SCREENS = [ "svc": "VELOCITY_DAY", "desc": "일 velocity 룰: 당일 카드 승인 건수와 합계를 세어 20건 초과 또는 합계", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", - "T_GROSS", - "T_RC" + "T_GROSS" ] }, { @@ -3812,12 +5338,16 @@ const SVC_SCREENS = [ "svc": "VELOCITY_HOUR", "desc": "시간 단위 velocity 점검: 직전 60분 슬라이딩 윈도우의 카드 승인", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", - "T_GROSS", - "T_RC" + "T_GROSS" ] }, { @@ -3826,11 +5356,15 @@ const SVC_SCREENS = [ "svc": "VELOCITY_MCC", "desc": "가맹점 분산 velocity 룰: 직전 1시간 동안 서로 다른 가맹점 수(카드가 돌린", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_FEE", + "T_COUNT" ] }, { @@ -3839,12 +5373,16 @@ const SVC_SCREENS = [ "svc": "VELOCITY_MERCH", "desc": "가맹점 velocity 점검: 직전 10분간 가맹점 승인 요청 폭주를 감시한다.", "in": [ + "T_KEY1", + "T_BIZDATE", + "T_AMOUNT", "T_MERCHANT" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", - "T_ARG1", - "T_RC" + "T_ARG1" ] }, { @@ -3853,12 +5391,15 @@ const SVC_SCREENS = [ "svc": "VELOCITY_RESET", "desc": "velocity 적발 리셋: 특정 카드의 당일 velocity 계열 부정 적발(reason 접두", "in": [ + "T_MERCHANT", + "T_AMOUNT", "T_KEY1", "T_BIZDATE" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_FEE", + "T_COUNT" ] }, { @@ -3867,13 +5408,17 @@ const SVC_SCREENS = [ "svc": "VELOCITY_WEEK", "desc": "주간 velocity 점검: 최근 7일 카드 승인 건수/금액에 더해 방문 가맹점의", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", "T_GROSS", - "T_ARG1", - "T_RC" + "T_ARG1" ] }, { @@ -3882,10 +5427,14 @@ const SVC_SCREENS = [ "svc": "WHITELIST_ADD", "desc": "화이트리스트 등록: 카드의 블랙리스트 등재를 삭제하고 원장 상태가 ACTIVE 가", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -3894,11 +5443,15 @@ const SVC_SCREENS = [ "svc": "WHITELIST_CHK", "desc": "화이트 여부 판정: 블랙리스트 미등재이면서 원장이 존재하는 카드를 화이트", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ - "T_ARG1", - "T_RC" + "T_RC", + "T_FEE", + "T_ARG1" ] }, { @@ -3907,11 +5460,15 @@ const SVC_SCREENS = [ "svc": "WHITELIST_DEL", "desc": "화이트리스트 해제: 카드를 다시 블랙리스트에 (사유와 함께) 등재/갱신해", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1", "T_REASON" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -3920,10 +5477,17 @@ const SVC_SCREENS = [ "svc": "CL_AGGREGATE", "desc": "마감 총계 재집계 후 close_log 반영.", "in": [ - "T_ID1", - "T_BIZDATE" + "T_BIZDATE", + "T_STR1", + "T_KEY1", + "T_ID1" ], "out": [ + "T_RC", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", "T_GROSS" ] @@ -3937,8 +5501,12 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_RC" + "T_GROSS" ] }, { @@ -3950,11 +5518,16 @@ const SVC_SCREENS = [ "T_ID1" ], "out": [ + "T_RC", "T_AMT1", "T_AMT2", "T_AMT3", "T_DELTA", - "T_RC" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -3967,8 +5540,12 @@ const SVC_SCREENS = [ "T_STR1" ], "out": [ - "T_ID1", - "T_RC" + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -3977,11 +5554,16 @@ const SVC_SCREENS = [ "svc": "CL_APPRFINAL", "desc": "최종 마감 승인: 2차 승인(APPR:L2) 마감을 최종 승인(status=A)으로 확정.", "in": [ - "T_ID1" + "T_ID1", + "T_STR1" ], "out": [ - "T_ID1", - "T_ARG1" + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -3990,13 +5572,17 @@ const SVC_SCREENS = [ "svc": "CL_APPRHIST", "desc": "마감 승인 이력: 기간 프리픽스(YYYY-MM 등)의 마감을 승인(A/L)/완료(C)/취소(X)", "in": [ + "T_KEY1", "T_STR1" ], "out": [ - "T_ARG1", - "T_ARG2", + "T_RC", + "T_COUNT", "T_ARG3", - "T_COUNT" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_GROSS" ] }, { @@ -4005,11 +5591,16 @@ const SVC_SCREENS = [ "svc": "CL_APPRL1", "desc": "1차 마감 승인: 개시(O)/완료(C) 마감을 1차 승인(memo=APPR:L1) 단계로 전이.", "in": [ - "T_ID1" + "T_ID1", + "T_STR1" ], "out": [ - "T_ID1", - "T_ARG1" + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4018,11 +5609,16 @@ const SVC_SCREENS = [ "svc": "CL_APPRL2", "desc": "2차 마감 승인: 1차 승인(APPR:L1) 마감을 2차 승인(APPR:L2)으로 전이.", "in": [ - "T_ID1" + "T_ID1", + "T_STR1" ], "out": [ - "T_ID1", - "T_ARG1" + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4031,9 +5627,17 @@ const SVC_SCREENS = [ "svc": "CL_APPROVE", "desc": "마감 승인: 상태 A.", "in": [ - "T_ID1" + "T_ID1", + "T_STR1" ], - "out": [] + "out": [ + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" + ] }, { "mod": "cl", @@ -4042,10 +5646,15 @@ const SVC_SCREENS = [ "desc": "마감 승인 반려: 승인(A) 상태 마감을 완료(C)로 되돌리며 반려 사유를 기록.", "in": [ "T_ID1", - "T_REASON" + "T_STR1" ], "out": [ - "T_RC" + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4054,11 +5663,16 @@ const SVC_SCREENS = [ "svc": "CL_APPRREQ", "desc": "마감 승인 요청: C 상태이며 집계건수>0 인 마감에 승인요청(APPR_REQ) 표시를", "in": [ - "T_ID1" + "T_ID1", + "T_STR1" ], "out": [ - "T_ARG1", - "T_RC" + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4068,10 +5682,13 @@ const SVC_SCREENS = [ "desc": "마감 승인 현황: 승인 단계별(L1/L2/최종A) 마감 건수를 집계한다.", "in": [], "out": [ - "T_ARG1", - "T_ARG2", + "T_RC", + "T_COUNT", "T_ARG3", - "T_COUNT" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_GROSS" ] }, { @@ -4080,10 +5697,16 @@ const SVC_SCREENS = [ "svc": "CL_APPRWD", "desc": "마감 승인 회수: 승인(A) 마감을 완료(C) 상태로 되돌리고 승인 이력 초기화.", "in": [ - "T_ID1" + "T_ID1", + "T_STR1" ], "out": [ - "T_ID1" + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4092,10 +5715,19 @@ const SVC_SCREENS = [ "svc": "CL_ARCHIVE", "desc": "마감 아카이브: 승인/완료 마감을 보관 상태로(memo 표시).", "in": [ + "T_BIZDATE", + "T_STR1", + "T_KEY1", "T_ID1" ], "out": [ - "T_COUNT" + "T_RC", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4104,11 +5736,21 @@ const SVC_SCREENS = [ "svc": "CL_AUDITTRAIL", "desc": "마감 이력 감사: 마감건의 생성-수정 시각 간격(초)을 산출하고, 잠금(L)", "in": [ + "T_BIZDATE", + "T_STR1", + "T_KEY1", "T_ID1" ], "out": [ + "T_RC", "T_ARG1", - "T_RC" + "T_ARG2", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4117,12 +5759,21 @@ const SVC_SCREENS = [ "svc": "CL_BALVERIFY", "desc": "잔액 검증: 스냅샷 net 합 vs close_log total 비교.", "in": [ - "T_ID1" + "T_ID1", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG1", + "T_ARG2", + "T_AMT1", "T_AMT2", - "T_AMT3", - "T_DELTA" + "T_DELTA", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4131,10 +5782,19 @@ const SVC_SCREENS = [ "svc": "CL_CALENDAR", "desc": "마감 캘린더: 월(YYYY-MM)의 1~31일 각 일자 마감 상태를 한 글자씩 표현한", "in": [ - "T_STR1" + "T_BIZDATE", + "T_STR1", + "T_KEY1", + "T_ID1" ], "out": [ - "T_COUNT" + "T_RC", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4143,9 +5803,18 @@ const SVC_SCREENS = [ "svc": "CL_CANCEL", "desc": "마감 취소: 상태 X.", "in": [ - "T_ID1" + "T_ID1", + "T_REASON", + "T_BIZDATE" ], - "out": [] + "out": [ + "T_RC", + "T_COUNT", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_GROSS" + ] }, { "mod": "cl", @@ -4156,12 +5825,12 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_ARG1", - "T_ARG2", - "T_ARG3", - "T_ARG4", + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_RC" + "T_GROSS" ] }, { @@ -4173,8 +5842,12 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_RC" + "T_GROSS" ] }, { @@ -4186,9 +5859,12 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_GROSS", - "T_RC" + "T_GROSS" ] }, { @@ -4200,8 +5876,12 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_RC" + "T_GROSS" ] }, { @@ -4213,8 +5893,12 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_RC" + "T_GROSS" ] }, { @@ -4224,10 +5908,17 @@ const SVC_SCREENS = [ "desc": "채널별 마감: 채널(POS/EDC/EDI/FOREIGN) 단위로 당일 매입을 집계하고", "in": [ "T_BIZDATE", + "T_STR1", "T_CHANNEL" ], "out": [ + "T_RC", "T_ID1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", "T_GROSS" ] @@ -4238,14 +5929,21 @@ const SVC_SCREENS = [ "svc": "CL_CHNVERIFY", "desc": "채널별 마감 검증: 영업일+채널의 매입건수와 해당 매입에 연결된 ledger", "in": [ - "T_BIZDATE", - "T_CHANNEL" + "T_ID1", + "T_BIZDATE" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", + "T_AMT1", + "T_AMT2", "T_DELTA", - "T_RC" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4254,13 +5952,19 @@ const SVC_SCREENS = [ "svc": "CL_CLOSERATE", "desc": "마감율 산출: 월 프리픽스(YYYY-MM)에 대해 매입이 발생한 영업일수 대비", "in": [ + "T_BIZDATE", "T_STR1" ], "out": [ - "T_ARG1", - "T_ARG2", - "T_ARG3", - "T_RC" + "T_RC", + "T_ID1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4269,13 +5973,21 @@ const SVC_SCREENS = [ "svc": "CL_CNTVERIFY", "desc": "건수 정합검증: 마감이력 txn_count 와 해당 영업일 매입 실건수를 대사한다.", "in": [ - "T_ID1" + "T_ID1", + "T_BIZDATE" ], "out": [ + "T_RC", "T_ARG1", - "T_COUNT", + "T_ARG2", + "T_AMT1", + "T_AMT2", "T_DELTA", - "T_RC" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4287,10 +5999,16 @@ const SVC_SCREENS = [ "T_ID1" ], "out": [ - "T_ARG1", - "T_ARG2", - "T_ARG3", - "T_RC" + "T_RC", + "T_AMT1", + "T_AMT2", + "T_AMT3", + "T_DELTA", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4299,9 +6017,17 @@ const SVC_SCREENS = [ "svc": "CL_CONFIRM", "desc": "마감 확정: 상태 C.", "in": [ - "T_ID1" + "T_ID1", + "T_STR1" ], - "out": [] + "out": [ + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" + ] }, { "mod": "cl", @@ -4309,11 +6035,20 @@ const SVC_SCREENS = [ "svc": "CL_CORPCLOSE", "desc": "법인별 마감: 당일 매입을 발급사(issuer) 법인 단위로 그룹 커서 순회하며", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1", + "T_ISSUER" ], "out": [ + "T_RC", + "T_ID1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_ID1" + "T_GROSS" ] }, { @@ -4322,14 +6057,21 @@ const SVC_SCREENS = [ "svc": "CL_CORPVERIFY", "desc": "법인별 마감 검증: 마감이력 헤더 건수/금액과 스냅샷 집계 정합 점검.", "in": [ - "T_ID1" + "T_ID1", + "T_BIZDATE" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", "T_AMT1", "T_AMT2", - "T_RC" + "T_DELTA", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4338,11 +6080,17 @@ const SVC_SCREENS = [ "svc": "CL_CXLDUP", "desc": "중복마감 취소: 동일 기간 중복 마감이력 중 최신을 남기고 취소(status=X) 처리.", "in": [ - "T_ID1" + "T_ID1", + "T_REASON", + "T_BIZDATE" ], "out": [ "T_RC", - "T_COUNT" + "T_COUNT", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_GROSS" ] }, { @@ -4351,11 +6099,17 @@ const SVC_SCREENS = [ "svc": "CL_CXLERROR", "desc": "오류정정 취소: 오류정정 사유로 마감을 취소(status=X)한다.", "in": [ - "T_ID1" + "T_ID1", + "T_REASON", + "T_BIZDATE" ], "out": [ - "T_ID1", - "T_RC" + "T_RC", + "T_COUNT", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_GROSS" ] }, { @@ -4364,11 +6118,17 @@ const SVC_SCREENS = [ "svc": "CL_CXLLATE", "desc": "지연자료 취소: 지연자료 반영 사유로 마감을 취소(status=X)한다.", "in": [ - "T_ID1" + "T_ID1", + "T_REASON", + "T_BIZDATE" ], "out": [ - "T_ID1", - "T_RC" + "T_RC", + "T_COUNT", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_GROSS" ] }, { @@ -4376,11 +6136,15 @@ const SVC_SCREENS = [ "modnm": "마감", "svc": "CL_CXLREASON", "desc": "마감 취소 사유별 집계: 기간 프리픽스의 취소(X) 마감을 memo(사유)별로 그룹", - "in": [ - "T_STR1" - ], + "in": [], "out": [ - "T_COUNT" + "T_RC", + "T_COUNT", + "T_ARG3", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_GROSS" ] }, { @@ -4390,11 +6154,16 @@ const SVC_SCREENS = [ "desc": "마감 취소 요청: 취소 사유(T_REASON)를 memo(CXLREQ:..)에 기록해 요청 표기(상태 유지).", "in": [ "T_ID1", - "T_REASON" + "T_REASON", + "T_BIZDATE" ], "out": [ - "T_ID1", - "T_RC" + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4404,10 +6173,13 @@ const SVC_SCREENS = [ "desc": "마감 취소 현황: 취소 사유별(오류정정/지연자료/중복마감) 취소 건수를 집계한다.", "in": [], "out": [ - "T_ARG1", - "T_ARG2", + "T_RC", + "T_COUNT", "T_ARG3", - "T_COUNT" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_GROSS" ] }, { @@ -4419,7 +6191,11 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG6", + "T_ARG7", + "T_ARG8" ] }, { @@ -4428,15 +6204,21 @@ const SVC_SCREENS = [ "svc": "CL_DAYVERIFY", "desc": "일 마감 검증: 당일 마감이력 집계와 매입 실집계 대사.", "in": [ + "T_ID1", "T_BIZDATE" ], "out": [ - "T_COUNT", + "T_RC", "T_ARG1", + "T_ARG2", "T_AMT1", "T_AMT2", "T_DELTA", - "T_RC" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4444,10 +6226,20 @@ const SVC_SCREENS = [ "modnm": "마감", "svc": "CL_DELAYLIST", "desc": "지연 마감 목록: 생성 후 24시간이 지나도록 개시(O) 상태로 남은 마감을", - "in": [], + "in": [ + "T_BIZDATE", + "T_STR1", + "T_KEY1", + "T_ID1" + ], "out": [ + "T_RC", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_RC" + "T_GROSS" ] }, { @@ -4456,11 +6248,20 @@ const SVC_SCREENS = [ "svc": "CL_DELAYREG", "desc": "마감 지연 사유 등록: 개시(O) 상태의 마감건에만 지연 사유를 memo 로", "in": [ + "T_BIZDATE", + "T_STR1", + "T_KEY1", "T_ID1", "T_REASON" ], "out": [ - "T_RC" + "T_RC", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4469,11 +6270,16 @@ const SVC_SCREENS = [ "svc": "CL_DIFFCHK", "desc": "차액 점검: close_log total vs 스냅샷 gross 합.", "in": [ + "T_BIZDATE", "T_ID1" ], "out": [ - "T_DELTA", - "T_RC" + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4483,12 +6289,19 @@ const SVC_SCREENS = [ "desc": "부문별 마감: 업종(category) 부문 단위로 당일 매입 건수/정산액(net)을 집계하고", "in": [ "T_BIZDATE", + "T_STR1", "T_CATEGORY" ], "out": [ + "T_RC", "T_ID1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_NET" + "T_GROSS" ] }, { @@ -4497,11 +6310,19 @@ const SVC_SCREENS = [ "svc": "CL_DUELIST", "desc": "마감 예정 목록: 기준일 이전 14일 내 영업일 중 DAILY 마감이 없는 미마감 대상일을 나열.", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1", + "T_KEY1", + "T_ID1" ], "out": [ + "T_RC", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_RC" + "T_GROSS" ] }, { @@ -4509,11 +6330,16 @@ const SVC_SCREENS = [ "modnm": "마감", "svc": "CL_DUPCHK", "desc": "중복 마감 탐지: 취소(X) 제외 동일 (close_type, period_key) 에 마감이 2건", - "in": [], + "in": [ + "T_BIZDATE" + ], "out": [ + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_ARG1", - "T_RC" + "T_GROSS" ] }, { @@ -4521,13 +6347,15 @@ const SVC_SCREENS = [ "modnm": "마감", "svc": "CL_ESCALATE", "desc": "승인 지연 에스컬레이션: 기준일수(T_ARG1, 기본 3일) 초과 승인 대기(C)", - "in": [ - "T_ARG1" - ], + "in": [], "out": [ + "T_RC", "T_COUNT", - "T_ARG2", - "T_RC" + "T_ARG3", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_GROSS" ] }, { @@ -4539,10 +6367,16 @@ const SVC_SCREENS = [ "T_ID1" ], "out": [ + "T_RC", "T_AMT1", "T_AMT2", + "T_AMT3", "T_DELTA", - "T_RC" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4551,14 +6385,21 @@ const SVC_SCREENS = [ "svc": "CL_FEEVERIFY", "desc": "수수료 검증: 마감 스냅샷의 fee 합계와 해당 영업일 매입 fee 합계에", "in": [ - "T_ID1" + "T_ID1", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG1", + "T_ARG2", "T_AMT1", "T_AMT2", - "T_AMT3", "T_DELTA", - "T_RC" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4567,12 +6408,19 @@ const SVC_SCREENS = [ "svc": "CL_FINALIZE", "desc": "본마감: 해당 영업일의 가마감(PROV, status O)을 찾아 매입을 재집계한 뒤", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1" ], "out": [ + "T_RC", "T_ID1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_DELTA" + "T_GROSS" ] }, { @@ -4581,12 +6429,19 @@ const SVC_SCREENS = [ "svc": "CL_FORCECLOSE", "desc": "강제 마감: 체크리스트를 우회해 해당 영업일의 미확정 개시(O) 마감을 제거한", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1" ], "out": [ + "T_RC", "T_ID1", - "T_ARG1", - "T_COUNT" + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4595,14 +6450,19 @@ const SVC_SCREENS = [ "svc": "CL_FXCLOSE", "desc": "해외(FOREIGN) 채널 마감: 해외 매입 집계 + ac_fx 원화환산 합계 후 FX 마감.", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1" ], "out": [ + "T_RC", "T_ID1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_GROSS", - "T_AMT1", - "T_RC" + "T_GROSS" ] }, { @@ -4611,11 +6471,19 @@ const SVC_SCREENS = [ "svc": "CL_GAPDAYS", "desc": "마감 누락일 탐지: 월 프리픽스 내 매입은 존재하나 일마감이 없는 일자를", "in": [ - "T_STR1" + "T_BIZDATE", + "T_STR1", + "T_KEY1", + "T_ID1" ], "out": [ + "T_RC", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_RC" + "T_GROSS" ] }, { @@ -4624,11 +6492,17 @@ const SVC_SCREENS = [ "svc": "CL_HALFCLOSE", "desc": "반기 결산 마감: 연도(T_STR1=YYYY)와 반기(T_ARG1=1|2)를 받아 상/하반기", "in": [ - "T_STR1", - "T_ARG1" + "T_BIZDATE", + "T_STR1" ], "out": [ + "T_RC", "T_ID1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", "T_GROSS" ] @@ -4642,8 +6516,12 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_ARG1", - "T_RC" + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4652,11 +6530,20 @@ const SVC_SCREENS = [ "svc": "CL_HOLIDAY", "desc": "휴일 마감 정책: 영업일의 요일을 판정해 주말이면 마감 SKIP(RC=1) 정책을,", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1", + "T_KEY1", + "T_ID1" ], "out": [ "T_RC", - "T_COUNT" + "T_ARG1", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4665,11 +6552,17 @@ const SVC_SCREENS = [ "svc": "CL_HYCLOSE", "desc": "반기 마감: 상/하반기 6개월 매입 집계 후 HALFYR 마감이력 생성.", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1" ], "out": [ + "T_RC", "T_ID1", - "T_ARG1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", "T_GROSS" ] @@ -4680,14 +6573,21 @@ const SVC_SCREENS = [ "svc": "CL_HYVERIFY", "desc": "반기 마감 검증: 반기 마감이력과 반기 매입 실집계 대사.", "in": [ + "T_ID1", "T_BIZDATE" ], "out": [ + "T_RC", "T_ARG1", + "T_ARG2", "T_AMT1", "T_AMT2", "T_DELTA", - "T_RC" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4695,11 +6595,14 @@ const SVC_SCREENS = [ "modnm": "마감", "svc": "CL_INQUIRY", "desc": "마감 조회: close_type/기간/상태/총건수 반환.", - "in": [ - "T_ID1" - ], + "in": [], "out": [ + "T_RC", "T_COUNT", + "T_ARG3", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_GROSS" ] }, @@ -4710,13 +6613,19 @@ const SVC_SCREENS = [ "desc": "발급사별 마감: 발급사(issuer) 단위 매입 집계 후 ISS 마감.", "in": [ "T_BIZDATE", + "T_STR1", "T_ISSUER" ], "out": [ + "T_RC", "T_ID1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_GROSS", - "T_FEE" + "T_GROSS" ] }, { @@ -4729,7 +6638,14 @@ const SVC_SCREENS = [ "T_STR1", "T_BIZDATE" ], - "out": [] + "out": [ + "T_RC", + "T_COUNT", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_GROSS" + ] }, { "mod": "cl", @@ -4738,8 +6654,13 @@ const SVC_SCREENS = [ "desc": "잠금 감사: 잠금(locked=true) 기간 중 승인(A) 또는 잠금(L) 마감이 하나도", "in": [], "out": [ + "T_RC", "T_COUNT", - "T_RC" + "T_ARG3", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_GROSS" ] }, { @@ -4748,12 +6669,17 @@ const SVC_SCREENS = [ "svc": "CL_LOCKSWEEP", "desc": "기간 잠금 일괄: 프리픽스 구간의 기존 잠금 레코드를 locked=true 로 재잠금하고", "in": [ - "T_STR1" + "T_ID1", + "T_STR1", + "T_BIZDATE" ], "out": [ - "T_ARG1", - "T_ARG2", - "T_COUNT" + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4762,10 +6688,17 @@ const SVC_SCREENS = [ "svc": "CL_MAINCLOSE", "desc": "본마감: 가마감(O) 이력을 당일 전체 매입으로 재집계해 확정(C)으로 승격.", "in": [ - "T_ID1" + "T_BIZDATE", + "T_STR1" ], "out": [ + "T_RC", "T_ID1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", "T_GROSS" ] @@ -4776,12 +6709,19 @@ const SVC_SCREENS = [ "svc": "CL_MEMOSEARCH", "desc": "마감 사유 검색: memo 에 키워드(T_STR1)가 포함된 마감을 검색해 총 건수와", "in": [ - "T_STR1" + "T_BIZDATE", + "T_STR1", + "T_KEY1", + "T_ID1" ], "out": [ - "T_COUNT", "T_RC", - "T_ID1" + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4791,14 +6731,19 @@ const SVC_SCREENS = [ "desc": "가맹점별 마감: 단일 가맹점의 당일 매입(gross/fee/net/건수)을 집계해 해당", "in": [ "T_BIZDATE", + "T_STR1", "T_MERCHANT" ], "out": [ + "T_RC", "T_ID1", - "T_ID2", - "T_GROSS", + "T_FEE", "T_NET", - "T_COUNT" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4810,9 +6755,16 @@ const SVC_SCREENS = [ "T_ID1" ], "out": [ + "T_RC", + "T_AMT1", + "T_AMT2", + "T_AMT3", + "T_DELTA", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_ARG1", - "T_RC" + "T_GROSS" ] }, { @@ -4821,11 +6773,19 @@ const SVC_SCREENS = [ "svc": "CL_MIDCLOSE", "desc": "중간 마감: 당일 중간 스냅 마감이력(MID) 생성.", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1" ], "out": [ + "T_RC", "T_ID1", - "T_COUNT" + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4838,7 +6798,15 @@ const SVC_SCREENS = [ "T_STR1" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4847,15 +6815,21 @@ const SVC_SCREENS = [ "svc": "CL_MONVERIFY", "desc": "월 마감 검증: 월 마감이력과 월 매입 실집계 대사.", "in": [ + "T_ID1", "T_BIZDATE" ], "out": [ + "T_RC", "T_ARG1", - "T_COUNT", + "T_ARG2", "T_AMT1", "T_AMT2", "T_DELTA", - "T_RC" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4867,10 +6841,16 @@ const SVC_SCREENS = [ "T_ID1" ], "out": [ + "T_RC", "T_AMT1", "T_AMT2", + "T_AMT3", "T_DELTA", - "T_RC" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4879,12 +6859,21 @@ const SVC_SCREENS = [ "svc": "CL_NETVERIFY", "desc": "정산액 검증: 마감 스냅샷에서 gross - fee <> net 인 내부 모순 행을 찾아", "in": [ - "T_ID1" + "T_ID1", + "T_BIZDATE" ], "out": [ - "T_COUNT", "T_RC", - "T_DELTA" + "T_ARG1", + "T_ARG2", + "T_AMT1", + "T_AMT2", + "T_DELTA", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4893,10 +6882,19 @@ const SVC_SCREENS = [ "svc": "CL_NEXTBIZ", "desc": "익영업일 산출: 기준일 포함 이후 주말을 건너뛴 첫 영업일과 건너뛴 일수를 반환.", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1", + "T_KEY1", + "T_ID1" ], "out": [ - "T_ARG1" + "T_RC", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4906,10 +6904,13 @@ const SVC_SCREENS = [ "desc": "승인 대기 목록: 승인요청(memo APPR_REQ) 상태로 대기 중인 완료(C) 마감의", "in": [], "out": [ - "T_COUNT", "T_RC", - "T_ID1", - "T_ARG1" + "T_COUNT", + "T_ARG3", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_GROSS" ] }, { @@ -4919,11 +6920,18 @@ const SVC_SCREENS = [ "desc": "기간 마감 개시(open): close_log 를 O 상태로 등록.", "in": [ "T_BIZDATE", - "T_STR1", - "T_STR2" + "T_STR1" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4932,14 +6940,19 @@ const SVC_SCREENS = [ "svc": "CL_POSCLOSE", "desc": "POS 채널 마감: POS 채널 당일 매입 집계 후 POS 마감이력.", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1" ], "out": [ + "T_RC", "T_ID1", - "T_COUNT", - "T_GROSS", "T_FEE", - "T_NET" + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4948,10 +6961,20 @@ const SVC_SCREENS = [ "svc": "CL_POSTTRIG", "desc": "마감 후속작업 트리거: 마감(C/A) 확인 후 해당 영업일의 정산건을 ledger 에", "in": [ + "T_BIZDATE", + "T_STR1", + "T_KEY1", "T_ID1" ], "out": [ - "T_COUNT" + "T_RC", + "T_ARG1", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4964,7 +6987,11 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", - "T_COUNT" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4973,11 +7000,17 @@ const SVC_SCREENS = [ "svc": "CL_PROVCLOSE", "desc": "가마감: 당일 매입 집계 후 status O + memo 'PROV' 가마감을 생성한다.", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1" ], "out": [ "T_RC", "T_ID1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", "T_GROSS" ] @@ -4988,13 +7021,20 @@ const SVC_SCREENS = [ "svc": "CL_PROVCMP", "desc": "가마감/본마감 비교: 동일 영업일의 가마감(TENT) 잠정 집계와 확정(DAILY C) 차이 산출.", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1", + "T_KEY1", + "T_ID1" ], "out": [ - "T_AMT1", - "T_AMT2", - "T_DELTA", - "T_RC" + "T_RC", + "T_ARG1", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5006,7 +7046,12 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_COUNT" + "T_RC", + "T_COUNT", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_GROSS" ] }, { @@ -5015,13 +7060,21 @@ const SVC_SCREENS = [ "svc": "CL_QTRVERIFY", "desc": "분기 마감 검증: 분기 마감이력과 분기 매입 실집계 대사.", "in": [ + "T_ID1", "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG1", + "T_ARG2", "T_AMT1", "T_AMT2", "T_DELTA", - "T_RC" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5034,7 +7087,15 @@ const SVC_SCREENS = [ "T_STR1" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5047,7 +7108,13 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_COUNT" + "T_RC", + "T_ID1", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5056,10 +7123,16 @@ const SVC_SCREENS = [ "svc": "CL_RECLOSECHK", "desc": "재마감 가능여부: 승인(A)/잠금(L) 상태면 재마감 불가로 판정한다.", "in": [ + "T_BIZDATE", "T_ID1" ], "out": [ - "T_RC" + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5068,11 +7141,16 @@ const SVC_SCREENS = [ "svc": "CL_RECLOSEREG", "desc": "재마감 등록: 기존 마감을 재마감(R) 표기하고 동일 기간에 신규 마감이력 개시.", "in": [ - "T_ID1" + "T_ID1", + "T_BIZDATE" ], "out": [ - "T_ID1", + "T_RC", "T_ID2", + "T_ID1", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", "T_GROSS" ] @@ -5087,9 +7165,17 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_COUNT", + "T_RC", + "T_ARG1", + "T_ARG2", + "T_AMT1", + "T_AMT2", "T_DELTA", - "T_RC" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5099,10 +7185,15 @@ const SVC_SCREENS = [ "desc": "마감 재개시: 완료(C) 상태이고 기간이 잠금되지 않은 마감을 개시(O)로", "in": [ "T_ID1", - "T_REASON" + "T_STR1" ], "out": [ - "T_RC" + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5114,8 +7205,14 @@ const SVC_SCREENS = [ "T_ID1" ], "out": [ - "T_COUNT", - "T_GROSS" + "T_RC", + "T_GROSS", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT" ] }, { @@ -5124,9 +7221,18 @@ const SVC_SCREENS = [ "svc": "CL_RESTORE", "desc": "마감 복원: 취소(X) 마감을 개시(O) 상태로 되돌림.", "in": [ - "T_ID1" + "T_ID1", + "T_REASON", + "T_BIZDATE" ], - "out": [] + "out": [ + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" + ] }, { "mod": "cl", @@ -5134,10 +7240,17 @@ const SVC_SCREENS = [ "svc": "CL_ROLLBACK", "desc": "마감 롤백: 스냅샷 삭제 + 상태 O 로 복귀.", "in": [ - "T_ID1" + "T_ID1", + "T_REASON", + "T_BIZDATE" ], "out": [ - "T_COUNT" + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5147,7 +7260,12 @@ const SVC_SCREENS = [ "desc": "승인 마감 리포트: 승인(A) 상태 마감이력을 최신순으로 나열한다.", "in": [], "out": [ + "T_RC", "T_COUNT", + "T_ARG3", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_GROSS" ] }, @@ -5160,8 +7278,13 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", - "T_ARG4" + "T_ARG3", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_GROSS" ] }, { @@ -5173,7 +7296,12 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", + "T_ARG3", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_GROSS" ] }, @@ -5184,7 +7312,13 @@ const SVC_SCREENS = [ "desc": "취소 마감 리포트: 취소(X) 상태 마감이력을 취소 사유(memo)와 함께 나열한다.", "in": [], "out": [ - "T_COUNT" + "T_RC", + "T_COUNT", + "T_ARG3", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_GROSS" ] }, { @@ -5196,8 +7330,13 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", - "T_ARG2" + "T_ARG3", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_GROSS" ] }, { @@ -5209,7 +7348,12 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", + "T_ARG3", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_GROSS" ] }, @@ -5219,11 +7363,16 @@ const SVC_SCREENS = [ "svc": "CL_RPTISSUER", "desc": "발급사별 마감 리포트: 영업일 매입을 발급사 그룹으로 집계하되 임계금액", "in": [ - "T_BIZDATE", - "T_AMT1" + "T_BIZDATE" ], "out": [ - "T_COUNT" + "T_RC", + "T_COUNT", + "T_ARG3", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_GROSS" ] }, { @@ -5232,10 +7381,16 @@ const SVC_SCREENS = [ "svc": "CL_RPTPERIOD", "desc": "기간별 마감 리포트: close_type(T_KEY1) 의 최근 마감이력을 기간별로 나열한다.", "in": [ - "T_KEY1" + "T_KEY1", + "T_STR1" ], "out": [ + "T_RC", "T_COUNT", + "T_ARG3", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_GROSS" ] }, @@ -5246,7 +7401,13 @@ const SVC_SCREENS = [ "desc": "상태별 마감 리포트: 마감이력을 상태(O/C/A/L/X/R)별 건수/총액으로 집계한다.", "in": [], "out": [ - "T_COUNT" + "T_RC", + "T_COUNT", + "T_ARG3", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_GROSS" ] }, { @@ -5255,11 +7416,16 @@ const SVC_SCREENS = [ "svc": "CL_RPTTOPMER", "desc": "상위 가맹점 마감 리포트: 마감 스냅샷에서 net 상위 N(T_COUNT 입력, 기본 5)", "in": [ - "T_ID1", - "T_COUNT" + "T_ID1" ], "out": [ - "T_COUNT" + "T_RC", + "T_COUNT", + "T_ARG3", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_GROSS" ] }, { @@ -5268,19 +7434,41 @@ const SVC_SCREENS = [ "svc": "CL_SCHEDNEXT", "desc": "다음 마감 일정: 기준일 다음날부터 주말(토/일)을 건너뛴 첫 영업일을 마감 예정일로 산출.", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1", + "T_KEY1", + "T_ID1" ], - "out": [] + "out": [ + "T_RC", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" + ] }, { "mod": "cl", "modnm": "마감", "svc": "CL_SCHEDULE", "desc": "마감 일정 관리: 최종 일마감(biz_date 최대) 기준으로 차기 마감 예정일과", - "in": [], + "in": [ + "T_BIZDATE", + "T_STR1", + "T_KEY1", + "T_ID1" + ], "out": [ "T_RC", - "T_ARG1" + "T_ARG1", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5290,10 +7478,17 @@ const SVC_SCREENS = [ "desc": "부문별 마감: 업종(category) 부문 단위 매입 집계 후 SEG 마감.", "in": [ "T_BIZDATE", + "T_STR1", "T_CATEGORY" ], "out": [ + "T_RC", "T_ID1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", "T_GROSS" ] @@ -5305,13 +7500,20 @@ const SVC_SCREENS = [ "desc": "부문별 마감 검증: 부문 마감이력 총액과 해당 부문 매입 집계 대사.", "in": [ "T_ID1", - "T_CATEGORY" + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG1", + "T_ARG2", "T_AMT1", "T_AMT2", "T_DELTA", - "T_RC" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5320,11 +7522,17 @@ const SVC_SCREENS = [ "svc": "CL_SEMIMONTH", "desc": "반월 마감: 전반(1~15)/후반(16~말) 구간 집계 후 SEMIMON 마감.", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1" ], "out": [ + "T_RC", "T_ID1", - "T_ARG1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", "T_GROSS" ] @@ -5335,10 +7543,20 @@ const SVC_SCREENS = [ "svc": "CL_SEQ", "desc": "마감 순번 발번(dbio upsert).", "in": [ - "T_STR2" + "T_BIZDATE", + "T_STR1", + "T_KEY1", + "T_ID1" ], "out": [ - "T_ID2" + "T_RC", + "T_ID1", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5346,11 +7564,22 @@ const SVC_SCREENS = [ "modnm": "마감", "svc": "CL_SEQAUDIT", "desc": "마감 순번 감사: cl_seq_reg 의 유형별 last_seq 와 실제 cl_close_log 의", - "in": [], + "in": [ + "T_BIZDATE", + "T_STR1", + "T_KEY1", + "T_ID1" + ], "out": [ - "T_COUNT", + "T_RC", "T_ARG1", - "T_RC" + "T_ARG2", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5362,8 +7591,12 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_RC" + "T_GROSS" ] }, { @@ -5372,13 +7605,20 @@ const SVC_SCREENS = [ "svc": "CL_SLAMON", "desc": "마감 SLA 모니터: 기간 프리픽스의 일마감 중 기준시각(T_STR2=HHMI, 기본 1000)", "in": [ + "T_BIZDATE", "T_STR1", - "T_STR2" + "T_KEY1", + "T_ID1" ], "out": [ - "T_COUNT", + "T_RC", "T_ARG1", - "T_RC" + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5387,11 +7627,20 @@ const SVC_SCREENS = [ "svc": "CL_SLASET", "desc": "마감 SLA 등록: close_type(T_KEY1)별 마감 목표시간(T_ARG1, 시간)을 seq_reg 에 upsert.", "in": [ + "T_BIZDATE", + "T_STR1", "T_KEY1", - "T_ARG1" + "T_ID1" ], "out": [ - "T_ARG1" + "T_RC", + "T_ID1", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5404,10 +7653,15 @@ const SVC_SCREENS = [ "T_ID2" ], "out": [ + "T_RC", "T_AMT1", "T_AMT2", - "T_ARG1", - "T_RC" + "T_DELTA", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5420,9 +7674,15 @@ const SVC_SCREENS = [ "T_ID2" ], "out": [ - "T_COUNT", + "T_RC", + "T_AMT1", + "T_AMT2", "T_DELTA", - "T_RC" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5436,6 +7696,11 @@ const SVC_SCREENS = [ "T_STR1" ], "out": [ + "T_RC", + "T_GROSS", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT" ] }, @@ -5448,11 +7713,16 @@ const SVC_SCREENS = [ "T_ID1" ], "out": [ + "T_RC", "T_AMT1", "T_AMT2", + "T_AMT3", "T_DELTA", - "T_ARG1", - "T_RC" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5460,11 +7730,14 @@ const SVC_SCREENS = [ "modnm": "마감", "svc": "CL_STATS", "desc": "마감 통계: close_type 별 마감 건수/총액.", - "in": [ - "T_STR2" - ], + "in": [], "out": [ + "T_RC", "T_COUNT", + "T_ARG3", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_GROSS" ] }, @@ -5474,9 +7747,20 @@ const SVC_SCREENS = [ "svc": "CL_STATUS", "desc": "마감 상태 조회(dbio getter).", "in": [ + "T_BIZDATE", + "T_STR1", + "T_KEY1", "T_ID1" ], - "out": [] + "out": [ + "T_RC", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" + ] }, { "mod": "cl", @@ -5484,16 +7768,21 @@ const SVC_SCREENS = [ "svc": "CL_STCHGAUDIT", "desc": "마감 상태변경 감사: 특정 기간(period_key) 마감이력의 상태 분포를 상태별로 집계해", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1", + "T_KEY1", + "T_ID1" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", - "T_ARG3", - "T_ARG4", "T_ARG5", "T_ARG6", - "T_COUNT" + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5502,15 +7791,19 @@ const SVC_SCREENS = [ "svc": "CL_STCROSS", "desc": "상태 크로스검증: 당일 매입 상태 분포(A/M/S/C)를 집계해 미마감(A+M) 잔량으로", "in": [ - "T_BIZDATE" + "T_ID1" ], "out": [ - "T_ARG1", - "T_ARG2", - "T_ARG3", - "T_ARG4", + "T_RC", + "T_AMT1", + "T_AMT2", + "T_AMT3", + "T_DELTA", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_RC" + "T_GROSS" ] }, { @@ -5522,9 +7815,14 @@ const SVC_SCREENS = [ "T_ID1" ], "out": [ + "T_RC", "T_GROSS", "T_FEE", - "T_NET" + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT" ] }, { @@ -5533,11 +7831,17 @@ const SVC_SCREENS = [ "svc": "CL_TENDAY", "desc": "순(10일) 마감: 상/중/하순 구간 매입 집계 후 TENDAY 마감이력 생성.", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1" ], "out": [ + "T_RC", "T_ID1", - "T_ARG1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", "T_GROSS" ] @@ -5548,10 +7852,17 @@ const SVC_SCREENS = [ "svc": "CL_TENTCLOSE", "desc": "가마감(잠정): 당일 접수/대사(A,M) 매입 잠정 집계로 가마감(status=O) 이력 생성.", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1" ], "out": [ + "T_RC", "T_ID1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", "T_GROSS" ] @@ -5566,10 +7877,14 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", - "T_COUNT", "T_AMT1", "T_AMT2", - "T_AMT3" + "T_AMT3", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5578,11 +7893,20 @@ const SVC_SCREENS = [ "svc": "CL_TRIGRPT", "desc": "마감 후 리포트 트리거: 마감완료(C)이나 리포트 미생성(memo!=RPT:*)인 마감을 대상으로", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1", + "T_KEY1", + "T_ID1" ], "out": [ + "T_RC", + "T_ARG1", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_RC" + "T_GROSS" ] }, { @@ -5591,12 +7915,20 @@ const SVC_SCREENS = [ "svc": "CL_TRIGSETTLE", "desc": "마감 후 정산 트리거: 마감 완료 당일 정산완료(S) 대상 건수/정산액을 산출하고", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1", + "T_KEY1", + "T_ID1" ], "out": [ + "T_RC", + "T_ARG1", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_NET", - "T_RC" + "T_GROSS" ] }, { @@ -5605,14 +7937,20 @@ const SVC_SCREENS = [ "svc": "CL_TRIGSTAT", "desc": "후속작업 상태: 영업일 정산 건수 대비 ledger XFER 전기 건수를 비교해", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1", + "T_KEY1", + "T_ID1" ], "out": [ + "T_RC", "T_ARG1", - "T_ARG2", - "T_ARG3", - "T_DELTA", - "T_RC" + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5620,9 +7958,20 @@ const SVC_SCREENS = [ "modnm": "마감", "svc": "CL_UNCLOSED", "desc": "미마감일 점검: close_log 없는 매입 영업일 건수.", - "in": [], + "in": [ + "T_BIZDATE", + "T_STR1" + ], "out": [ - "T_COUNT" + "T_RC", + "T_ID1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5631,11 +7980,17 @@ const SVC_SCREENS = [ "svc": "CL_UNLOCK", "desc": "기간 잠금 해제.", "in": [ + "T_ID1", "T_STR1", "T_BIZDATE" ], "out": [ - "T_COUNT" + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5648,8 +8003,17 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG1", + "T_ARG2", + "T_AMT1", + "T_AMT2", + "T_DELTA", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_RC" + "T_GROSS" ] }, { @@ -5658,10 +8022,17 @@ const SVC_SCREENS = [ "svc": "CL_WEEKLY", "desc": "주 마감: 주간(월~일) 매입을 집계해 WEEKLY 마감이력 생성.", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1" ], "out": [ + "T_RC", "T_ID1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", "T_GROSS" ] @@ -5672,13 +8043,21 @@ const SVC_SCREENS = [ "svc": "CL_WEEKVERIFY", "desc": "주 마감 검증: 주간 마감이력과 주간 매입 실집계 대사.", "in": [ + "T_ID1", "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG1", + "T_ARG2", "T_AMT1", "T_AMT2", "T_DELTA", - "T_RC" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5687,10 +8066,19 @@ const SVC_SCREENS = [ "svc": "CL_YEAREND", "desc": "연말 마감: 연도(YYYY) 프리픽스로 집계.", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5699,13 +8087,19 @@ const SVC_SCREENS = [ "svc": "CL_YEARPREP", "desc": "연말 결산 사전점검: 연도 내 완료된 월마감(MONTHLY, C/A/L)의 distinct", "in": [ - "T_STR1" + "T_BIZDATE", + "T_STR1", + "T_KEY1", + "T_ID1" ], "out": [ - "T_ARG1", - "T_ARG2", - "T_ARG3", - "T_RC" + "T_RC", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5714,15 +8108,21 @@ const SVC_SCREENS = [ "svc": "CL_YEARVERIFY", "desc": "연 마감 검증: 연간 마감이력과 연간 매입 실집계 대사.", "in": [ + "T_ID1", "T_BIZDATE" ], "out": [ + "T_RC", "T_ARG1", - "T_COUNT", + "T_ARG2", "T_AMT1", "T_AMT2", "T_DELTA", - "T_RC" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5732,10 +8132,16 @@ const SVC_SCREENS = [ "desc": "* CM_ADDDAYS.pgc - cm 모듈 서비스 CM_ADDDAYS (one service = one file).", "in": [ "T_STR1", - "T_ARG1" + "T_ARG1", + "T_BIZDATE", + "T_AMOUNT" ], "out": [ - "T_STR3" + "T_STR3", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5744,13 +8150,20 @@ const SVC_SCREENS = [ "svc": "CM_ADLER32", "desc": "Adler-32 체크섬(순수 계산): 입력(T_STR1)에 대해 두 누산기 a(초기 1)와", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_ARG1", "T_ARG2", "T_ARG3", - "T_STR4" + "T_STR4", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5759,11 +8172,17 @@ const SVC_SCREENS = [ "svc": "CM_AMT_HANGUL", "desc": "금액 한글변환(순수 계산): 정수 금액을 4자리(만 단위) 그룹으로 쪼개", "in": [ - "T_AMOUNT" + "T_AMOUNT", + "T_BIZDATE", + "T_ARG1", + "T_STR1" ], "out": [ "T_RC", - "T_STR4" + "T_STR4", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5773,10 +8192,16 @@ const SVC_SCREENS = [ "desc": "금액 반올림 (순수 계산): 단위(T_ARG1)로 반올림. */", "in": [ "T_AMOUNT", - "T_ARG1" + "T_ARG1", + "T_BIZDATE", + "T_STR1" ], "out": [ - "T_AMT1" + "T_AMT1", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5786,12 +8211,18 @@ const SVC_SCREENS = [ "desc": "금액 균등 분할 (순수 계산): n등분 + 잔액. */", "in": [ "T_AMOUNT", - "T_ARG1" + "T_ARG1", + "T_BIZDATE", + "T_STR1" ], "out": [ "T_AMT1", "T_AMT2", - "T_COUNT" + "T_COUNT", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5801,12 +8232,17 @@ const SVC_SCREENS = [ "desc": "감사로그 레벨별 조회(조회 세분): 레벨(I/W/E)을 검증한 뒤 기준일 해당 레벨", "in": [ "T_STR1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", "T_COUNT", - "T_MSG" + "T_MSG", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5816,12 +8252,19 @@ const SVC_SCREENS = [ "desc": "감사로그 서비스별 조회(조회 세분): 서비스명 기준으로 기준일 로그 건수와", "in": [ "T_SVCNAME", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_COUNT", "T_ID1", - "T_ID2" + "T_ID2", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5831,13 +8274,18 @@ const SVC_SCREENS = [ "desc": "감사로그 오류 비율: dbio(cmdb_log_err_ratio)로 기준일 오류(E) 로그 비율(bps)", "in": [ "T_BIZDATE", - "T_ARG1" + "T_ARG1", + "T_AMOUNT", + "T_STR1" ], "out": [ "T_ARG2", "T_ARG3", "T_STATUS", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5847,13 +8295,20 @@ const SVC_SCREENS = [ "desc": "감사로그 ID 구간 조회(조회 세분): log_id 구간(id1~id2, 최대 폭 10000)의", "in": [ "T_ID1", - "T_ID2" + "T_ID2", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_RC", "T_COUNT", "T_ARG1", - "T_ARG2" + "T_ARG2", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5863,12 +8318,18 @@ const SVC_SCREENS = [ "desc": "감사로그 최근 N건: 기준일(T_BIZDATE)에서 최근 로그 N개(T_ARG1, 기본 5,", "in": [ "T_BIZDATE", - "T_ARG1" + "T_ARG1", + "T_AMOUNT", + "T_STR1" ], "out": [ "T_COUNT", "T_ID1", - "T_STATUS" + "T_STATUS", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5877,13 +8338,19 @@ const SVC_SCREENS = [ "svc": "CM_AUDIT_TOP", "desc": "감사로그 최다 서비스: 기준일(T_BIZDATE) 공통로그를 서비스명으로 그룹핑해", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_RC", "T_SVCNAME", "T_COUNT", - "T_ARG1" + "T_ARG1", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5893,12 +8360,18 @@ const SVC_SCREENS = [ "desc": "배치 선행 완료 점검: 잡(T_KEY1)의 선행 잡(DEP:)을 찾아, 기준일", "in": [ "T_KEY1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_STR4", "T_RC", - "T_COUNT" + "T_COUNT", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5909,10 +8382,15 @@ const SVC_SCREENS = [ "in": [ "T_KEY1", "T_STR1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5922,14 +8400,21 @@ const SVC_SCREENS = [ "desc": "배치 실행 이력 집계: 기준일 해당 잡(svc_name)의 로그를 레벨별로 집계해", "in": [ "T_KEY1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_COUNT", "T_ARG1", "T_ARG2", "T_ARG3", - "T_ID1" + "T_ID1", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5938,13 +8423,20 @@ const SVC_SCREENS = [ "svc": "CM_BATLAST", "desc": "배치 최근 실행 조회: 잡(T_KEY1)의 완료 마커(svc=BATRUN, \" ...\")를", "in": [ - "T_KEY1" + "T_KEY1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_RC", "T_STR4", "T_ID1", - "T_MSG" + "T_MSG", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5955,12 +8447,18 @@ const SVC_SCREENS = [ "in": [ "T_KEY1", "T_BIZDATE", - "T_REASON" + "T_REASON", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_RC", "T_ID1", - "T_COUNT" + "T_COUNT", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5970,11 +8468,17 @@ const SVC_SCREENS = [ "desc": "배치 스케줄 삭제: 잡(T_KEY1)의 스케줄 파라미터(SCHD:)와 딸린 의존성", "in": [ "T_KEY1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_RC", - "T_COUNT" + "T_COUNT", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5983,12 +8487,19 @@ const SVC_SCREENS = [ "svc": "CM_BATSCHD_GET", "desc": "배치 스케줄 조회: SCHD: 파라미터를 읽어 주기와 실행시각을 분리해", "in": [ - "T_KEY1" + "T_KEY1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_RC", "T_STR4", - "T_ARG1" + "T_ARG1", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5996,13 +8507,22 @@ const SVC_SCREENS = [ "modnm": "공통", "svc": "CM_BATSCHD_LIST", "desc": "배치 스케줄 목록 집계: 등록된 스케줄(SCHD:%) 중 주기별(DAILY/WEEKLY/MONTHLY)", - "in": [], + "in": [ + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" + ], "out": [ "T_COUNT", "T_ARG1", "T_ARG2", "T_ARG3", - "T_ARG4" + "T_ARG4", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6014,20 +8534,33 @@ const SVC_SCREENS = [ "T_KEY1", "T_STR1", "T_ARG1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { "mod": "cm", "modnm": "공통", "svc": "CM_BIZDAY", - "desc": "* CM_BIZDAY.pgc - cm 모듈 서비스 CM_BIZDAY (one service = one file).", - "in": [], + "desc": "원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */", + "in": [ + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" + ], "out": [ - "T_BIZDATE" + "T_BIZDATE", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6037,12 +8570,17 @@ const SVC_SCREENS = [ "desc": "N영업일 가산: 기준일(T_BIZDATE)에서 N(T_ARG1)영업일 뒤 날짜를 구한다.", "in": [ "T_BIZDATE", - "T_ARG1" + "T_ARG1", + "T_AMOUNT", + "T_STR1" ], "out": [ "T_RC", "T_STR4", - "T_ARG2" + "T_ARG2", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6052,13 +8590,19 @@ const SVC_SCREENS = [ "desc": "영업일수 계산: from~to 사이를 하루씩 진행하며 주말은 순수 요일 공식", "in": [ "T_STR1", - "T_STR2" + "T_STR2", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", "T_COUNT", "T_ARG1", - "T_ARG2" + "T_ARG2", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6067,49 +8611,77 @@ const SVC_SCREENS = [ "svc": "CM_CACHE_INV", "desc": "캐시 무효화 (순수 처리: 버전 증가 표시). */", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", - "T_MSG" + "T_MSG", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { "mod": "cm", "modnm": "공통", "svc": "CM_CACHE_LOAD", - "desc": "* CM_CACHE_LOAD.pgc - cm 모듈 서비스 CM_CACHE_LOAD (one service = one file).", + "desc": "원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ - "T_COUNT" + "T_COUNT", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { "mod": "cm", "modnm": "공통", "svc": "CM_CHECKDIGIT", - "desc": "* CM_CHECKDIGIT.pgc - cm 모듈 서비스 CM_CHECKDIGIT (one service = one file).", + "desc": "원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */", "in": [ - "T_STR2" + "T_STR2", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_ARG1", - "T_STR4" + "T_STR4", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { "mod": "cm", "modnm": "공통", "svc": "CM_CODE", - "desc": "* CM_CODE.pgc - cm 모듈 서비스 CM_CODE (one service = one file).", + "desc": "원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */", "in": [ "T_STR1", - "T_STR2" + "T_STR2", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STATUS" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6121,10 +8693,17 @@ const SVC_SCREENS = [ "T_KEY1", "T_KEY2", "T_REASON", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1", + "T_STATUS" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6133,13 +8712,21 @@ const SVC_SCREENS = [ "svc": "CM_CODEGRP_CNT", "desc": "코드그룹 현황: 그룹의 전체 코드 수와 사용중(use_yn='Y') 코드 수를 한 번의", "in": [ - "T_KEY1" + "T_KEY1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1", + "T_STATUS" ], "out": [ "T_RC", "T_COUNT", "T_ARG1", - "T_ARG2" + "T_ARG2", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6149,12 +8736,21 @@ const SVC_SCREENS = [ "desc": "코드그룹 차집합 비교: 기준 그룹(T_KEY1)에는 있으나 대상 그룹(T_KEY2)에는", "in": [ "T_KEY1", - "T_KEY2" + "T_KEY2", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1", + "T_STATUS" ], "out": [ "T_COUNT", "T_ARG1", - "T_ARG2" + "T_ARG2", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6163,12 +8759,21 @@ const SVC_SCREENS = [ "svc": "CM_CODEGRP_HIST", "desc": "코드그룹 변경 이력 조회: CODEGRP 이력 로그에서 그룹 접두 패턴으로 건수와", "in": [ - "T_KEY1" + "T_KEY1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1", + "T_STATUS" ], "out": [ "T_COUNT", "T_ID1", - "T_MSG" + "T_MSG", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6181,10 +8786,15 @@ const SVC_SCREENS = [ "T_KEY2", "T_STR1", "T_ARG1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_STATUS" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6193,13 +8803,21 @@ const SVC_SCREENS = [ "svc": "CM_CODEGRP_SPAN", "desc": "코드그룹 정렬범위 조회: 그룹(T_KEY1) 내 sort_ord 의 최대-최소 폭을 dbio", "in": [ - "T_KEY1" + "T_KEY1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1", + "T_STATUS" ], "out": [ "T_RC", "T_ARG1", "T_COUNT", - "T_ARG2" + "T_ARG2", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6212,11 +8830,16 @@ const SVC_SCREENS = [ "T_KEY2", "T_STR1", "T_ARG1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_STATUS" ], "out": [ "T_RC", - "T_STR4" + "T_STR4", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6227,24 +8850,38 @@ const SVC_SCREENS = [ "in": [ "T_KEY1", "T_ARG1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_STR1", + "T_STATUS" ], "out": [ "T_ARG2", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { "mod": "cm", "modnm": "공통", "svc": "CM_CODENAME", - "desc": "* CM_CODENAME.pgc - cm 모듈 서비스 CM_CODENAME (one service = one file).", + "desc": "원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */", "in": [ "T_STR1", - "T_STR2" + "T_STR2", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STATUS" ], "out": [ - "T_STR3" + "T_STR3", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6253,12 +8890,21 @@ const SVC_SCREENS = [ "svc": "CM_CODE_EXPORT", "desc": "코드그룹 직렬화: 그룹(T_KEY1)의 사용중 코드를 정렬순서대로 커서 순회하며", "in": [ - "T_KEY1" + "T_KEY1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1", + "T_STATUS" ], "out": [ "T_STR4", "T_COUNT", - "T_ARG1" + "T_ARG1", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6270,10 +8916,16 @@ const SVC_SCREENS = [ "T_KEY1", "T_KEY2", "T_STR1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STATUS" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6282,12 +8934,20 @@ const SVC_SCREENS = [ "svc": "CM_CODE_REORDER", "desc": "코드 정렬순서 재부여: 대상 그룹(T_KEY1)의 사용중 코드를 코드값 오름차순으로", "in": [ - "T_KEY1" + "T_KEY1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1", + "T_STATUS" ], "out": [ "T_COUNT", "T_ARG1", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6296,12 +8956,19 @@ const SVC_SCREENS = [ "svc": "CM_CODE_SEARCH", "desc": "코드명 검색: 입력어(T_STR1)를 코드명 부분일치 패턴(%검색어%)으로 만들어", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STATUS" ], "out": [ "T_RC", "T_COUNT", - "T_STR4" + "T_STR4", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6311,11 +8978,19 @@ const SVC_SCREENS = [ "desc": "코드 사용여부 토글: 대상 코드(그룹 T_KEY1 / 코드 T_KEY2)의 use_yn 를 현재", "in": [ "T_KEY1", - "T_KEY2" + "T_KEY2", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1", + "T_STATUS" ], "out": [ "T_RC", - "T_STATUS" + "T_STATUS", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6324,11 +8999,18 @@ const SVC_SCREENS = [ "svc": "CM_CRC", "desc": "* CM_CRC.pgc - cm 모듈 서비스 CM_CRC (one service = one file).", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_STR4", - "T_ARG1" + "T_ARG1", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6338,12 +9020,18 @@ const SVC_SCREENS = [ "desc": "기간 계산 유틸(순수 계산): 두 날짜를 역법 공식으로 절대 일련일수로 바꿔", "in": [ "T_STR1", - "T_STR2" + "T_STR2", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", "T_ARG1", - "T_ARG2" + "T_ARG2", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6352,14 +9040,20 @@ const SVC_SCREENS = [ "svc": "CM_DAYOFYEAR", "desc": "연중 경과일/잔여일(순수 계산): 입력일(T_STR1)이 그 해의 몇 번째 날인지", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", "T_ARG1", "T_ARG2", "T_ARG3", - "T_ARG4" + "T_ARG4", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6370,24 +9064,37 @@ const SVC_SCREENS = [ "in": [ "T_STR1", "T_KEY1", - "T_ARG1" + "T_ARG1", + "T_BIZDATE", + "T_AMOUNT" ], "out": [ "T_RC", - "T_STR4" + "T_STR4", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { "mod": "cm", "modnm": "공통", "svc": "CM_ERRMSG", - "desc": "* CM_ERRMSG.pgc - cm 모듈 서비스 CM_ERRMSG (one service = one file).", + "desc": "원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */", "in": [ "T_STR1", - "T_STR2" + "T_STR2", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STATUS" ], "out": [ - "T_MSG" + "T_MSG", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6396,25 +9103,39 @@ const SVC_SCREENS = [ "svc": "CM_FILE_CKSUM", "desc": "파일 체크섬(순수 계산): 파일 내용 문자열에 대해 Adler-32 체크섬(mod 65521", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_STR4", "T_ARG1", "T_ARG2", - "T_COUNT" + "T_COUNT", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { "mod": "cm", "modnm": "공통", "svc": "CM_FMT_AMT", - "desc": "* CM_FMT_AMT.pgc - cm 모듈 서비스 CM_FMT_AMT (one service = one file).", + "desc": "원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */", "in": [ - "T_AMOUNT" + "T_AMOUNT", + "T_BIZDATE", + "T_ARG1", + "T_STR1" ], "out": [ - "T_STR4" + "T_STR4", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6423,23 +9144,37 @@ const SVC_SCREENS = [ "svc": "CM_FMT_DATE", "desc": "날짜 포맷 변환 (순수 계산): YYYYMMDD -> YYYY-MM-DD. */", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ - "T_STR3" + "T_STR3", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { "mod": "cm", "modnm": "공통", "svc": "CM_FX", - "desc": "* CM_FX.pgc - cm 모듈 서비스 CM_FX (one service = one file).", + "desc": "원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */", "in": [ "T_CCY", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ - "T_ARG1" + "T_ARG1", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6449,12 +9184,19 @@ const SVC_SCREENS = [ "desc": "환율 최신성 조회: 통화의 마지막 고시일과 오늘 사이 경과일수를 DB 날짜", "in": [ "T_CCY", - "T_ARG2" + "T_ARG2", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_RC", "T_ARG1", - "T_STR4" + "T_STR4", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6465,11 +9207,18 @@ const SVC_SCREENS = [ "in": [ "T_CCY", "T_BIZDATE", - "T_FX_AMT" + "T_FX_AMT", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_AMOUNT", - "T_ARG1" + "T_ARG1", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6480,13 +9229,18 @@ const SVC_SCREENS = [ "in": [ "T_CCY", "T_STR1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_ARG1", "T_RC", "T_AMT1", - "T_AMT2" + "T_AMT2", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6497,14 +9251,20 @@ const SVC_SCREENS = [ "in": [ "T_CCY", "T_STR1", - "T_STR2" + "T_STR2", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", "T_COUNT", "T_AMT1", "T_AMT2", - "T_STR4" + "T_STR4", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6514,12 +9274,18 @@ const SVC_SCREENS = [ "desc": "역환율 산출: 통화(T_CCY)의 고시환율(rate_bps = KRW*100/unit)을 조회해", "in": [ "T_CCY", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_RC", "T_ARG1", - "T_ARG2" + "T_ARG2", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6529,13 +9295,20 @@ const SVC_SCREENS = [ "desc": "매매기준율 스프레드(순수 계산): 고시환율(T_ARG1, bps)에 스프레드율", "in": [ "T_ARG1", - "T_ARG2" + "T_ARG2", + "T_BIZDATE", + "T_AMOUNT", + "T_STR1", + "T_CCY" ], "out": [ "T_RC", "T_ARG3", "T_ARG4", - "T_DELTA" + "T_DELTA", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6545,13 +9318,19 @@ const SVC_SCREENS = [ "desc": "기준일 평균환율: dbio(cmdb_fx_avg)로 기준일 전체 통화의 평균환율을 구하고,", "in": [ "T_BIZDATE", - "T_CCY" + "T_CCY", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_ARG1", "T_ARG2", "T_ARG3", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6562,10 +9341,15 @@ const SVC_SCREENS = [ "in": [ "T_CCY", "T_BIZDATE", - "T_ARG1" + "T_ARG1", + "T_AMOUNT", + "T_STR1" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6574,13 +9358,21 @@ const SVC_SCREENS = [ "svc": "CM_FX_TICKS", "desc": "환율 고시 현황: 기준일(T_BIZDATE)에 고시된 통화 수와 CCY 코드그룹의 등록", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1", + "T_CCY" ], "out": [ "T_COUNT", "T_ARG1", "T_ARG2", - "T_ARG3" + "T_ARG3", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6591,7 +9383,9 @@ const SVC_SCREENS = [ "in": [ "T_CCY", "T_STR1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", @@ -6599,7 +9393,10 @@ const SVC_SCREENS = [ "T_ARG2", "T_DELTA", "T_ARG3", - "T_STATUS" + "T_STATUS", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6611,12 +9408,16 @@ const SVC_SCREENS = [ "T_CCY", "T_STR1", "T_ARG1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT" ], "out": [ "T_RC", "T_ARG2", - "T_ARG3" + "T_ARG3", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6627,12 +9428,17 @@ const SVC_SCREENS = [ "in": [ "T_CCY", "T_BIZDATE", - "T_ARG1" + "T_ARG1", + "T_AMOUNT", + "T_STR1" ], "out": [ "T_ARG2", "T_ARG3", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6641,12 +9447,19 @@ const SVC_SCREENS = [ "svc": "CM_HASH_DJB2", "desc": "djb2 해시(순수 계산): 입력 문자열(T_STR1)에 대해 hash = hash*33 + c 점화식", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_ARG1", "T_STR4", - "T_COUNT" + "T_COUNT", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6655,12 +9468,19 @@ const SVC_SCREENS = [ "svc": "CM_HASH_FNV", "desc": "FNV-1a 해시(순수 계산): 입력(T_STR1)을 오프셋 베이시스(2166136261)에서", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_ARG1", "T_STR4", - "T_ARG2" + "T_ARG2", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6669,12 +9489,19 @@ const SVC_SCREENS = [ "svc": "CM_HASH_SUM", "desc": "가중 체크섬(순수 계산): 입력(T_STR1) 각 문자의 코드값에 위치 가중(1부터", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_ARG1", "T_ARG2", - "T_STR4" + "T_STR4", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6683,14 +9510,20 @@ const SVC_SCREENS = [ "svc": "CM_HEALTH_ALL", "desc": "종합 헬스 스코어: 기준일 로그/환율/채번/파라미터 존재를 각 25점으로 가중해", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_ARG1", "T_STATUS", "T_ARG2", "T_ARG3", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6698,13 +9531,21 @@ const SVC_SCREENS = [ "modnm": "공통", "svc": "CM_HEALTH_DB", "desc": "시스템 헬스(DB 세분): DB 왕복(SELECT)으로 연결을 확인하고 DB 서버 일자와", - "in": [], + "in": [ + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" + ], "out": [ "T_STATUS", "T_STR4", "T_STR5", "T_ARG2", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6714,13 +9555,18 @@ const SVC_SCREENS = [ "desc": "시스템 헬스(환율 신선도): 기준일(T_BIZDATE)에 고시된 통화가 하나라도 있는지,", "in": [ "T_BIZDATE", - "T_ARG1" + "T_ARG1", + "T_AMOUNT", + "T_STR1" ], "out": [ "T_STATUS", "T_COUNT", "T_ARG2", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6729,13 +9575,20 @@ const SVC_SCREENS = [ "svc": "CM_HEALTH_LOG", "desc": "시스템 헬스(로그 세분): 금일 로그 총량과 오류(E) 비율(bps)을 계산해 등급을", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_STATUS", "T_COUNT", "T_ARG1", - "T_ARG2" + "T_ARG2", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6743,13 +9596,21 @@ const SVC_SCREENS = [ "modnm": "공통", "svc": "CM_HEALTH_PARAM", "desc": "필수 파라미터 점검: 운영에 반드시 필요한 키 목록(BIZ_DATE/VAT_RATE/WHT_BPS", - "in": [], + "in": [ + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" + ], "out": [ "T_ARG1", "T_ARG2", "T_STR4", "T_STATUS", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6757,25 +9618,39 @@ const SVC_SCREENS = [ "modnm": "공통", "svc": "CM_HEALTH_SEQ", "desc": "시스템 헬스(채번 세분): 카운터 개수/최대값과 음수로 굴러간 비정상 카운터", - "in": [], + "in": [ + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" + ], "out": [ "T_STATUS", "T_COUNT", "T_ARG1", "T_ARG2", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { "mod": "cm", "modnm": "공통", "svc": "CM_HOLIDAY_CHK", - "desc": "* CM_HOLIDAY_CHK.pgc - cm 모듈 서비스 CM_HOLIDAY_CHK (one service = one file).", + "desc": "원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6786,12 +9661,19 @@ const SVC_SCREENS = [ "in": [ "T_STR1", "T_STR2", - "T_STR3" + "T_STR3", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_COUNT", "T_STR4", - "T_STR5" + "T_STR5", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6800,11 +9682,17 @@ const SVC_SCREENS = [ "svc": "CM_HOLIDAY_DEL", "desc": "휴일 삭제(캘린더 관리): 삭제 전에 휴일명을 읽어 이력에 남기고 cm_holiday", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", - "T_STR4" + "T_STR4", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6814,13 +9702,18 @@ const SVC_SCREENS = [ "desc": "다음 휴일 조회: 기준일(T_BIZDATE) 이후 국가(T_STR1, 기본 KR)의 가장 가까운", "in": [ "T_BIZDATE", - "T_STR1" + "T_STR1", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", "T_STR4", "T_MSG", - "T_ARG1" + "T_ARG1", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6831,10 +9724,16 @@ const SVC_SCREENS = [ "in": [ "T_STR1", "T_STR2", - "T_STR3" + "T_STR3", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6844,13 +9743,18 @@ const SVC_SCREENS = [ "desc": "연도별 휴일 집계: 연도(T_ARG1)와 국가(T_STR1, 기본 KR)의 등록 휴일 수와", "in": [ "T_ARG1", - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT" ], "out": [ "T_RC", "T_COUNT", "T_ARG2", - "T_ARG3" + "T_ARG3", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6861,10 +9765,16 @@ const SVC_SCREENS = [ "in": [ "T_AMOUNT", "T_ARG1", - "T_ARG2" + "T_ARG2", + "T_BIZDATE", + "T_STR1" ], "out": [ - "T_AMT1" + "T_AMT1", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6873,39 +9783,59 @@ const SVC_SCREENS = [ "svc": "CM_ISO_WEEK", "desc": "ISO-8601 주차 산출(순수 계산): 입력일(T_STR1)의 통일된 일련일수를 구해", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", "T_ARG1", "T_ARG2", - "T_ARG3" + "T_ARG3", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { "mod": "cm", "modnm": "공통", "svc": "CM_LOG", - "desc": "* CM_LOG.pgc - cm 모듈 서비스 CM_LOG (one service = one file).", + "desc": "원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */", "in": [ "T_SVCNAME", "T_MSG", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ - "T_ID1" + "T_ID1", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { "mod": "cm", "modnm": "공통", "svc": "CM_LUHN", - "desc": "* CM_LUHN.pgc - cm 모듈 서비스 CM_LUHN (one service = one file).", + "desc": "원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */", "in": [ - "T_STR5" + "T_STR5", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6914,10 +9844,18 @@ const SVC_SCREENS = [ "svc": "CM_MASK", "desc": "카드번호 마스킹 (순수 계산): 앞6 뒤4 노출. */", "in": [ - "T_STR5" + "T_STR5", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ - "T_STR4" + "T_STR4", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6926,12 +9864,18 @@ const SVC_SCREENS = [ "svc": "CM_MASK_ACCT", "desc": "계좌번호 마스킹(순수 계산): 하이픈 구분자는 위치를 보존하면서, 숫자 기준", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", "T_STR4", - "T_ARG1" + "T_ARG1", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6940,12 +9884,18 @@ const SVC_SCREENS = [ "svc": "CM_MASK_CARD", "desc": "카드번호 마스킹(마스킹 세분, 순수 계산): 전체가 숫자(13~19자리)인지 먼저", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", "T_STR4", - "T_ARG1" + "T_ARG1", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6954,12 +9904,18 @@ const SVC_SCREENS = [ "svc": "CM_MASK_EMAIL", "desc": "이메일 마스킹(순수 계산): 로컬파트(@ 앞)의 첫 글자만 남기고 나머지를 '*' 로", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", "T_STR4", - "T_ARG1" + "T_ARG1", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6968,12 +9924,18 @@ const SVC_SCREENS = [ "svc": "CM_MASK_NAME", "desc": "성명 마스킹(마스킹 세분, 순수 계산): UTF-8 문자 단위로 첫 글자만 남기고", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", "T_STR4", - "T_ARG1" + "T_ARG1", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6982,12 +9944,18 @@ const SVC_SCREENS = [ "svc": "CM_MASK_PHONE", "desc": "전화번호 마스킹(마스킹 세분, 순수 계산): 하이픈 등 구분자는 위치를 그대로", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", "T_STR4", - "T_ARG1" + "T_ARG1", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6996,12 +9964,18 @@ const SVC_SCREENS = [ "svc": "CM_MASK_RRN", "desc": "주민등록번호 마스킹(순수 계산): \"YYMMDD-SXXXXXX\" 13자리 형식을 검증하고,", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", "T_STR4", - "T_ARG1" + "T_ARG1", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7011,13 +9985,18 @@ const SVC_SCREENS = [ "desc": "시장 개장 여부: 대상일(T_BIZDATE)이 국가(T_STR1, 기본 KR) 기준 개장일인지", "in": [ "T_BIZDATE", - "T_STR1" + "T_STR1", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_STATUS", "T_STR4", "T_ARG1", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7026,11 +10005,18 @@ const SVC_SCREENS = [ "svc": "CM_MONTHEND", "desc": "* CM_MONTHEND.pgc - cm 모듈 서비스 CM_MONTHEND (one service = one file).", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_STR3", - "T_ARG1" + "T_ARG1", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7040,12 +10026,19 @@ const SVC_SCREENS = [ "desc": "다국어 메시지 폐기: 언어(T_STR1)와 메시지 ID(T_KEY1)로 MSG_ 그룹의", "in": [ "T_STR1", - "T_KEY1" + "T_KEY1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STATUS" ], "out": [ "T_RC", "T_STR4", - "T_COUNT" + "T_COUNT", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7055,11 +10048,18 @@ const SVC_SCREENS = [ "desc": "다국어 메시지 폴백 조회: 요청 언어 그룹에서 먼저 찾고, 없으면 기본 언어", "in": [ "T_STR1", - "T_KEY1" + "T_KEY1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STATUS" ], "out": [ "T_MSG", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7071,11 +10071,19 @@ const SVC_SCREENS = [ "T_STR1", "T_STR2", "T_STR3", - "T_STR4" + "T_STR4", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STATUS" ], "out": [ "T_MSG", - "T_COUNT" + "T_COUNT", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7085,12 +10093,19 @@ const SVC_SCREENS = [ "desc": "다국어 메시지 조회: 언어코드(KO/EN/JA...)로 메시지 그룹 MSG_ 을 정해", "in": [ "T_STR1", - "T_KEY1" + "T_KEY1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STATUS" ], "out": [ "T_RC", "T_MSG", - "T_STR4" + "T_STR4", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7099,11 +10114,20 @@ const SVC_SCREENS = [ "svc": "CM_MSG_LANGS", "desc": "메시지 다국어 커버리지: 특정 메시지 ID(T_KEY1)가 어떤 언어 그룹(MSG_%)에", "in": [ - "T_KEY1" + "T_KEY1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1", + "T_STATUS" ], "out": [ "T_COUNT", - "T_STR4" + "T_STR4", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7115,11 +10139,18 @@ const SVC_SCREENS = [ "T_STR1", "T_KEY1", "T_STR3", - "T_STR4" + "T_STR4", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STATUS" ], "out": [ "T_RC", - "T_MSG" + "T_MSG", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7130,10 +10161,17 @@ const SVC_SCREENS = [ "in": [ "T_STR1", "T_KEY1", - "T_MSG" + "T_MSG", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STATUS" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7143,23 +10181,37 @@ const SVC_SCREENS = [ "desc": "네팅 계산 (순수 계산): 차변-대변. */", "in": [ "T_AMT1", - "T_AMT2" + "T_AMT2", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_NET", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { "mod": "cm", "modnm": "공통", "svc": "CM_NEXTBIZ", - "desc": "* CM_NEXTBIZ.pgc - cm 모듈 서비스 CM_NEXTBIZ (one service = one file).", + "desc": "원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ - "T_STR3" + "T_STR3", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7169,12 +10221,19 @@ const SVC_SCREENS = [ "desc": "알림 발송 이력: 기준일 NOTI 로그에서 채널 접두 패턴([SMS>...] 등)으로 발송", "in": [ "T_CHANNEL", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_COUNT", "T_ID1", - "T_MSG" + "T_MSG", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7185,13 +10244,18 @@ const SVC_SCREENS = [ "in": [ "T_CHANNEL", "T_BIZDATE", - "T_ARG1" + "T_ARG1", + "T_AMOUNT", + "T_STR1" ], "out": [ "T_ARG2", "T_COUNT", "T_ARG3", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7201,12 +10265,18 @@ const SVC_SCREENS = [ "desc": "알림 재발송: 원 발송 로그 ID(T_ID1)의 내용을 조회해 동일 본문을 재큐잉", "in": [ "T_ID1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_RC", "T_ID2", - "T_MSG" + "T_MSG", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7218,11 +10288,16 @@ const SVC_SCREENS = [ "T_CHANNEL", "T_STR1", "T_MSG", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", - "T_ID1" + "T_ID1", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7231,13 +10306,20 @@ const SVC_SCREENS = [ "svc": "CM_NOTI_STAT", "desc": "알림 채널별 통계: 기준일(T_BIZDATE) NOTI 로그를 본문 접두([SMS>/[EMAIL>/", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_ARG1", "T_ARG2", "T_ARG3", - "T_COUNT" + "T_COUNT", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7251,24 +10333,38 @@ const SVC_SCREENS = [ "T_STR3", "T_CHANNEL", "T_STR1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", "T_ID1", - "T_MSG" + "T_MSG", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { "mod": "cm", "modnm": "공통", "svc": "CM_PARAM", - "desc": "* CM_PARAM.pgc - cm 모듈 서비스 CM_PARAM (one service = one file).", + "desc": "원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_KEY1", + "T_STATUS" ], "out": [ - "T_STR3" + "T_STR3", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7279,11 +10375,18 @@ const SVC_SCREENS = [ "in": [ "T_STR1", "T_STR3", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_KEY1", + "T_STATUS" ], "out": [ "T_RC", - "T_STR4" + "T_STR4", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7294,11 +10397,18 @@ const SVC_SCREENS = [ "in": [ "T_STR1", "T_STR3", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_KEY1", + "T_STATUS" ], "out": [ "T_RC", - "T_STR4" + "T_STR4", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7307,7 +10417,12 @@ const SVC_SCREENS = [ "svc": "CM_PARAM_DIFF", "desc": "파라미터 요청-현재 비교: 대기 요청 값(REQ:)과 현재 본 값을 읽어와", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_KEY1", + "T_STATUS" ], "out": [ "T_RC", @@ -7315,7 +10430,10 @@ const SVC_SCREENS = [ "T_STR5", "T_ARG1", "T_ARG2", - "T_DELTA" + "T_DELTA", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7324,12 +10442,21 @@ const SVC_SCREENS = [ "svc": "CM_PARAM_HIST", "desc": "파라미터 변경 이력 조회: 승인 워크플로가 남긴 공통로그(svc=PARAMWF)에서", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_KEY1", + "T_STATUS" ], "out": [ "T_COUNT", "T_ID1", - "T_MSG" + "T_MSG", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7338,11 +10465,20 @@ const SVC_SCREENS = [ "svc": "CM_PARAM_LIST", "desc": "파라미터 접두 목록: 접두어(T_STR1, 예 \"SCHD:\")로 시작하는 파라미터 키의", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_KEY1", + "T_STATUS" ], "out": [ "T_COUNT", - "T_ARG1" + "T_ARG1", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7353,12 +10489,19 @@ const SVC_SCREENS = [ "in": [ "T_STR1", "T_ARG1", - "T_ARG2" + "T_ARG2", + "T_BIZDATE", + "T_AMOUNT", + "T_KEY1", + "T_STATUS" ], "out": [ "T_RC", "T_ARG3", - "T_ARG4" + "T_ARG4", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7366,12 +10509,22 @@ const SVC_SCREENS = [ "modnm": "공통", "svc": "CM_PARAM_PENDING", "desc": "파라미터 승인 대기 현황: 대기 키(REQ:%) 건수, 가장 오래된 요청 키, 최장", - "in": [], + "in": [ + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1", + "T_KEY1", + "T_STATUS" + ], "out": [ "T_COUNT", "T_STR4", "T_ARG1", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7382,10 +10535,17 @@ const SVC_SCREENS = [ "in": [ "T_STR1", "T_REASON", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_KEY1", + "T_STATUS" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7397,33 +10557,58 @@ const SVC_SCREENS = [ "T_STR1", "T_STR2", "T_STR3", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_KEY1", + "T_STATUS" ], "out": [ "T_RC", - "T_STR4" + "T_STR4", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { "mod": "cm", "modnm": "공통", "svc": "CM_PARAM_SET", - "desc": "* CM_PARAM_SET.pgc - cm 모듈 서비스 CM_PARAM_SET (one service = one file).", + "desc": "원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */", "in": [ "T_STR1", - "T_STR3" + "T_STR3", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_KEY1", + "T_STATUS" ], - "out": [] + "out": [ + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" + ] }, { "mod": "cm", "modnm": "공통", "svc": "CM_PING", - "desc": "* CM_PING.pgc - cm 모듈 서비스 CM_PING (one service = one file).", - "in": [], + "desc": "원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */", + "in": [ + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" + ], "out": [ "T_MSG", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7432,11 +10617,18 @@ const SVC_SCREENS = [ "svc": "CM_PREVBIZ", "desc": "직전 영업일 조회: 기준일에서 하루씩 거슬러 내려가며 토/일(순수 요일 공식)과", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_STR3", - "T_ARG1" + "T_ARG1", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7447,10 +10639,16 @@ const SVC_SCREENS = [ "in": [ "T_AMOUNT", "T_ARG1", - "T_ARG2" + "T_ARG2", + "T_BIZDATE", + "T_STR1" ], "out": [ - "T_AMT1" + "T_AMT1", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7459,24 +10657,38 @@ const SVC_SCREENS = [ "svc": "CM_QTR_END", "desc": "분기말일 산출(순수 계산): 입력일(T_STR1)이 속한 분기(1~4)를 판정하고 그", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", "T_STR4", - "T_ARG1" + "T_ARG1", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { "mod": "cm", "modnm": "공통", "svc": "CM_SEQ", - "desc": "* CM_SEQ.pgc - cm 모듈 서비스 CM_SEQ (one service = one file).", + "desc": "원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */", "in": [ - "T_KEY1" + "T_KEY1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ - "T_ARG1" + "T_ARG1", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7486,13 +10698,19 @@ const SVC_SCREENS = [ "desc": "채번 범위 예약: 카운터(T_KEY1)의 seq_val 을 한 번의 UPDATE 로 N(T_ARG1)만큼", "in": [ "T_KEY1", - "T_ARG1" + "T_ARG1", + "T_BIZDATE", + "T_AMOUNT", + "T_STR1" ], "out": [ "T_RC", "T_ID1", "T_ID2", - "T_COUNT" + "T_COUNT", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7502,11 +10720,18 @@ const SVC_SCREENS = [ "desc": "일자별 채번(채번 세분): 이름+영업일을 합친 키(:)로 매일 1부터", "in": [ "T_KEY1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_ARG1", - "T_STR4" + "T_STR4", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7516,13 +10741,19 @@ const SVC_SCREENS = [ "desc": "연속 채번(블록): 카운터(T_KEY1)에서 N개(T_ARG1)의 일련번호를 연속 채번해", "in": [ "T_KEY1", - "T_ARG1" + "T_ARG1", + "T_BIZDATE", + "T_AMOUNT", + "T_STR1" ], "out": [ "T_RC", "T_ID1", "T_ID2", - "T_COUNT" + "T_COUNT", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7531,12 +10762,19 @@ const SVC_SCREENS = [ "svc": "CM_SEQ_PEEK", "desc": "채번 현재값 조회(채번 세분): 카운터를 증가시키지 않고 cm_seq 의 현재값과", "in": [ - "T_KEY1" + "T_KEY1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_RC", "T_ARG1", - "T_STR4" + "T_STR4", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7547,11 +10785,16 @@ const SVC_SCREENS = [ "in": [ "T_KEY1", "T_ARG1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_STR1" ], "out": [ "T_RC", - "T_ARG2" + "T_ARG2", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7561,12 +10804,18 @@ const SVC_SCREENS = [ "desc": "일자형 채번: 유형(T_KEY1)과 영업일(T_BIZDATE)을 결합한 카운터명", "in": [ "T_KEY1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_RC", "T_ARG1", - "T_STR4" + "T_STR4", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7575,12 +10824,19 @@ const SVC_SCREENS = [ "svc": "CM_SEQ_TYPED", "desc": "유형별 채번(채번 세분): 허용된 채번 유형(SLIP/TICKET/BATCH/NOTI)만 유형별", "in": [ - "T_KEY1" + "T_KEY1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_RC", "T_ARG1", - "T_STR4" + "T_STR4", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7589,12 +10845,19 @@ const SVC_SCREENS = [ "svc": "CM_SEQ_VERIFY", "desc": "채번 체크디짓 검증(순수 계산): 발번 문자열(T_STR1)의 마지막 자리를 체크디짓", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_KEY1" ], "out": [ "T_RC", "T_ARG1", - "T_ARG2" + "T_ARG2", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7602,11 +10865,19 @@ const SVC_SCREENS = [ "modnm": "공통", "svc": "CM_STATUS", "desc": "모듈 상태: 파라미터 건수 조회로 DB 연결 확인. */", - "in": [], + "in": [ + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" + ], "out": [ "T_COUNT", "T_RC", - "T_STATUS" + "T_STATUS", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7618,13 +10889,18 @@ const SVC_SCREENS = [ "T_STR1", "T_STR2", "T_KEY1", - "T_ARG1" + "T_ARG1", + "T_BIZDATE", + "T_AMOUNT" ], "out": [ "T_RC", "T_ARG2", "T_STR4", - "T_ARG1" + "T_ARG1", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7633,13 +10909,20 @@ const SVC_SCREENS = [ "svc": "CM_STR_TRIM", "desc": "문자열 정규화(순수 계산): 입력(T_STR1)의 양끝 공백/탭을 제거하고, 내부의", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_STR4", "T_ARG1", "T_ARG2", - "T_ARG3" + "T_ARG3", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7648,12 +10931,18 @@ const SVC_SCREENS = [ "svc": "CM_WEEKDAY", "desc": "* CM_WEEKDAY.pgc - cm 모듈 서비스 CM_WEEKDAY (one service = one file).", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_ARG1", "T_STR3", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7662,11 +10951,19 @@ const SVC_SCREENS = [ "svc": "LG_ACCRUEDPAY", "desc": "미지급비용 계상: 미지급수수료비용(D)/미지급금(C)을 균형 계상.", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", - "T_AMOUNT" + "T_MERCHANT" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7675,12 +10972,21 @@ const SVC_SCREENS = [ "svc": "LG_ACCRUEDRECV", "desc": "미수수익 계상: 현재 RECV 잔액을 참조해 미수수익(D)/이자수익(C)을 계상.", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", + "T_MERCHANT", "T_COUNT" ], "out": [ + "T_RC", "T_NET", - "T_ID1" + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7689,11 +10995,20 @@ const SVC_SCREENS = [ "svc": "LG_ACCRUEREV", "desc": "계상 환입 분개: 기준일(T_BIZDATE) 이전의 미결 '계상' 전표를 커서로 찾아", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", - "T_ID2" + "T_ID2", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7702,13 +11017,22 @@ const SVC_SCREENS = [ "svc": "LG_ACCTGROUP", "desc": "계정그룹별 집계: 계정과목 접두어(prefix)로 묶어 그룹별 차대 합계를 순회.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", "T_AMT2", "T_AMT3", - "T_DELTA" + "T_DELTA", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7717,15 +11041,24 @@ const SVC_SCREENS = [ "svc": "LG_ACCTHIST", "desc": "계정 잔액 이력 조회: [T_BIZDATE ~ T_STR1] 구간의 계정잔액(lg_account_bal)", "in": [ - "T_KEY1", + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", + "T_MERCHANT", + "T_KEY1", "T_STR1" ], "out": [ + "T_RC", "T_COUNT", "T_AMT2", "T_AMT3", - "T_AMT4" + "T_AMT4", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7734,16 +11067,25 @@ const SVC_SCREENS = [ "svc": "LG_ACCTLEDGER", "desc": "계정과목별 원장조회: 지정 계정/영업일의 라인을 커서로 훑어 차변/대변", "in": [ - "T_KEY1", - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT", + "T_KEY1" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", "T_AMT2", "T_AMT3", "T_DELTA", - "T_ID1" + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7752,14 +11094,23 @@ const SVC_SCREENS = [ "svc": "LG_ACCTPIVOT", "desc": "계정x일자 피벗 잔액: 기간 내 일자별 순액을 누적해 누적잔액과 최고점을 산출.", "in": [ - "T_KEY1", + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", + "T_MERCHANT", + "T_KEY1", "T_STR1" ], "out": [ + "T_RC", "T_COUNT", "T_NET", - "T_AMT2" + "T_AMT2", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7768,15 +11119,24 @@ const SVC_SCREENS = [ "svc": "LG_ACCTRANGE", "desc": "계정과목 기간별 원장집계: [T_BIZDATE ~ T_STR1] 구간의 차변/대변 합계와", "in": [ - "T_KEY1", + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", + "T_MERCHANT", + "T_KEY1", "T_STR1" ], "out": [ + "T_RC", "T_AMT2", "T_AMT3", "T_COUNT", - "T_DELTA" + "T_DELTA", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7785,11 +11145,20 @@ const SVC_SCREENS = [ "svc": "LG_ACCTSUM", "desc": "계정 잔액 재집계(dbio) 후 잔액 반환.", "in": [ - "T_KEY1", - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT", + "T_KEY1" ], "out": [ - "T_NET" + "T_RC", + "T_NET", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7798,12 +11167,21 @@ const SVC_SCREENS = [ "svc": "LG_ACCTTOPN", "desc": "상위 N 계정 잔액: 계정별 순액 절대값 내림차순으로 상위 N개를 훑어 1위를 반환.", "in": [ - "T_COUNT", - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT", + "T_COUNT" ], "out": [ + "T_RC", "T_COUNT", - "T_NET" + "T_NET", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7812,15 +11190,22 @@ const SVC_SCREENS = [ "svc": "LG_ACQPOST", "desc": "매입 전기: 차변 RECV=amount, 대변 MERCH_PAY=net + FEE_INC=fee.", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", "T_MERCHANT", - "T_AMOUNT", "T_FEE", "T_NET", "T_PURCHASE_ID" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7829,14 +11214,21 @@ const SVC_SCREENS = [ "svc": "LG_ADJUST", "desc": "조정 전표: 부호에 따라 차변/대변 단일 분개.", "in": [ - "T_KEY1", + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", "T_MERCHANT", - "T_DELTA", + "T_KEY1", "T_PURCHASE_ID" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7845,13 +11237,22 @@ const SVC_SCREENS = [ "svc": "LG_ALLOWANCE", "desc": "대손충당금 설정: RECV 잔액의 일정률을 대손상각비(D)/대손충당금(C)으로 설정.", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", + "T_MERCHANT", "T_COUNT" ], "out": [ + "T_RC", "T_NET", "T_ID1", - "T_AMT2" + "T_AMT2", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7860,13 +11261,22 @@ const SVC_SCREENS = [ "svc": "LG_ALLOWRECALC", "desc": "충당금 재산정: 필요 충당금(RECV x 율)과 현재 충당금 잔액 차이를 조정 전기.", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", + "T_MERCHANT", "T_COUNT" ], "out": [ + "T_RC", "T_DELTA", "T_ID1", - "T_AMT2" + "T_AMT2", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7875,10 +11285,19 @@ const SVC_SCREENS = [ "svc": "LG_APPLY", "desc": "전표 전기확정(P): 라인들의 계정잔액을 잔액테이블에 반영.", "in": [ + "T_AMOUNT", "T_ID1", - "T_BIZDATE" + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], - "out": [] + "out": [ + "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" + ] }, { "mod": "lg", @@ -7886,11 +11305,20 @@ const SVC_SCREENS = [ "svc": "LG_APPRDELEG", "desc": "승인 위임: 작성(D) 전표의 승인 권한을 대리인에게 위임(메모 누적).", "in": [ + "T_AMOUNT", "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT", "T_STR1" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7899,11 +11327,20 @@ const SVC_SCREENS = [ "svc": "LG_APPRHIST", "desc": "승인 이력 조회: 당일 전기(P) 완료된 전표 수와 차변 총액을 집계.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", - "T_AMT2" + "T_AMT2", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7912,11 +11349,20 @@ const SVC_SCREENS = [ "svc": "LG_APPROVE", "desc": "전표 승인 워크플로: 작성(D) 전표만 대상으로 차변합=대변합 균형을 검증한 뒤", "in": [ - "T_ID1" + "T_ID1", + "T_AMOUNT", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_DELTA", - "T_AMT2" + "T_AMT2", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7924,13 +11370,24 @@ const SVC_SCREENS = [ "modnm": "원장", "svc": "LG_APPRQUEUE", "desc": "전표 승인 대기 큐 조회: 미결(D) 전표의 대기 건수와 전표 ID 범위(최소/최대),", - "in": [], + "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" + ], "out": [ + "T_RC", "T_COUNT", "T_ID1", "T_ID2", "T_ARG1", - "T_ARG2" + "T_ARG2", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7939,12 +11396,21 @@ const SVC_SCREENS = [ "svc": "LG_AUDITSCAN", "desc": "원장 감사 스캔: 취소(X) 전표인데 라인이 여전히 posted 인 불일치와 고아 라인을 탐지.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", - "T_COUNT" + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7954,12 +11420,19 @@ const SVC_SCREENS = [ "desc": "대손 처리 분개: 가맹점(T_MERCHANT) 채권 T_AMOUNT 를 상각 —", "in": [ "T_AMOUNT", - "T_MERCHANT", + "T_ID1", + "T_DELTA", "T_BIZDATE", + "T_MERCHANT", "T_REASON" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7969,12 +11442,19 @@ const SVC_SCREENS = [ "desc": "대손 회수(환입) 분개: 상각했던 채권이 회수되면 D:CASH / C:BADDEBT_RCV 로", "in": [ "T_AMOUNT", - "T_MERCHANT", - "T_BIZDATE" + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_AMT2", - "T_ID1" + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7983,13 +11463,22 @@ const SVC_SCREENS = [ "svc": "LG_BALCERT", "desc": "잔액증명서 발급: 계정 차변/대변을 재계산해 계정잔액에 upsert 하고 잔액을 증명.", "in": [ - "T_KEY1", - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT", + "T_KEY1" ], "out": [ + "T_RC", "T_AMT2", "T_AMT3", - "T_NET" + "T_NET", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7998,12 +11487,20 @@ const SVC_SCREENS = [ "svc": "LG_BALCHK", "desc": "전표 단위 대차 균형 점검 (차변합 vs 대변합).", "in": [ - "T_ID1" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_AMT2", "T_AMT3", - "T_RC" + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8012,13 +11509,22 @@ const SVC_SCREENS = [ "svc": "LG_BALPROOF", "desc": "잔액 증명: 계정잔액 테이블(lg_account_bal)의 기록 잔액과 원장 라인에서", "in": [ - "T_KEY1", - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT", + "T_KEY1" ], "out": [ + "T_RC", "T_AMT2", "T_AMT3", - "T_DELTA" + "T_DELTA", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8027,11 +11533,20 @@ const SVC_SCREENS = [ "svc": "LG_BATCHAPPROVE", "desc": "일괄 승인: 당일 작성(D) 전표를 순회하며 차대 균형이 맞는 것만 전기(P) 승인.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", - "T_ARG1" + "T_ARG1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8041,14 +11556,22 @@ const SVC_SCREENS = [ "desc": "고액 분개 조회: 임계금액(T_AMOUNT, 기본 1000만) 이상 라인의 건수·합계와", "in": [ "T_AMOUNT", - "T_BIZDATE" + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", "T_GROSS", "T_ID1", "T_ID2", - "T_AMT2" + "T_AMT2", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8057,14 +11580,21 @@ const SVC_SCREENS = [ "svc": "LG_BLACKADJUST", "desc": "흑색 정정분개: 과소계상분을 양수(흑색) 분개 쌍으로 추가 보정.", "in": [ - "T_KEY1", - "T_BIZDATE", + "T_AMOUNT", "T_ID1", - "T_DELTA" + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT", + "T_KEY1" ], "out": [ + "T_RC", "T_ID2", - "T_DELTA" + "T_DELTA", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8075,12 +11605,18 @@ const SVC_SCREENS = [ "in": [ "T_ID1", "T_AMOUNT", + "T_DELTA", "T_BIZDATE", "T_MERCHANT" ], "out": [ + "T_RC", "T_ID2", - "T_ARG1" + "T_ARG1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8089,12 +11625,21 @@ const SVC_SCREENS = [ "svc": "LG_BSSUMMARY", "desc": "재무상태 요약: 자산(RECV/CASH/PREPAID) - 부채(MERCH_PAY/UNEARNED_REV) = 자본.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_AMT2", "T_AMT3", - "T_NET" + "T_NET", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8103,10 +11648,19 @@ const SVC_SCREENS = [ "svc": "LG_CARRY", "desc": "이월: 당일 계정잔액을 익영업일 개시잔액으로 이월(upsert).", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ - "T_COUNT" + "T_RC", + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8115,14 +11669,22 @@ const SVC_SCREENS = [ "svc": "LG_CLOSECHK", "desc": "결산 체크리스트: 당일 (1)미결 전표 (2)불균형 전표 (3)0원 라인", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", "T_ARG3", "T_ARG4", - "T_RC" + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8131,13 +11693,21 @@ const SVC_SCREENS = [ "svc": "LG_CLOSELIST", "desc": "결산 체크리스트: 미결전표/시산표 불균형/무분개 전표를 집계해 마감 준비도를 판정.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", "T_ARG3", - "T_RC" + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8146,10 +11716,19 @@ const SVC_SCREENS = [ "svc": "LG_CLOSE_LINK", "desc": "마감연계: 전표 라인 closed + 전표 상태 S(마감연계).", "in": [ - "T_ID1" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ - "T_COUNT" + "T_RC", + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8158,10 +11737,18 @@ const SVC_SCREENS = [ "svc": "LG_CONFIRM", "desc": "전표 확정: 대차 균형이면 P로 확정, 아니면 실패.", "in": [ - "T_ID1" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ - "T_RC" + "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8170,13 +11757,21 @@ const SVC_SCREENS = [ "svc": "LG_CORPLEDGER", "desc": "법인(가맹점)별 원장 집계: 가맹점 원장 라인의 차변/대변 잔액을 산출.", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_AMT2", "T_AMT3", - "T_NET" + "T_NET", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8185,11 +11780,20 @@ const SVC_SCREENS = [ "svc": "LG_CORPSUM", "desc": "법인별 순액 집계: 가맹점별 (차변-대변) 순액의 절대값 합과 법인 수를 순회.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", - "T_NET" + "T_NET", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8198,10 +11802,19 @@ const SVC_SCREENS = [ "svc": "LG_CORRECT", "desc": "원장 라인 금액 정정.", "in": [ + "T_AMOUNT", "T_ID1", - "T_AMOUNT" + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], - "out": [] + "out": [ + "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" + ] }, { "mod": "lg", @@ -8209,14 +11822,21 @@ const SVC_SCREENS = [ "svc": "LG_CORRPAIR", "desc": "정정 짝분개: 잘못 분류된 계정을 올바른 계정으로 재분류(D 신계정/C 구계정).", "in": [ - "T_KEY1", - "T_KEY2", + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", "T_MERCHANT", - "T_AMOUNT" + "T_KEY1", + "T_KEY2" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8227,11 +11847,18 @@ const SVC_SCREENS = [ "in": [ "T_ID1", "T_AMOUNT", - "T_KEY1", + "T_DELTA", "T_BIZDATE", - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], - "out": [] + "out": [ + "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" + ] }, { "mod": "lg", @@ -8239,10 +11866,19 @@ const SVC_SCREENS = [ "svc": "LG_DAILYSUM", "desc": "당일 계정별 잔액 일괄 재집계(전 계정 upsert).", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ - "T_COUNT" + "T_RC", + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8251,11 +11887,20 @@ const SVC_SCREENS = [ "svc": "LG_DAYCLOSE", "desc": "일마감: 당일 균형(차변합=대변합) 미결 전표를 일괄 P 승격한 뒤, 당일 사용된", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_ARG1", - "T_COUNT" + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8266,11 +11911,18 @@ const SVC_SCREENS = [ "in": [ "T_ID1", "T_AMOUNT", - "T_KEY1", + "T_DELTA", "T_BIZDATE", - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], - "out": [] + "out": [ + "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" + ] }, { "mod": "lg", @@ -8278,14 +11930,23 @@ const SVC_SCREENS = [ "svc": "LG_DEPTLEDGER", "desc": "부문(전표유형)별 원장 조회: 유형별 차변/대변 합계와 분개 건수를 집계.", "in": [ - "T_KEY1", - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT", + "T_KEY1" ], "out": [ + "T_RC", "T_AMT2", "T_AMT3", "T_DELTA", - "T_COUNT" + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8294,12 +11955,21 @@ const SVC_SCREENS = [ "svc": "LG_DEPTPL", "desc": "부문별 손익 조회: 당일 매입을 업종/부문(category)별로 묶어 수수료 손익을", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", "T_GROSS", - "T_AMT2" + "T_AMT2", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8308,15 +11978,24 @@ const SVC_SCREENS = [ "svc": "LG_DEPTRANGE", "desc": "부문 기간 금액범위: 전표유형의 기간 내 분개 최소/최대 금액과 건수를 조회.", "in": [ - "T_KEY1", + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", + "T_MERCHANT", + "T_KEY1", "T_STR1" ], "out": [ + "T_RC", "T_AMT2", "T_AMT3", "T_COUNT", - "T_DELTA" + "T_DELTA", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8325,11 +12004,20 @@ const SVC_SCREENS = [ "svc": "LG_DEPTSUM", "desc": "부문별 차변 합계 최대 유형 탐색: 전표유형별 차변합을 순회해 최대 부문을 선별.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_AMT2", - "T_AMT3" + "T_AMT3", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8338,13 +12026,21 @@ const SVC_SCREENS = [ "svc": "LG_DIFFCHK", "desc": "당일 전체 대차 차액 점검(총차변-총대변).", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_AMT2", "T_AMT3", "T_DELTA", - "T_RC" + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8353,15 +12049,22 @@ const SVC_SCREENS = [ "svc": "LG_DOUBLE", "desc": "신규 전표 즉시 전기(posted): 차변/대변 + 양 계정잔액 반영.", "in": [ - "T_KEY1", - "T_KEY2", + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", "T_MERCHANT", - "T_AMOUNT", + "T_KEY1", + "T_KEY2", "T_PURCHASE_ID" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8370,13 +12073,22 @@ const SVC_SCREENS = [ "svc": "LG_DUPSCAN", "desc": "중복 분개 스캔: 당일 동일 (전표, 계정, 차대, 금액) 조합이 2건 이상인 중복", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", "T_ARG1", "T_ID1", - "T_ARG2" + "T_ARG2", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8385,12 +12097,21 @@ const SVC_SCREENS = [ "svc": "LG_EQUITYMOVE", "desc": "자본 변동: 기간 내 이익잉여금(RETAINED) 대변순액 변동을 집계.", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", + "T_MERCHANT", "T_STR1" ], "out": [ + "T_RC", "T_NET", - "T_COUNT" + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8399,11 +12120,20 @@ const SVC_SCREENS = [ "svc": "LG_EXPENSEROLL", "desc": "비용 롤업: 마감일까지 비용계정(_EXP)별 차변순액을 순회해 총비용을 집계.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", - "T_NET" + "T_NET", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8412,11 +12142,20 @@ const SVC_SCREENS = [ "svc": "LG_FEEACCRUE", "desc": "수수료 미수 계상: 대사완료(M)했지만 아직 미정산인 매입의 수수료를", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_FEE", - "T_ID1" + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8425,13 +12164,21 @@ const SVC_SCREENS = [ "svc": "LG_FEEPOST", "desc": "수수료수익 전기: 차변 RECV=fee, 대변 FEE_INC=fee.", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", "T_MERCHANT", "T_FEE", "T_PURCHASE_ID" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8440,12 +12187,21 @@ const SVC_SCREENS = [ "svc": "LG_FEERECOG", "desc": "수수료 수익 인식: 당일 정산완료(S) 매입 수수료 합계를", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_FEE", "T_ID1", - "T_COUNT" + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8455,13 +12211,22 @@ const SVC_SCREENS = [ "desc": "수수료 부문 배부: 당일 FEE_INC 대변 합계를 배부율(T_ARG1 bps)로 부문 A/B 에", "in": [ "T_ARG1", - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_GROSS", "T_ID1", "T_AMT2", - "T_AMT3" + "T_AMT3", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8470,12 +12235,21 @@ const SVC_SCREENS = [ "svc": "LG_HALFCLOSE", "desc": "반기 결산 분개: 반기 FEE_INC 순액을 이익잉여금(RETAINED)으로 균형 대체.", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", + "T_MERCHANT", "T_STR1" ], "out": [ + "T_RC", "T_NET", - "T_ID1" + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8484,11 +12258,20 @@ const SVC_SCREENS = [ "svc": "LG_INCOMEROLL", "desc": "수익 롤업: 마감일까지 수익계정(_INC)별 대변순액을 순회해 총수익을 집계.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", - "T_NET" + "T_NET", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8497,9 +12280,19 @@ const SVC_SCREENS = [ "svc": "LG_INQUIRY", "desc": "전표 상태 조회(dbio).", "in": [ - "T_ID1" + "T_ID1", + "T_AMOUNT", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], - "out": [] + "out": [ + "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" + ] }, { "mod": "lg", @@ -8507,10 +12300,19 @@ const SVC_SCREENS = [ "svc": "LG_JOURNAL", "desc": "당일 분개(원장 라인) 건수 조회.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ - "T_COUNT" + "T_RC", + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8519,12 +12321,21 @@ const SVC_SCREENS = [ "svc": "LG_LEDGSETTLE", "desc": "원장-정산 대사: base 원장(ledger) SETTLE 합계와 원장라인 지급 합계를 대조.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_AMT2", "T_AMT3", - "T_DELTA" + "T_DELTA", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8533,13 +12344,22 @@ const SVC_SCREENS = [ "svc": "LG_LINECOUNT", "desc": "원장 라인 집계: 당일 라인의 posted/closed 상태별 건수, 관련 전표 수,", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", "T_COUNT", - "T_ARG3" + "T_ARG3", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8548,11 +12368,20 @@ const SVC_SCREENS = [ "svc": "LG_MASKINQ", "desc": "원장 마스킹 조회: 라인 ID(T_ID1)의 원장 라인을 가맹점 ID 마스킹(앞 2자리", "in": [ - "T_ID1" + "T_ID1", + "T_AMOUNT", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_ID2", - "T_AMOUNT" + "T_AMOUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8561,13 +12390,22 @@ const SVC_SCREENS = [ "svc": "LG_MEMOAUDIT", "desc": "전표 메모 감사: 당일 전기(P) 전표 중 메모가 비어 있는 건수와 표본 전표 ID,", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", "T_ARG1", "T_ID1", - "T_ARG2" + "T_ARG2", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8576,10 +12414,19 @@ const SVC_SCREENS = [ "svc": "LG_MERCHBAL", "desc": "가맹점 잔액 재집계(dbio).", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], - "out": [] + "out": [ + "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" + ] }, { "mod": "lg", @@ -8587,13 +12434,21 @@ const SVC_SCREENS = [ "svc": "LG_MERCHCERT", "desc": "가맹점 잔액증명: 가맹점잔액 기록치와 라인 재계산치를 대조해 증명(OK/NG).", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_AMT2", "T_AMT3", - "T_DELTA" + "T_DELTA", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8602,14 +12457,22 @@ const SVC_SCREENS = [ "svc": "LG_MERCHPROOF", "desc": "가맹점 잔액 증명: 가맹점 잔액 테이블(lg_merch_bal)의 차변/대변 누계와", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_AMT2", "T_AMT3", "T_AMT4", - "T_ARG1" + "T_ARG1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8618,12 +12481,21 @@ const SVC_SCREENS = [ "svc": "LG_MONCLOSEJE", "desc": "월차 결산 분개: 당월 RECV 순증가를 미수수익(RECV_ACCRUED)으로 계상.", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", + "T_MERCHANT", "T_STR1" ], "out": [ + "T_RC", "T_NET", - "T_ID1" + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8632,12 +12504,21 @@ const SVC_SCREENS = [ "svc": "LG_MONTHCLOSE", "desc": "월차 결산 분개: [T_BIZDATE=월초 ~ T_STR1=월말] 구간 FEE_INC 순액(대변-차변)을", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", + "T_MERCHANT", "T_STR1" ], "out": [ + "T_RC", "T_AMT2", - "T_ID1" + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8646,12 +12527,21 @@ const SVC_SCREENS = [ "svc": "LG_MONTHLYSUM", "desc": "월 계정과목별 차/대 집계 건수 조회(YYYY-MM).", "in": [ - "T_STR1", - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT", + "T_STR1" ], "out": [ + "T_RC", "T_COUNT", - "T_GROSS" + "T_GROSS", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8660,12 +12550,21 @@ const SVC_SCREENS = [ "svc": "LG_NETPOS", "desc": "순포지션 조회: 당일 채권(RECV) 잔액과 가맹점미지급(MERCH_PAY) 잔액을", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_AMT2", "T_AMT3", - "T_NET" + "T_NET", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8674,13 +12573,21 @@ const SVC_SCREENS = [ "svc": "LG_PARTREVERSE", "desc": "부분 역분개: 원전표에서 특정 계정 라인만 차대를 뒤집어 신규 전표로 복사.", "in": [ + "T_AMOUNT", "T_ID1", - "T_KEY1", - "T_BIZDATE" + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT", + "T_KEY1" ], "out": [ + "T_RC", "T_ID2", - "T_COUNT" + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8689,13 +12596,22 @@ const SVC_SCREENS = [ "svc": "LG_PAYACCRUE", "desc": "미지급 계상 분개: 당일 정산 순액 합계와 원장 MERCH_PAY 대변 합계의 갭을", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_NET", "T_AMT3", "T_DELTA", - "T_ID1" + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8704,12 +12620,21 @@ const SVC_SCREENS = [ "svc": "LG_PAYMATCH", "desc": "지급 매칭: 원장 지급(MERCH_PAY 대변) 건수와 정산 건수를 대조해 매칭율 판정.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", - "T_DELTA" + "T_DELTA", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8718,14 +12643,22 @@ const SVC_SCREENS = [ "svc": "LG_PAYRECON", "desc": "원장-지급 대사: 가맹점(T_MERCHANT)의 원장 라인 순액(대변-차변)과 정산", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_AMT2", "T_NET", "T_DELTA", - "T_COUNT" + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8733,13 +12666,24 @@ const SVC_SCREENS = [ "modnm": "원장", "svc": "LG_PENDAGE", "desc": "미결전표 경과일 집계: 작성(D) 전표를 경과일 구간(0-1일 / 2-7일 / 8일+)별로", - "in": [], + "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" + ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", "T_ARG3", "T_ARG4", - "T_COUNT" + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8748,10 +12692,19 @@ const SVC_SCREENS = [ "svc": "LG_PENDCLEAR", "desc": "미결전표 정리: 기준일 이전 작성(D) 전표를 취소(X)하고 라인을 마감 처리.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ - "T_COUNT" + "T_RC", + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8760,11 +12713,20 @@ const SVC_SCREENS = [ "svc": "LG_PENDESCAL", "desc": "미결 에스컬레이션: 기준일보다 오래 방치된 작성(D) 전표 건수를 집계.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", - "T_AMT2" + "T_AMT2", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8772,11 +12734,22 @@ const SVC_SCREENS = [ "modnm": "원장", "svc": "LG_PENDLIST", "desc": "미결전표 관리(목록 집계): 작성(D) 상태 전표의 건수, 연계 라인 총차변액,", - "in": [], + "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" + ], "out": [ + "T_RC", "T_COUNT", "T_ID1", - "T_AMT2" + "T_AMT2", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8785,11 +12758,20 @@ const SVC_SCREENS = [ "svc": "LG_PENDROUTE", "desc": "미결전표 배정: 특정 작성(D) 전표의 메모에 담당자를 기록해 배정.", "in": [ + "T_AMOUNT", "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT", "T_STR1" ], "out": [ - "T_COUNT" + "T_RC", + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8798,11 +12780,20 @@ const SVC_SCREENS = [ "svc": "LG_PENDSTAT", "desc": "미결 상태별 집계: 당일 전표를 상태(D/P/M/S/X)별로 순회 집계.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", - "T_ARG1" + "T_ARG1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8811,12 +12802,21 @@ const SVC_SCREENS = [ "svc": "LG_PERIODTB", "desc": "기간별 시산표: [T_BIZDATE ~ T_STR1] 구간을 계정별 GROUP BY 커서로 집계해", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", + "T_MERCHANT", "T_STR1" ], "out": [ + "T_RC", "T_COUNT", - "T_DELTA" + "T_DELTA", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8825,11 +12825,20 @@ const SVC_SCREENS = [ "svc": "LG_PLCLOSE", "desc": "손익 대체 분개(일 단위): 당일 손익계정(FEE_INC, BADDEBT_EXP)의 순액을 각각", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", - "T_NET" + "T_NET", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8838,12 +12847,21 @@ const SVC_SCREENS = [ "svc": "LG_PLSUMMARY", "desc": "손익 요약: 당일 수익(_INC)과 비용(_EXP)의 순액을 계산해 순손익을 산출.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_AMT2", "T_AMT3", - "T_NET" + "T_NET", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8852,15 +12870,22 @@ const SVC_SCREENS = [ "svc": "LG_POST", "desc": "전표 작성(draft) + 차변/대변 균형 분개 2줄.", "in": [ - "T_KEY1", - "T_KEY2", + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", "T_MERCHANT", - "T_PURCHASE_ID", - "T_AMOUNT" + "T_KEY1", + "T_KEY2", + "T_PURCHASE_ID" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8869,13 +12894,21 @@ const SVC_SCREENS = [ "svc": "LG_PREPAIDAMORT", "desc": "선급 상각: 선급비용 총액을 개월(N)로 나눠 1개월분을 비용(D)/선급(C) 상각.", "in": [ - "T_BIZDATE", "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT", "T_COUNT" ], "out": [ + "T_RC", "T_ID1", - "T_NET" + "T_NET", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8884,11 +12917,19 @@ const SVC_SCREENS = [ "svc": "LG_PREPAIDPOST", "desc": "선급비용 계상: 선급비용(D)/현금(C)으로 지출 시점 자산 계상.", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", - "T_AMOUNT" + "T_MERCHANT" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8898,11 +12939,21 @@ const SVC_SCREENS = [ "desc": "분기 결산 분개: 연도(T_ARG1)/분기(T_ARG2)로 분기 구간을 산출해 그 구간의", "in": [ "T_ARG1", - "T_ARG2" + "T_ARG2", + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_ID1", - "T_AMT2" + "T_AMT2", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8911,12 +12962,21 @@ const SVC_SCREENS = [ "svc": "LG_QTRCLOSEJE", "desc": "분기 결산 분개: 당분기 대손비용 발생분을 미지급(ACCRUED_PAY)으로 계상.", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", + "T_MERCHANT", "T_STR1" ], "out": [ + "T_RC", "T_NET", - "T_ID1" + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8925,10 +12985,20 @@ const SVC_SCREENS = [ "svc": "LG_RECALC", "desc": "계정 잔액 원장 라인 기준 재계산(dbio).", "in": [ - "T_KEY1", - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT", + "T_KEY1" ], - "out": [] + "out": [ + "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" + ] }, { "mod": "lg", @@ -8936,12 +13006,19 @@ const SVC_SCREENS = [ "svc": "LG_RECOVERY", "desc": "상각채권 회수: 제각 채권 회수를 현금(D)/대손충당금환입(C)으로 인식.", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", - "T_MERCHANT", - "T_AMOUNT" + "T_MERCHANT" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8950,12 +13027,21 @@ const SVC_SCREENS = [ "svc": "LG_RECVACCRUE", "desc": "미수 계상 분개: 접수(A) 상태 매입 총액을 D:RECV_ACCR / C:SUSP 로 미결 계상", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", "T_ID1", - "T_AMT2" + "T_AMT2", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8964,14 +13050,21 @@ const SVC_SCREENS = [ "svc": "LG_REDADJUST", "desc": "적색 정정분개: 과대계상된 차변을 음수(적색) 분개 쌍으로 취소 감액.", "in": [ - "T_KEY1", - "T_BIZDATE", + "T_AMOUNT", "T_ID1", - "T_DELTA" + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT", + "T_KEY1" ], "out": [ + "T_RC", "T_ID2", - "T_DELTA" + "T_DELTA", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8980,12 +13073,21 @@ const SVC_SCREENS = [ "svc": "LG_REDBLACKNET", "desc": "적흑 상계: 원전표에 연결된 적색/흑색 정정 금액을 합산해 순정정액을 반환.", "in": [ - "T_ID1" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_AMT2", "T_AMT3", - "T_NET" + "T_NET", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8995,11 +13097,19 @@ const SVC_SCREENS = [ "desc": "적색 정정 분개: 원전표(T_ID1)의 모든 라인을 차/대변 반전(INSERT..SELECT CASE)", "in": [ "T_ID1", - "T_BIZDATE" + "T_AMOUNT", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_ID2", - "T_COUNT" + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9009,9 +13119,19 @@ const SVC_SCREENS = [ "desc": "전표 반려 워크플로: 작성(D) 전표를 X(취소) 처리하고 반려 사유(T_REASON)를", "in": [ "T_ID1", + "T_AMOUNT", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT", "T_REASON" ], - "out": [] + "out": [ + "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" + ] }, { "mod": "lg", @@ -9019,10 +13139,19 @@ const SVC_SCREENS = [ "svc": "LG_REOPEN", "desc": "취소 전표 재개: X(취소) 전표를 D(작성)로 되돌리고 closed 라인을 posted 로", "in": [ - "T_ID1" + "T_ID1", + "T_AMOUNT", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ - "T_COUNT" + "T_RC", + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9031,12 +13160,20 @@ const SVC_SCREENS = [ "svc": "LG_REPOST", "desc": "재전기: 취소(X)된 전표의 라인을 방향 그대로 신규 전기(P) 전표로 복제.", "in": [ + "T_AMOUNT", "T_ID1", - "T_BIZDATE" + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_ID2", - "T_COUNT" + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9045,12 +13182,19 @@ const SVC_SCREENS = [ "svc": "LG_REVACCRUE", "desc": "수익 발생 계상: 발생주의 수익을 미수수익(D)/수수료수익(C)으로 균형 계상.", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", - "T_MERCHANT", - "T_AMOUNT" + "T_MERCHANT" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9059,12 +13203,19 @@ const SVC_SCREENS = [ "svc": "LG_REVDEFER", "desc": "수익 이연: 미실현 수익을 수수료수익(D)/선수수익(C)으로 이연 대체.", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", - "T_MERCHANT", - "T_AMOUNT" + "T_MERCHANT" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9073,12 +13224,20 @@ const SVC_SCREENS = [ "svc": "LG_REVERSE", "desc": "역분개 전표: 원전표 라인의 차/대를 뒤집어 신규 전표에 복사.", "in": [ + "T_AMOUNT", "T_ID1", - "T_BIZDATE" + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_ID2", - "T_COUNT" + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9087,12 +13246,19 @@ const SVC_SCREENS = [ "svc": "LG_REVRELEASE", "desc": "이연수익 환입: 선수수익(D)을 수수료수익(C)으로 기간 경과분 환입 인식.", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", - "T_MERCHANT", - "T_AMOUNT" + "T_MERCHANT" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9101,10 +13267,19 @@ const SVC_SCREENS = [ "svc": "LG_ROLLFWD", "desc": "전 계정 잔액 익일 이월(개시잔액 롤포워드).", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ - "T_COUNT" + "T_RC", + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9113,11 +13288,20 @@ const SVC_SCREENS = [ "svc": "LG_ROLLTB", "desc": "누적 시산표: 마감일까지 계정별 누계 차대를 순회해 시산표 upsert 및 총차액 검증.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", - "T_DELTA" + "T_DELTA", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9126,12 +13310,21 @@ const SVC_SCREENS = [ "svc": "LG_SEQAUDIT", "desc": "전표 채번 감사: 당일 전표번호 min/max/건수로 채번 누락(gap)을 검출.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", "T_ARG1", - "T_DELTA" + "T_DELTA", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9140,12 +13333,21 @@ const SVC_SCREENS = [ "svc": "LG_SETTLEPAYGAP", "desc": "정산-지급 차이: 정산 net 합계와 원장 지급(MERCH_PAY 대변) 합계의 갭을 산출.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_AMT2", "T_AMT3", - "T_DELTA" + "T_DELTA", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9154,13 +13356,21 @@ const SVC_SCREENS = [ "svc": "LG_SETTLEPOST", "desc": "정산 전기: 차변 MERCH_PAY, 대변 CASH (net).", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", "T_MERCHANT", "T_NET", "T_PURCHASE_ID" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9169,13 +13379,22 @@ const SVC_SCREENS = [ "svc": "LG_SETTLERECON", "desc": "원장-정산 대사: 당일 정산(settlement) 순액 합계와 원장 MERCH_PAY 대변", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_NET", "T_AMT3", "T_DELTA", - "T_COUNT" + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9184,14 +13403,23 @@ const SVC_SCREENS = [ "svc": "LG_SPANTB", "desc": "기간 시산표: 계정의 기간 차변/대변 합계와 차액을 산출해 시산표에 upsert.", "in": [ - "T_KEY1", + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", + "T_MERCHANT", + "T_KEY1", "T_STR1" ], "out": [ + "T_RC", "T_AMT2", "T_AMT3", - "T_DELTA" + "T_DELTA", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9200,11 +13428,20 @@ const SVC_SCREENS = [ "svc": "LG_STATACCT", "desc": "원장 통계 세분(계정): 당일 사용된 계정과목 수와 잔액 절대값이 가장 큰", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", - "T_AMT2" + "T_AMT2", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9213,16 +13450,25 @@ const SVC_SCREENS = [ "svc": "LG_STATDC", "desc": "원장 통계 세분(차대변): 당일 차변/대변 각각의 건수·합계·최대금액을 구하고", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", "T_AMT2", "T_AMT3", "T_ARG3", "T_ARG4", - "T_AMT4" + "T_AMT4", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9231,13 +13477,22 @@ const SVC_SCREENS = [ "svc": "LG_STATMERCH", "desc": "원장 통계 세분(가맹점): 특정 가맹점의 원장 라인 건수, 최초/최종 영업일,", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", "T_AMT2", "T_AMT3", - "T_DELTA" + "T_DELTA", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9246,11 +13501,20 @@ const SVC_SCREENS = [ "svc": "LG_STATS", "desc": "당일 전표/원장 통계(전표수, 총차변).", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", - "T_GROSS" + "T_GROSS", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9259,12 +13523,21 @@ const SVC_SCREENS = [ "svc": "LG_STATTYPE", "desc": "원장 통계 세분(전표유형): 당일 전표를 유형(voucher_type)별로 집계해", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_ARG1", "T_COUNT", - "T_ARG2" + "T_ARG2", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9274,9 +13547,19 @@ const SVC_SCREENS = [ "desc": "전표 상태 직접 변경(setter).", "in": [ "T_ID1", + "T_AMOUNT", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT", "T_STATUS" ], - "out": [] + "out": [ + "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" + ] }, { "mod": "lg", @@ -9284,12 +13567,21 @@ const SVC_SCREENS = [ "svc": "LG_SUSPENSE", "desc": "가지급(SUSP) 정리 분개: 당일 SUSP 계정 순액(차-대)을 0 으로 만드는 반대", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_DELTA", "T_ID1", - "T_AMT2" + "T_AMT2", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9298,12 +13590,21 @@ const SVC_SCREENS = [ "svc": "LG_TBDIFF", "desc": "시산표 차액 조회: 당일 시산표(lg_trial_bal)에서 불균형(diff<>0) 계정 수,", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", "T_GROSS", - "T_DELTA" + "T_DELTA", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9312,14 +13613,23 @@ const SVC_SCREENS = [ "svc": "LG_THREEWAY", "desc": "원장-정산-매입 3단 대사: 원장 지급대변, 정산 net, 매입 net 을 대조해 차이 산출.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_AMT2", "T_AMT3", "T_AMT4", "T_DELTA", - "T_NET" + "T_NET", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9328,14 +13638,21 @@ const SVC_SCREENS = [ "svc": "LG_TRANSFER", "desc": "계정간 대체: 출금계정 대변, 입금계정 차변(균형).", "in": [ - "T_KEY1", - "T_KEY2", + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", "T_MERCHANT", - "T_AMOUNT" + "T_KEY1", + "T_KEY2" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9344,13 +13661,22 @@ const SVC_SCREENS = [ "svc": "LG_TRIALBAL", "desc": "계정별 시산표 산출 후 lg_trial_bal upsert.", "in": [ - "T_KEY1", - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT", + "T_KEY1" ], "out": [ + "T_RC", "T_AMT2", "T_AMT3", - "T_DELTA" + "T_DELTA", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9359,10 +13685,19 @@ const SVC_SCREENS = [ "svc": "LG_UNAPPROVE", "desc": "승인 취소: 마감(S)되지 않은 전기(P) 전표를 작성(D) 상태로 되돌림.", "in": [ - "T_ID1" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9371,12 +13706,19 @@ const SVC_SCREENS = [ "svc": "LG_UNEARNPOST", "desc": "선수수익 계상: 현금(D)/선수수익(C)으로 선수금 수령 시점 부채 계상.", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", - "T_MERCHANT", - "T_AMOUNT" + "T_MERCHANT" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9385,13 +13727,21 @@ const SVC_SCREENS = [ "svc": "LG_UNEARNRECOG", "desc": "선수수익 인식: 선수수익 총액을 기간(N)으로 나눠 1기분을 수익 인식.", "in": [ - "T_BIZDATE", "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT", "T_COUNT" ], "out": [ + "T_RC", "T_ID1", - "T_NET" + "T_NET", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9400,10 +13750,19 @@ const SVC_SCREENS = [ "svc": "LG_UNSETTLED", "desc": "미마감(status=posted) 원장 라인 건수.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ - "T_COUNT" + "T_RC", + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9412,10 +13771,19 @@ const SVC_SCREENS = [ "svc": "LG_VOIDVOUCHER", "desc": "전표 취소: 마감연계(S) 상태가 아닌 전표를 X 처리하고 소속 라인 전부를", "in": [ - "T_ID1" + "T_ID1", + "T_AMOUNT", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ - "T_COUNT" + "T_RC", + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9425,12 +13793,21 @@ const SVC_SCREENS = [ "desc": "빈 전표 헤더 생성(작성 상태).", "in": [ "T_PURCHASE_ID", - "T_STR2", + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", + "T_MERCHANT", + "T_STR2", "T_MSG" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9439,14 +13816,22 @@ const SVC_SCREENS = [ "svc": "LG_VOUCHTRACE", "desc": "전표 추적: 전표(T_ID1)의 헤더(유형/상태/연계 ID)와 라인 통계(건수, 차변합,", "in": [ - "T_ID1" + "T_ID1", + "T_AMOUNT", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_ID2", "T_COUNT", "T_AMT2", "T_AMT3", - "T_RC" + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9455,11 +13840,20 @@ const SVC_SCREENS = [ "svc": "LG_WEEKCLOSE", "desc": "주차 결산: 지정 주(기간) 내 작성(D) 전표를 일괄 전기(P)로 승격.", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", + "T_MERCHANT", "T_STR1" ], "out": [ - "T_COUNT" + "T_RC", + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9468,12 +13862,19 @@ const SVC_SCREENS = [ "svc": "LG_WRITEOFF", "desc": "대손 상각: 회수불능 채권을 대손충당금(D)/미수금(C)으로 제각.", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", - "T_MERCHANT", - "T_AMOUNT" + "T_MERCHANT" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9482,13 +13883,21 @@ const SVC_SCREENS = [ "svc": "LG_XFERPOST", "desc": "대체 전기: 두 계정 간 대체(D 대상/C 원천) 전기 후 양 계정 잔액을 재계산.", "in": [ - "T_KEY1", - "T_KEY2", + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", - "T_AMOUNT" + "T_MERCHANT", + "T_KEY1", + "T_KEY2" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9497,11 +13906,20 @@ const SVC_SCREENS = [ "svc": "LG_YEARCARRY", "desc": "연말 이월: 마감일까지 계정별 누계잔액을 익년 개시일 계정잔액으로 이월(upsert).", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", + "T_MERCHANT", "T_STR1" ], "out": [ - "T_COUNT" + "T_RC", + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9510,11 +13928,20 @@ const SVC_SCREENS = [ "svc": "LG_YEARCLOSE", "desc": "연말 결산 대체: 손익계정(_INC/_EXP) 누계 잔액을 계정별로 RETAINED 로 대체.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", - "T_NET" + "T_NET", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9523,11 +13950,20 @@ const SVC_SCREENS = [ "svc": "LG_ZEROLINE", "desc": "0원 분개 검출/정리: 당일 금액 0 인 posted 라인을 검출해 closed 처리하고", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", - "T_ARG1" + "T_ARG1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9536,9 +13972,22 @@ const SVC_SCREENS = [ "svc": "MG_ACK", "desc": "* MG_ACK.pgc - mg 모듈 서비스 MG_ACK (one service = one file).", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ID1" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STATUS" ] }, @@ -9549,14 +13998,23 @@ const SVC_SCREENS = [ "desc": "0220 매입 어드바이스 수신: 응답이 필요 없는 통지 전문이라", "in": [ "T_CHANNEL", - "T_STR3", "T_BIZDATE", - "T_AMOUNT" + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", + "T_STR3" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ID1", - "T_COUNT", - "T_RC" + "T_COUNT" ] }, { @@ -9565,13 +14023,24 @@ const SVC_SCREENS = [ "svc": "MG_AMTCHECK", "desc": "전문 금액/카드 사전검증: 금액 범위(0 < 금액 <= 한도)와", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", "T_AMOUNT", "T_AMT4", "T_STR5" ], "out": [ - "T_AMT4", - "T_RC" + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", + "T_AMT4" ] }, { @@ -9580,15 +14049,25 @@ const SVC_SCREENS = [ "svc": "MG_AUTHADV", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_CHANNEL", + "T_STR1", + "T_STR4", "T_STR3", - "T_BIZDATE", - "T_AMOUNT" + "T_STR5", + "T_AMOUNT", + "T_CHANNEL", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ID1", - "T_ARG1", - "T_RC" + "T_ARG1" ] }, { @@ -9597,12 +14076,24 @@ const SVC_SCREENS = [ "svc": "MG_AUTHADVRSP", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_STR1", + "T_STR4", + "T_STR3", + "T_STR5", + "T_AMOUNT", "T_CHANNEL", "T_BIZDATE", "T_ARG1" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ID1", "T_ID2" ] @@ -9614,11 +14105,21 @@ const SVC_SCREENS = [ "desc": "PAN 앞6/뒤4 만 남기고 '*' 마스킹 (승인요청 로그 저장용). */", "in": [ "T_CHANNEL", - "T_STR3", "T_BIZDATE", - "T_AMOUNT" + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", + "T_STR3" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR2", "T_STR5", "T_ID1", @@ -9633,13 +14134,22 @@ const SVC_SCREENS = [ "in": [ "T_CHANNEL", "T_BIZDATE", - "T_ID1", - "T_AMOUNT" + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", + "T_ID1" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR2", - "T_ID2", - "T_RC" + "T_ID2" ] }, { @@ -9648,11 +14158,23 @@ const SVC_SCREENS = [ "svc": "MG_BALINQ", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_CHANNEL", + "T_STR1", + "T_STR4", "T_STR3", + "T_STR5", + "T_AMOUNT", + "T_CHANNEL", "T_BIZDATE" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR2", "T_STR4", "T_ID1", @@ -9665,10 +14187,23 @@ const SVC_SCREENS = [ "svc": "MG_BITMAP", "desc": "* MG_BITMAP.pgc - mg 모듈 서비스 MG_BITMAP (one service = one file).", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ARG1", "T_ID1" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR6" ] }, @@ -9678,11 +14213,23 @@ const SVC_SCREENS = [ "svc": "MG_BMPMERGE", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_STR1", + "T_STR4", + "T_STR3", + "T_STR5", + "T_AMOUNT", "T_STR6", "T_STR7" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR6", "T_ARG3" ] @@ -9693,10 +14240,22 @@ const SVC_SCREENS = [ "svc": "MG_BMPPARSE", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_STR1", + "T_STR4", + "T_STR3", + "T_STR5", + "T_AMOUNT", "T_STR6" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR3", "T_COUNT" ] @@ -9709,10 +14268,20 @@ const SVC_SCREENS = [ "in": [ "T_CHANNEL", "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ARG1" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ID1", "T_ID2" ] @@ -9724,13 +14293,22 @@ const SVC_SCREENS = [ "desc": "0200 매입요청 전문 처리: MDR(bps) 수수료/정산액을 산출해", "in": [ "T_CHANNEL", - "T_STR3", "T_BIZDATE", + "T_STR1", + "T_MERCHANT", "T_AMOUNT", + "T_STR3", "T_ARG2" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_FEE", "T_NET", "T_ID1" @@ -9742,11 +14320,23 @@ const SVC_SCREENS = [ "svc": "MG_CARDVER", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_CHANNEL", + "T_STR1", + "T_STR4", + "T_STR3", "T_STR5", + "T_AMOUNT", + "T_CHANNEL", "T_BIZDATE" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR2", "T_STR6", "T_COUNT", @@ -9759,10 +14349,21 @@ const SVC_SCREENS = [ "svc": "MG_CHANAUTH", "desc": "채널 인증: UP/SIGNON 상태만 통과. */", "in": [ - "T_CHANNEL" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STATUS" ] }, @@ -9772,10 +14373,21 @@ const SVC_SCREENS = [ "svc": "MG_CHANCLOSE", "desc": "채널 폐쇄: 채널을 DOWN 으로 내리고 세션키를 폐기하되,", "in": [ - "T_CHANNEL" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STATUS", "T_COUNT" ] @@ -9787,10 +14399,20 @@ const SVC_SCREENS = [ "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ "T_CHANNEL", - "T_STR1" + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STATUS", "T_STR5" ] @@ -9802,14 +14424,22 @@ const SVC_SCREENS = [ "desc": "채널 종합 건강점검: 상태(UP 여부) + 당일 오류율(bps) +", "in": [ "T_CHANNEL", - "T_BIZDATE" + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ "T_RC", - "T_STATUS", - "T_ARG3", + "T_MSG", + "T_ARG6", "T_ARG7", - "T_ARG8" + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", + "T_STATUS", + "T_ARG3" ] }, { @@ -9817,12 +14447,25 @@ const SVC_SCREENS = [ "modnm": "전문게이트웨이", "svc": "MG_CHANLIST", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", - "in": [], + "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" + ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR3", "T_ARG2", - "T_ARG3", - "T_RC" + "T_ARG3" ] }, { @@ -9831,10 +14474,21 @@ const SVC_SCREENS = [ "svc": "MG_CHANMAP", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_STR1" + "T_STR1", + "T_STR4", + "T_STR3", + "T_STR5", + "T_AMOUNT" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_CHANNEL", "T_STR5" ] @@ -9845,10 +14499,21 @@ const SVC_SCREENS = [ "svc": "MG_CHANOPEN", "desc": "채널 개통: DOWN 채널만 UP 으로 올리고 sign-on 시각을 기록.", "in": [ - "T_CHANNEL" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STATUS", "T_STR5" ] @@ -9859,10 +14524,21 @@ const SVC_SCREENS = [ "svc": "MG_CHANPING", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_CHANNEL" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STATUS" ] }, @@ -9872,10 +14548,21 @@ const SVC_SCREENS = [ "svc": "MG_CHANRESET", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_CHANNEL" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR5", "T_STATUS" ] @@ -9886,9 +14573,21 @@ const SVC_SCREENS = [ "svc": "MG_CHANSTAT", "desc": "* MG_CHANSTAT.pgc - mg 모듈 서비스 MG_CHANSTAT (one service = one file).", "in": [ - "T_CHANNEL" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STATUS" ] }, @@ -9899,10 +14598,21 @@ const SVC_SCREENS = [ "desc": "채널 절체: 주채널을 DOWN, 예비채널을 UP 으로 한 트랜잭션", "in": [ "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_STR2" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR5", "T_STR2", "T_COUNT" @@ -9914,9 +14624,22 @@ const SVC_SCREENS = [ "svc": "MG_CONFIRM", "desc": "* MG_CONFIRM.pgc - mg 모듈 서비스 MG_CONFIRM (one service = one file).", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ID1" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STATUS" ] }, @@ -9927,12 +14650,21 @@ const SVC_SCREENS = [ "desc": "* MG_CONVERT.pgc - mg 모듈 서비스 MG_CONVERT (one service = one file).", "in": [ "T_CHANNEL", - "T_STR1", - "T_STR3", "T_BIZDATE", - "T_AMOUNT" + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", + "T_STR3" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR1", "T_ID1" ] @@ -9943,10 +14675,21 @@ const SVC_SCREENS = [ "svc": "MG_DECODE", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_STR3" + "T_STR1", + "T_STR4", + "T_STR3", + "T_STR5", + "T_AMOUNT" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR1", "T_STR4", "T_AMOUNT", @@ -9959,8 +14702,22 @@ const SVC_SCREENS = [ "modnm": "전문게이트웨이", "svc": "MG_DEQUEUE", "desc": "* MG_DEQUEUE.pgc - mg 모듈 서비스 MG_DEQUEUE (one service = one file).", - "in": [], + "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" + ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT", "T_ID2", "T_ARG1", @@ -9974,10 +14731,22 @@ const SVC_SCREENS = [ "svc": "MG_DIRECTADPT", "desc": "구간 [s, s+n) 이 전부 십진 숫자인지 검사 (직연동 전문 필드 검증). */", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_STR3" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR1", "T_AMOUNT", "T_CHANNEL", @@ -9990,11 +14759,22 @@ const SVC_SCREENS = [ "svc": "MG_DUPCHK", "desc": "중복전문 검출: 동일 STAN 이 당일 원장에 이미 몇 건인지 세어", "in": [ + "T_CHANNEL", "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ARG1" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT", "T_ARG2" ] @@ -10005,11 +14785,22 @@ const SVC_SCREENS = [ "svc": "MG_DUPPURGE", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_CHANNEL", "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ARG1" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT", "T_ID1" ] @@ -10021,9 +14812,20 @@ const SVC_SCREENS = [ "desc": "* MG_ECHO.pgc - mg 모듈 서비스 MG_ECHO (one service = one file).", "in": [ "T_CHANNEL", - "T_BIZDATE" + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ID1", "T_STATUS" ] @@ -10036,14 +14838,23 @@ const SVC_SCREENS = [ "in": [ "T_STR1", "T_STR4", - "T_CHANNEL", + "T_STR3", + "T_STR5", "T_AMOUNT", + "T_CHANNEL", "T_ARG1" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR3", - "T_COUNT", - "T_RC" + "T_COUNT" ] }, { @@ -10052,10 +14863,22 @@ const SVC_SCREENS = [ "svc": "MG_ERRCLASS", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_STR1", + "T_STR4", + "T_STR3", + "T_STR5", + "T_AMOUNT", "T_STR2" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR5", "T_ARG3" ] @@ -10066,13 +14889,24 @@ const SVC_SCREENS = [ "svc": "MG_ERRESCAL", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_CHANNEL", "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ARG4" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT", - "T_ARG4", - "T_RC" + "T_ARG4" ] }, { @@ -10081,14 +14915,25 @@ const SVC_SCREENS = [ "svc": "MG_ERRLIST", "desc": "오류전문 대기목록 요약: 당일 NACK(N) 전문의 건수, 금액합,", "in": [ - "T_BIZDATE" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT", "T_GROSS", "T_ID1", - "T_ID2", - "T_RC" + "T_ID2" ] }, { @@ -10098,12 +14943,22 @@ const SVC_SCREENS = [ "desc": "* MG_ERRMSG.pgc - mg 모듈 서비스 MG_ERRMSG (one service = one file).", "in": [ "T_CHANNEL", - "T_STR1", - "T_STR3", "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", + "T_STR3", "T_RC" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ID1" ] }, @@ -10113,11 +14968,22 @@ const SVC_SCREENS = [ "svc": "MG_ERRPURGE", "desc": "오류전문 폐기: 재처리를 포기한 당일 NACK(N) 전문 중", "in": [ + "T_CHANNEL", "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_STR2" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT" ] }, @@ -10127,11 +14993,22 @@ const SVC_SCREENS = [ "svc": "MG_ERRREQUEUE", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_CHANNEL", "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ID1" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ID2", "T_ARG1" ] @@ -10142,10 +15019,22 @@ const SVC_SCREENS = [ "svc": "MG_ERRRETRY", "desc": "오류전문 단건 재처리: NACK(N) 된 전문을 rc=00/R 로 리셋하고", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ID1" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ID2" ] }, @@ -10155,13 +15044,24 @@ const SVC_SCREENS = [ "svc": "MG_FEP", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_STR3" + "T_STR1", + "T_STR4", + "T_STR3", + "T_STR5", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR3", "T_COUNT", - "T_ARG2", - "T_RC" + "T_ARG2" ] }, { @@ -10170,12 +15070,23 @@ const SVC_SCREENS = [ "svc": "MG_FIELDGET", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_STR1", + "T_STR4", "T_STR3", + "T_STR5", + "T_AMOUNT", "T_ARG1", "T_ARG2" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR5", "T_COUNT" ] @@ -10186,12 +15097,22 @@ const SVC_SCREENS = [ "svc": "MG_FIELDSET", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_STR1", + "T_STR4", "T_STR3", "T_STR5", + "T_AMOUNT", "T_ARG1" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR3", "T_COUNT", "T_ARG2" @@ -10203,16 +15124,26 @@ const SVC_SCREENS = [ "svc": "MG_FINADV", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_CHANNEL", + "T_STR1", + "T_STR4", "T_STR3", - "T_BIZDATE", - "T_AMOUNT" + "T_STR5", + "T_AMOUNT", + "T_CHANNEL", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR4", "T_ID1", - "T_ARG1", - "T_RC" + "T_ARG1" ] }, { @@ -10221,12 +15152,24 @@ const SVC_SCREENS = [ "svc": "MG_FINADVRSP", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_STR1", + "T_STR4", + "T_STR3", + "T_STR5", + "T_AMOUNT", "T_CHANNEL", "T_BIZDATE", "T_ARG1" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR5", "T_ID1" ] @@ -10237,12 +15180,23 @@ const SVC_SCREENS = [ "svc": "MG_FINREQ", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_CHANNEL", + "T_STR1", + "T_STR4", "T_STR3", - "T_BIZDATE", - "T_AMOUNT" + "T_STR5", + "T_AMOUNT", + "T_CHANNEL", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR2", "T_ID1", "T_ARG1", @@ -10256,16 +15210,27 @@ const SVC_SCREENS = [ "svc": "MG_FINRSP", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_STR1", + "T_STR4", + "T_STR3", + "T_STR5", + "T_AMOUNT", "T_CHANNEL", "T_BIZDATE", - "T_AMOUNT", "T_ARG2", "T_ARG1" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR2", - "T_ID1", - "T_RC" + "T_ID1" ] }, { @@ -10274,10 +15239,22 @@ const SVC_SCREENS = [ "svc": "MG_FROMISO", "desc": "ISO -> 내부전문 변환: 고정길이 ISO 전문을 잘라", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_STR3" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR3", "T_STR1", "T_STR4", @@ -10291,14 +15268,24 @@ const SVC_SCREENS = [ "svc": "MG_GAPSCAN", "desc": "STAN 순번 갭 검출: 당일 최소~최대 순번 구간 폭에서 실제", "in": [ - "T_BIZDATE" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ARG1", "T_ARG2", - "T_COUNT", - "T_ARG6", - "T_RC" + "T_COUNT" ] }, { @@ -10306,10 +15293,23 @@ const SVC_SCREENS = [ "modnm": "전문게이트웨이", "svc": "MG_HEALTH", "desc": "게이트웨이 헬스: 활성 채널 수 조회. */", - "in": [], + "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" + ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", + "T_COUNT" ] }, { @@ -10318,15 +15318,25 @@ const SVC_SCREENS = [ "svc": "MG_HOSTADPT", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_CHANNEL", "T_STR1", - "T_STR3" - ], - "out": [ + "T_STR4", "T_STR3", "T_STR5", - "T_COUNT", - "T_RC" + "T_AMOUNT", + "T_CHANNEL" + ], + "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", + "T_STR3", + "T_STR5", + "T_COUNT" ] }, { @@ -10337,12 +15347,22 @@ const SVC_SCREENS = [ "in": [ "T_CHANNEL", "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ARG4" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT", - "T_ARG4", - "T_RC" + "T_ARG4" ] }, { @@ -10351,15 +15371,27 @@ const SVC_SCREENS = [ "svc": "MG_INQRSP", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_STR1", + "T_STR4", + "T_STR3", + "T_STR5", + "T_AMOUNT", "T_CHANNEL", "T_BIZDATE", "T_AMT1", "T_ARG1" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR3", - "T_ID1", - "T_RC" + "T_ID1" ] }, { @@ -10369,11 +15401,21 @@ const SVC_SCREENS = [ "desc": "* MG_ISO8583.pgc - mg 모듈 서비스 MG_ISO8583 (one service = one file).", "in": [ "T_CHANNEL", - "T_STR3", "T_BIZDATE", - "T_AMOUNT" + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", + "T_STR3" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR1", "T_STR4", "T_AMOUNT", @@ -10386,10 +15428,21 @@ const SVC_SCREENS = [ "svc": "MG_ISOADPT", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_STR3" + "T_STR1", + "T_STR4", + "T_STR3", + "T_STR5", + "T_AMOUNT" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR1", "T_STR5", "T_CHANNEL" @@ -10402,10 +15455,21 @@ const SVC_SCREENS = [ "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_KEY2" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_KEY1", "T_ARG3" ] @@ -10417,9 +15481,21 @@ const SVC_SCREENS = [ "desc": "키 교환: 키버전 채번 + 세션키 갱신. */", "in": [ "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ARG2" ] }, @@ -10429,13 +15505,24 @@ const SVC_SCREENS = [ "svc": "MG_KEYEXPIRE", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ARG3", "T_ARG4" ], "out": [ - "T_ARG1", + "T_RC", + "T_MSG", "T_ARG6", - "T_RC" + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", + "T_ARG1" ] }, { @@ -10444,12 +15531,23 @@ const SVC_SCREENS = [ "svc": "MG_KEYLOAD", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_CHANNEL" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_KEY1", - "T_ARG3", - "T_RC" + "T_ARG3" ] }, { @@ -10458,9 +15556,21 @@ const SVC_SCREENS = [ "svc": "MG_KEYROTATE", "desc": "키 전환(교체): KEYVER 카운터를 올려 새 세대키 번호를 따고,", "in": [ - "T_CHANNEL" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_KEY1", "T_ARG3", "T_ARG2" @@ -10472,10 +15582,21 @@ const SVC_SCREENS = [ "svc": "MG_KEYSTATUS", "desc": "키 상태 조회: 현재 키버전(KEYVER)과 채널의 세션키/사인온", "in": [ - "T_CHANNEL" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_KEY1", "T_ARG3", "T_ARG4" @@ -10487,10 +15608,21 @@ const SVC_SCREENS = [ "svc": "MG_LOADBAL", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_BIZDATE" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_CHANNEL", "T_COUNT", "T_ARG3" @@ -10503,9 +15635,20 @@ const SVC_SCREENS = [ "desc": "* MG_LOG.pgc - mg 모듈 서비스 MG_LOG (one service = one file).", "in": [ "T_CHANNEL", - "T_BIZDATE" + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT" ] }, @@ -10515,10 +15658,23 @@ const SVC_SCREENS = [ "svc": "MG_MAC", "desc": "simple additive 4-hex MAC/checksum over a buffer (전문 무결성 데모). */", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_STR3", "T_ID1" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_KEY1" ] }, @@ -10528,10 +15684,22 @@ const SVC_SCREENS = [ "svc": "MG_MACGEN", "desc": "XOR-회전 8-hex MAC: 키버전을 시드로 섞어 세대별로 값이 달라진다. */", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_STR3" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_KEY1", "T_ARG3" ] @@ -10543,10 +15711,21 @@ const SVC_SCREENS = [ "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_STR3" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_KEY1" ] }, @@ -10556,10 +15735,22 @@ const SVC_SCREENS = [ "svc": "MG_MACVERIFY", "desc": "MG_MAC 과 동일 계열의 가산 체크섬 (검증측 독립 구현, 4-hex). */", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ID1" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_KEY2" ] }, @@ -10569,10 +15760,23 @@ const SVC_SCREENS = [ "svc": "MG_NACK", "desc": "* MG_NACK.pgc - mg 모듈 서비스 MG_NACK (one service = one file).", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ID1", "T_RC" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STATUS" ] }, @@ -10583,10 +15787,21 @@ const SVC_SCREENS = [ "desc": "0800 망관리 전문 라우터: 처리코드 001=사인온(UP),", "in": [ "T_CHANNEL", - "T_STR4", - "T_BIZDATE" + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", + "T_STR4" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR1", "T_STR2", "T_ID1" @@ -10598,16 +15813,27 @@ const SVC_SCREENS = [ "svc": "MG_NMADVICE", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_CHANNEL", + "T_STR1", + "T_STR4", "T_STR3", + "T_STR5", + "T_AMOUNT", + "T_CHANNEL", "T_BIZDATE" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_KEY1", "T_STR4", "T_ID1", - "T_ARG3", - "T_RC" + "T_ARG3" ] }, { @@ -10616,14 +15842,25 @@ const SVC_SCREENS = [ "svc": "MG_NMRSP", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_CHANNEL", + "T_STR1", + "T_STR4", "T_STR3", + "T_STR5", + "T_AMOUNT", + "T_CHANNEL", "T_BIZDATE" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR4", - "T_ID1", - "T_RC" + "T_ID1" ] }, { @@ -10632,11 +15869,22 @@ const SVC_SCREENS = [ "svc": "MG_PACK", "desc": "고정길이 전문 조립 (순수 계산): MTI(4)+처리코드(6)+금액(12). */", "in": [ + "T_CHANNEL", + "T_BIZDATE", "T_STR1", - "T_STR4", - "T_AMOUNT" + "T_MERCHANT", + "T_AMOUNT", + "T_STR4" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR3", "T_COUNT" ] @@ -10647,11 +15895,23 @@ const SVC_SCREENS = [ "svc": "MG_PANMASK", "desc": "BIN(앞6)/뒤4 보존 마스킹. 12자리 미만 짧은 PAN 은 전체 마스킹. */", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_STR5", "T_ID1" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR5" ] }, @@ -10661,9 +15921,22 @@ const SVC_SCREENS = [ "svc": "MG_PARSE", "desc": "* MG_PARSE.pgc - mg 모듈 서비스 MG_PARSE (one service = one file).", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_STR3" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR1", "T_STR4", "T_COUNT" @@ -10675,11 +15948,22 @@ const SVC_SCREENS = [ "svc": "MG_PINBLOCK", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_KEY1", - "T_STR5" + "T_STR1", + "T_STR4", + "T_STR3", + "T_STR5", + "T_AMOUNT", + "T_KEY1" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_KEY2", "T_COUNT" ] @@ -10690,13 +15974,24 @@ const SVC_SCREENS = [ "svc": "MG_PREAUTH", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_CHANNEL", + "T_STR1", + "T_STR4", "T_STR3", - "T_BIZDATE", + "T_STR5", "T_AMOUNT", + "T_CHANNEL", + "T_BIZDATE", "T_ARG4" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_AMT1", "T_ARG4", "T_ID1", @@ -10709,15 +16004,25 @@ const SVC_SCREENS = [ "svc": "MG_QDEPTH", "desc": "송수신 큐 깊이 조회: 대기(Q)/송신(S)/완료(D)/실패(F) 상태별", "in": [ - "T_BIZDATE" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ARG1", "T_ARG2", "T_ARG3", - "T_ARG4", - "T_ARG7", - "T_RC" + "T_ARG4" ] }, { @@ -10727,12 +16032,22 @@ const SVC_SCREENS = [ "desc": "* MG_QUEUE.pgc - mg 모듈 서비스 MG_QUEUE (one service = one file).", "in": [ "T_CHANNEL", - "T_STR1", - "T_STR3", "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", + "T_STR3", "T_ARG1" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ID2", "T_ARG1" ] @@ -10744,13 +16059,23 @@ const SVC_SCREENS = [ "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ARG4" ], "out": [ - "T_COUNT", - "T_ARG4", + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", "T_ARG8", - "T_RC" + "T_AMT2", + "T_AMT3", + "T_STR8", + "T_COUNT", + "T_ARG4" ] }, { @@ -10759,14 +16084,24 @@ const SVC_SCREENS = [ "svc": "MG_READYZ", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_BIZDATE" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ARG1", "T_ARG2", - "T_ARG3", - "T_ARG8", - "T_RC" + "T_ARG3" ] }, { @@ -10775,10 +16110,22 @@ const SVC_SCREENS = [ "svc": "MG_REASM", "desc": "분할전문 재조립: 세그먼트 T_STR5~T_STR8 (최대 4편) 을 순서대로", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ARG1" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR3", "T_COUNT", "T_ARG2" @@ -10791,13 +16138,22 @@ const SVC_SCREENS = [ "desc": "* MG_RECV.pgc - mg 모듈 서비스 MG_RECV (one service = one file).", "in": [ "T_CHANNEL", - "T_STR1", - "T_STR3", - "T_STR5", "T_BIZDATE", - "T_AMOUNT" + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", + "T_STR3", + "T_STR5" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ARG1", "T_ID1" ] @@ -10808,10 +16164,21 @@ const SVC_SCREENS = [ "svc": "MG_RELAYADPT", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_STR3" + "T_STR1", + "T_STR4", + "T_STR3", + "T_STR5", + "T_AMOUNT" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_CHANNEL", "T_STR3", "T_STR5", @@ -10824,13 +16191,23 @@ const SVC_SCREENS = [ "svc": "MG_RESEND", "desc": "전문 재전송: STAN 으로 원전문 찾아 큐에 적재. */", "in": [ - "T_ARG1", "T_CHANNEL", + "T_BIZDATE", "T_STR1", - "T_STR3", - "T_BIZDATE" + "T_MERCHANT", + "T_AMOUNT", + "T_ARG1", + "T_STR3" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ID2" ] }, @@ -10840,11 +16217,23 @@ const SVC_SCREENS = [ "svc": "MG_RESNDONE", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ID2", "T_ARG4" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT" ] }, @@ -10854,9 +16243,22 @@ const SVC_SCREENS = [ "svc": "MG_RETRY", "desc": "큐 재시도: retry_cnt +1, status=Q. */", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ID2" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT" ] }, @@ -10868,11 +16270,21 @@ const SVC_SCREENS = [ "in": [ "T_CHANNEL", "T_BIZDATE", - "T_STR3", + "T_STR1", + "T_MERCHANT", "T_AMOUNT", + "T_STR3", "T_ARG1" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ID1", "T_ARG1" ] @@ -10883,12 +16295,24 @@ const SVC_SCREENS = [ "svc": "MG_REVADVRSP", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_STR1", + "T_STR4", + "T_STR3", + "T_STR5", + "T_AMOUNT", "T_CHANNEL", "T_BIZDATE", "T_ARG1" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ID1", "T_ARG2" ] @@ -10901,11 +16325,20 @@ const SVC_SCREENS = [ "in": [ "T_CHANNEL", "T_BIZDATE", - "T_ARG1", - "T_AMOUNT" + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ID1", "T_ID2" ] @@ -10916,13 +16349,24 @@ const SVC_SCREENS = [ "svc": "MG_REVRSP", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_STR1", + "T_STR4", + "T_STR3", + "T_STR5", + "T_AMOUNT", "T_CHANNEL", "T_BIZDATE", - "T_AMOUNT", "T_ARG1" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR2", "T_ID1" ] @@ -10934,10 +16378,20 @@ const SVC_SCREENS = [ "desc": "* MG_ROUTE.pgc - mg 모듈 서비스 MG_ROUTE (one service = one file).", "in": [ "T_CHANNEL", - "T_STR1" + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR3" ] }, @@ -10948,13 +16402,22 @@ const SVC_SCREENS = [ "desc": "라우팅 룰 등록: 채널+MTI -> 목적지 룰을 신규 채번해 추가.", "in": [ "T_CHANNEL", - "T_STR1", - "T_STR3", "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", + "T_STR3", "T_ARG4" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ID1" ] }, @@ -10964,13 +16427,24 @@ const SVC_SCREENS = [ "svc": "MG_RTCOUNT", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_CHANNEL" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT", "T_ARG2", - "T_ARG3", - "T_RC" + "T_ARG3" ] }, { @@ -10979,13 +16453,23 @@ const SVC_SCREENS = [ "svc": "MG_RTDISABLE", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_ID1", "T_CHANNEL", - "T_STR1" + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", + "T_ID1" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", + "T_COUNT" ] }, { @@ -10995,10 +16479,20 @@ const SVC_SCREENS = [ "desc": "라우팅 룰 비활성: 채널+MTI 의 활성 룰을 전부 내려서", "in": [ "T_CHANNEL", - "T_STR1" + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT" ] }, @@ -11008,10 +16502,22 @@ const SVC_SCREENS = [ "svc": "MG_RTENABLE", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ID1" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT" ] }, @@ -11022,11 +16528,21 @@ const SVC_SCREENS = [ "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ "T_CHANNEL", - "T_STR1" + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ - "T_COUNT", "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", + "T_COUNT", "T_STR3", "T_ARG4" ] @@ -11037,14 +16553,24 @@ const SVC_SCREENS = [ "svc": "MG_RTLIST", "desc": "라우팅 룰 열람 요약: 채널의 활성/비활성 룰 수와 최우선(최소)", "in": [ - "T_CHANNEL" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR3", "T_ARG4", - "T_COUNT", - "T_ARG7", - "T_RC" + "T_COUNT" ] }, { @@ -11054,11 +16580,21 @@ const SVC_SCREENS = [ "desc": "라우팅 목적지 절체: 채널+MTI 활성 룰의 dest 를 새 목적지로", "in": [ "T_CHANNEL", + "T_BIZDATE", "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_STR3" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR5", "T_COUNT" ] @@ -11070,12 +16606,21 @@ const SVC_SCREENS = [ "desc": "* MG_SEND.pgc - mg 모듈 서비스 MG_SEND (one service = one file).", "in": [ "T_CHANNEL", - "T_STR1", - "T_STR3", "T_BIZDATE", - "T_AMOUNT" + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", + "T_STR3" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ARG1", "T_ID1" ] @@ -11087,13 +16632,22 @@ const SVC_SCREENS = [ "desc": "채널 순번 감사: 채널이 기억하는 last_stan 과 원장의 실제", "in": [ "T_CHANNEL", - "T_BIZDATE" + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ARG1", - "T_ARG2", - "T_ARG6" + "T_ARG2" ] }, { @@ -11104,10 +16658,20 @@ const SVC_SCREENS = [ "in": [ "T_CHANNEL", "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ARG1" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT" ] }, @@ -11118,12 +16682,22 @@ const SVC_SCREENS = [ "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ARG1" ], "out": [ "T_RC", - "T_ARG2", - "T_ARG6" + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", + "T_ARG2" ] }, { @@ -11133,10 +16707,21 @@ const SVC_SCREENS = [ "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ARG1" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ARG2", "T_ARG3" ] @@ -11146,8 +16731,22 @@ const SVC_SCREENS = [ "modnm": "전문게이트웨이", "svc": "MG_SESSION", "desc": "* MG_SESSION.pgc - mg 모듈 서비스 MG_SESSION (one service = one file).", - "in": [], + "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" + ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ARG2" ] }, @@ -11158,9 +16757,20 @@ const SVC_SCREENS = [ "desc": "0500 정산집계 요청 생성: 당일 채널의 수신 0200 건수/금액을", "in": [ "T_CHANNEL", - "T_BIZDATE" + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT", "T_GROSS", "T_ID1", @@ -11175,15 +16785,25 @@ const SVC_SCREENS = [ "in": [ "T_CHANNEL", "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_COUNT", "T_GROSS" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR2", "T_ARG1", "T_AMT1", - "T_ID1", - "T_RC" + "T_ID1" ] }, { @@ -11192,9 +16812,21 @@ const SVC_SCREENS = [ "svc": "MG_SIGNOFF", "desc": "* MG_SIGNOFF.pgc - mg 모듈 서비스 MG_SIGNOFF (one service = one file).", "in": [ - "T_CHANNEL" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STATUS" ] }, @@ -11205,9 +16837,21 @@ const SVC_SCREENS = [ "desc": "* MG_SIGNON.pgc - mg 모듈 서비스 MG_SIGNON (one service = one file).", "in": [ "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STATUS" ] }, @@ -11217,13 +16861,24 @@ const SVC_SCREENS = [ "svc": "MG_STALEMARK", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_CHANNEL", "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ARG4" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT", - "T_ARG5", - "T_RC" + "T_ARG5" ] }, { @@ -11231,8 +16886,22 @@ const SVC_SCREENS = [ "modnm": "전문게이트웨이", "svc": "MG_STAN", "desc": "* MG_STAN.pgc - mg 모듈 서비스 MG_STAN (one service = one file).", - "in": [], + "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" + ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ARG1" ] }, @@ -11242,14 +16911,24 @@ const SVC_SCREENS = [ "svc": "MG_STATAMT", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_CHANNEL", "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_AMT1" ], "out": [ - "T_ARG1", - "T_ARG2", + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", "T_ARG8", - "T_AMT2" + "T_AMT2", + "T_AMT3", + "T_STR8", + "T_ARG1", + "T_ARG2" ] }, { @@ -11259,13 +16938,23 @@ const SVC_SCREENS = [ "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ "T_CHANNEL", - "T_BIZDATE" + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ARG1", "T_ARG2", - "T_GROSS", - "T_ARG8" + "T_GROSS" ] }, { @@ -11274,15 +16963,24 @@ const SVC_SCREENS = [ "svc": "MG_STATDIR", "desc": "송수신 방향별 통계: 당일 IN/OUT 각각의 건수와 금액합,", "in": [ - "T_BIZDATE" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ARG1", "T_AMT1", - "T_ARG2", - "T_AMT2", - "T_ARG8", - "T_RC" + "T_ARG2" ] }, { @@ -11291,14 +16989,24 @@ const SVC_SCREENS = [ "svc": "MG_STATERR", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_CHANNEL", "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ARG4" ], "out": [ - "T_COUNT", - "T_ARG2", + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", "T_ARG8", - "T_RC" + "T_AMT2", + "T_AMT3", + "T_STR8", + "T_COUNT", + "T_ARG2" ] }, { @@ -11307,14 +17015,25 @@ const SVC_SCREENS = [ "svc": "MG_STATHOUR", "desc": "시간대별 통계: 요청 시(0~23)의 당일 건수/금액합과 함께", "in": [ + "T_CHANNEL", "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ARG5" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT", "T_GROSS", - "T_ARG5", - "T_ARG6" + "T_ARG5" ] }, { @@ -11323,14 +17042,24 @@ const SVC_SCREENS = [ "svc": "MG_STATMTI", "desc": "전문유형(MTI)별 당일 통계: 지정 MTI 의 건수/금액합/최대금액과", "in": [ + "T_CHANNEL", + "T_BIZDATE", "T_STR1", - "T_BIZDATE" + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT", "T_GROSS", - "T_AMT1", - "T_ARG8" + "T_AMT1" ] }, { @@ -11339,13 +17068,24 @@ const SVC_SCREENS = [ "svc": "MG_STATPEAK", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_BIZDATE" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ARG1", "T_COUNT", - "T_ARG3", - "T_RC" + "T_ARG3" ] }, { @@ -11354,14 +17094,24 @@ const SVC_SCREENS = [ "svc": "MG_STATRC", "desc": "응답코드 분포 조회: 당일 정상(00)/거절 건수, 고유 거절코드", "in": [ - "T_BIZDATE" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ARG1", "T_ARG2", - "T_ARG7", - "T_STR2", - "T_ARG6" + "T_STR2" ] }, { @@ -11371,9 +17121,20 @@ const SVC_SCREENS = [ "desc": "채널 당일 통계: 수신/송신/오류 건수. */", "in": [ "T_CHANNEL", - "T_BIZDATE" + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ARG1", "T_ARG2", "T_ARG3", @@ -11386,15 +17147,26 @@ const SVC_SCREENS = [ "svc": "MG_STATSUM", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_BIZDATE" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT", "T_GROSS", "T_AMT1", "T_ARG2", - "T_ARG3", - "T_RC" + "T_ARG3" ] }, { @@ -11403,9 +17175,22 @@ const SVC_SCREENS = [ "svc": "MG_STATUS", "desc": "* MG_STATUS.pgc - mg 모듈 서비스 MG_STATUS (one service = one file).", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ID1" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STATUS" ] }, @@ -11417,13 +17202,22 @@ const SVC_SCREENS = [ "in": [ "T_CHANNEL", "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ARG4" ], "out": [ - "T_COUNT", - "T_ARG4", + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", "T_ARG8", - "T_RC" + "T_AMT2", + "T_AMT3", + "T_STR8", + "T_COUNT", + "T_ARG4" ] }, { @@ -11432,9 +17226,21 @@ const SVC_SCREENS = [ "svc": "MG_TIMEOUT", "desc": "전문 타임아웃 스윕: 임계초(TMOUTSEC, 기본 60초)를 넘겨도", "in": [ - "T_BIZDATE" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT", "T_ARG5" ] @@ -11445,11 +17251,23 @@ const SVC_SCREENS = [ "svc": "MG_TIMEOUTSET", "desc": "타임아웃 임계초 설정: 운영자가 지정한 초(5~600 범위 강제)를", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ARG5" ], "out": [ - "T_ARG5", - "T_RC" + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", + "T_ARG5" ] }, { @@ -11458,10 +17276,22 @@ const SVC_SCREENS = [ "svc": "MG_TOISO", "desc": "내부전문 -> ISO 변환: 콜론 구분 내부포맷 \"MTI:처리코드:금액\" 을", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_STR3" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR3", "T_STR6", "T_STR1", @@ -11473,13 +17303,24 @@ const SVC_SCREENS = [ "modnm": "전문게이트웨이", "svc": "MG_TPSCHECK", "desc": "순간 처리율 점검: 최근 60초 유입 전문 수로 근사 TPS(x100)를", - "in": [], + "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" + ], "out": [ - "T_ARG1", - "T_ARG2", - "T_ARG8", + "T_RC", + "T_MSG", "T_ARG6", - "T_RC" + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", + "T_ARG1", + "T_ARG2" ] }, { @@ -11488,9 +17329,22 @@ const SVC_SCREENS = [ "svc": "MG_UNPACK", "desc": "* MG_UNPACK.pgc - mg 모듈 서비스 MG_UNPACK (one service = one file).", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_STR3" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR1", "T_STR4", "T_AMOUNT" @@ -11502,10 +17356,22 @@ const SVC_SCREENS = [ "svc": "MG_VAN1ADPT", "desc": "VAN1 채널 어댑터: 파이프 구분 전문 \"V1|MTI|금액|처리코드\" 를", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_STR3" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR3", "T_STR1", "T_AMOUNT", @@ -11518,10 +17384,22 @@ const SVC_SCREENS = [ "svc": "MG_VAN2ADPT", "desc": "VAN2 채널 어댑터: 키=값 세미콜론 전문 \"M=0200;A=1000;P=000000\"", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_STR3" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR3", "T_STR1", "T_AMOUNT", @@ -11534,10 +17412,21 @@ const SVC_SCREENS = [ "svc": "MG_VAN3ADPT", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_STR3" + "T_STR1", + "T_STR4", + "T_STR3", + "T_STR5", + "T_AMOUNT" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR3", "T_STR1", "T_AMOUNT", @@ -11550,10 +17439,21 @@ const SVC_SCREENS = [ "svc": "MG_VAN4ADPT", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_STR3" + "T_STR1", + "T_STR4", + "T_STR3", + "T_STR5", + "T_AMOUNT" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR3", "T_COUNT", "T_CHANNEL" @@ -11565,7 +17465,8 @@ const SVC_SCREENS = [ "svc": "MM_ACCT_INQ", "desc": "기능: 정산계좌 원문 조회 (등록 여부 판정 포함).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC", @@ -11578,7 +17479,8 @@ const SVC_SCREENS = [ "svc": "MM_ACCT_MASK", "desc": "기능: 정산계좌 마스킹 조회 (앞3 뒤2만 노출, 중간은 * 처리하여 반환 - 개인정보 보호).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC" @@ -11591,7 +17493,8 @@ const SVC_SCREENS = [ "desc": "기능: 정산계좌 등록/변경 (숫자 10~16자리 형식 검증 통과분만 마스터에 반영, 검증 이력 기록).", "in": [ "T_MERCHANT", - "T_STR1" + "T_STR1", + "T_KEY1" ], "out": [ "T_RC" @@ -11603,7 +17506,8 @@ const SVC_SCREENS = [ "svc": "MM_ACCT_VERIFY", "desc": "기능: 정산계좌 검증 실행 (형식 검사 + ACCT_VRF 검증 이력 적재).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC" @@ -11615,7 +17519,8 @@ const SVC_SCREENS = [ "svc": "MM_ACCT_VRF_HIST", "desc": "기능: 정산계좌 검증 이력 조회 (누적 횟수 + 최근 결과/일시).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC", @@ -11627,7 +17532,10 @@ const SVC_SCREENS = [ "modnm": "마스터", "svc": "MM_ACCT_VRF_STAT", "desc": "기능: 정산계좌 검증 전사 통계 (총 검증 수, 검증 대상 가맹점 수, 실패 건수).", - "in": [], + "in": [ + "T_MERCHANT", + "T_KEY1" + ], "out": [ "T_COUNT", "T_ARG1", @@ -11641,7 +17549,8 @@ const SVC_SCREENS = [ "svc": "MM_AGENT_INQ", "desc": "기능: 모집인 단건 조회 (AGENT 코드그룹에서 명칭/상태 반환).", "in": [ - "T_KEY1" + "T_KEY1", + "T_STR1" ], "out": [ "T_RC" @@ -11668,7 +17577,8 @@ const SVC_SCREENS = [ "desc": "기능: 모집인 상태 변경 (ACTIVE/SUSPENDED/RETIRED 허용값 검증 후 갱신).", "in": [ "T_KEY1", - "T_STATUS" + "T_STATUS", + "T_STR1" ], "out": [ "T_RC" @@ -11681,7 +17591,8 @@ const SVC_SCREENS = [ "desc": "기능: 가입 심사 승인 (REVIEW -> ACTIVE, 기본 수수료율/한도 자동 개설).", "in": [ "T_MERCHANT", - "T_ARG1" + "T_ARG1", + "T_KEY1" ], "out": [ "T_RC" @@ -11694,7 +17605,8 @@ const SVC_SCREENS = [ "desc": "기능: 심사 담당자 배정 (담당자 코드를 REVIEWER 매핑 이력에 기록).", "in": [ "T_MERCHANT", - "T_STR1" + "T_STR1", + "T_KEY1" ], "out": [ "T_RC" @@ -11706,7 +17618,9 @@ const SVC_SCREENS = [ "svc": "MM_APPLY_CNT", "desc": "기능: 심사 상태별 신청 건수 집계 (대시보드용, 특정 상태 또는 전 워크플로 상태).", "in": [ - "T_STR1" + "T_STR1", + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_COUNT", @@ -11719,7 +17633,8 @@ const SVC_SCREENS = [ "svc": "MM_APPLY_DOCCHK", "desc": "기능: 심사 서류 접수 확인 (필수 마스터 필드 충족도 점검, 심사 서브단계).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC", @@ -11733,7 +17648,8 @@ const SVC_SCREENS = [ "desc": "기능: 심사 진행 중 신청 건 보류 (APPLIED->HOLD, 사유 이력 기록).", "in": [ "T_MERCHANT", - "T_REASON" + "T_REASON", + "T_KEY1" ], "out": [ "T_RC" @@ -11748,7 +17664,8 @@ const SVC_SCREENS = [ "T_MERCHANT", "T_STR1", "T_STR2", - "T_STR3" + "T_STR3", + "T_KEY1" ], "out": [ "T_RC" @@ -11761,7 +17678,8 @@ const SVC_SCREENS = [ "desc": "기능: 가입 심사 반려 (APPLIED/REVIEW -> REJECTED, 반려 사유 필수).", "in": [ "T_MERCHANT", - "T_REASON" + "T_REASON", + "T_KEY1" ], "out": [ "T_RC" @@ -11773,7 +17691,8 @@ const SVC_SCREENS = [ "svc": "MM_APPLY_RESUME", "desc": "기능: 보류된 심사 재개 (HOLD->APPLIED, 심사 대기열 복귀).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC", @@ -11786,7 +17705,8 @@ const SVC_SCREENS = [ "svc": "MM_APPLY_REVIEW", "desc": "기능: 가입 신청 심사 개시 (APPLIED -> REVIEW, MCC 유효성 사전점검).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC", @@ -11799,7 +17719,8 @@ const SVC_SCREENS = [ "svc": "MM_APPLY_SCORE", "desc": "기능: 심사 점수 산정 (MCC 리스크 + 서류 완비도 가중 합산, 등급 예비판정).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC", @@ -11812,7 +17733,8 @@ const SVC_SCREENS = [ "svc": "MM_APPLY_STATUS", "desc": "기능: 가입 심사 진행 상태 조회 (단계 코드 + 워크플로 이력 건수).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC", @@ -11827,7 +17749,8 @@ const SVC_SCREENS = [ "desc": "기능: 신청 철회 (신청/보류 단계 건을 WITHDRAWN 처리, 활성 가맹점은 철회 불가).", "in": [ "T_MERCHANT", - "T_REASON" + "T_REASON", + "T_KEY1" ], "out": [ "T_RC" @@ -11839,7 +17762,8 @@ const SVC_SCREENS = [ "svc": "MM_AUDIT_DIFF", "desc": "기능: 마스터 최근 변경 전후 비교 (가장 최신 이력 1건의 액션/이전값/이후값 반환).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC" @@ -11851,7 +17775,9 @@ const SVC_SCREENS = [ "svc": "MM_AUDIT_PURGE", "desc": "기능: 감사 이력 보존기한 경과분 삭제 (삭제 내역 자체도 감사 기록으로 남김).", "in": [ - "T_ARG1" + "T_ARG1", + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC", @@ -11864,7 +17790,8 @@ const SVC_SCREENS = [ "svc": "MM_AUDIT_RECENT", "desc": "기능: 가맹점 최근 변경 감사 조회 (최신 이력 1건: 액션/변경값/메모/일자).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC" @@ -11876,7 +17803,9 @@ const SVC_SCREENS = [ "svc": "MM_AUDIT_STAT", "desc": "기능: 전 마스터 감사 통계 (특정 액션의 전체 발생 건수 + 관련 가맹점 수 집계).", "in": [ - "T_STR1" + "T_STR1", + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_COUNT", @@ -11891,7 +17820,8 @@ const SVC_SCREENS = [ "desc": "기능: 가맹점 마스터 변경 감사 추적 (액션 필터별 이력 건수 + 발생 액션 종류 수).", "in": [ "T_MERCHANT", - "T_STR1" + "T_STR1", + "T_KEY1" ], "out": [ "T_COUNT", @@ -11905,7 +17835,9 @@ const SVC_SCREENS = [ "svc": "MM_BIN_INQ", "desc": "기능: BIN 조회.", "in": [ - "T_KEY1" + "T_KEY1", + "T_ISSUER", + "T_CCY" ], "out": [] }, @@ -11917,7 +17849,8 @@ const SVC_SCREENS = [ "in": [ "T_KEY1", "T_ISSUER", - "T_STR5" + "T_STR5", + "T_CCY" ], "out": [] }, @@ -11927,7 +17860,9 @@ const SVC_SCREENS = [ "svc": "MM_BIZNO_CHECK", "desc": "기능: 사업자등록번호 검증 (10자리 형식 + 국세청 검증공식 checksum + 마스터 중복 사용 확인).", "in": [ - "T_STR2" + "T_STR2", + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC", @@ -11941,7 +17876,8 @@ const SVC_SCREENS = [ "desc": "기능: 사업자등록번호 변경 (형식 검사 + 타 가맹점 중복 차단 + 변경 이력).", "in": [ "T_MERCHANT", - "T_STR2" + "T_STR2", + "T_KEY1" ], "out": [ "T_RC" @@ -11954,7 +17890,8 @@ const SVC_SCREENS = [ "desc": "기능: 폐업 확정 처리 (상태 CLOSED, 미결 계약 잔여 여부를 함께 점검하여 경고 코드 반환).", "in": [ "T_MERCHANT", - "T_REASON" + "T_REASON", + "T_KEY1" ], "out": [ "T_RC", @@ -11967,7 +17904,8 @@ const SVC_SCREENS = [ "svc": "MM_BIZ_REOPEN", "desc": "기능: 재개업 처리 (SUSPENDED/DORMANT 가맹점을 ACTIVE로 복귀, 폐업/삭제 건은 불가).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC" @@ -11980,7 +17918,8 @@ const SVC_SCREENS = [ "desc": "기능: 휴업 신고 처리 (ACTIVE 가맹점을 SUSPENDED로, 소속 활성 단말도 함께 정지).", "in": [ "T_MERCHANT", - "T_REASON" + "T_REASON", + "T_KEY1" ], "out": [ "T_RC", @@ -11993,7 +17932,8 @@ const SVC_SCREENS = [ "svc": "MM_BRANCH_INQ", "desc": "기능: 영업점 조회 (\"명칭|상위코드\" 저장값을 명칭만 분리하여 반환).", "in": [ - "T_KEY1" + "T_KEY1", + "T_STR1" ], "out": [ "T_RC" @@ -12004,7 +17944,10 @@ const SVC_SCREENS = [ "modnm": "마스터", "svc": "MM_BRANCH_LIST", "desc": "기능: 영업점 계층 요약 (전체 수 + 최상위(상위=-) 영업점 수 집계).", - "in": [], + "in": [ + "T_KEY1", + "T_STR1" + ], "out": [ "T_COUNT", "T_ARG1", @@ -12030,7 +17973,10 @@ const SVC_SCREENS = [ "modnm": "마스터", "svc": "MM_CHK_ACCT", "desc": "기능: 정합성 점검 - 정산계좌 미등록 활성 가맹점 적발 (정산 지급 리스크 사전 탐지).", - "in": [], + "in": [ + "T_MERCHANT", + "T_KEY1" + ], "out": [ "T_COUNT", "T_ARG1", @@ -12042,7 +17988,10 @@ const SVC_SCREENS = [ "modnm": "마스터", "svc": "MM_CHK_FEERATE", "desc": "기능: 수수료율 마스터 정합성 점검 (활성 가맹점 요율 누락 / bps 범위 이탈 / 고아 요율).", - "in": [], + "in": [ + "T_MERCHANT", + "T_KEY1" + ], "out": [ "T_COUNT", "T_ARG1", @@ -12055,7 +18004,10 @@ const SVC_SCREENS = [ "modnm": "마스터", "svc": "MM_CHK_GRADE", "desc": "기능: 정합성 점검 - 마스터 등급과 누적 매출 기준 산정 등급의 불일치 적발.", - "in": [], + "in": [ + "T_MERCHANT", + "T_KEY1" + ], "out": [ "T_COUNT", "T_ARG1", @@ -12067,7 +18019,10 @@ const SVC_SCREENS = [ "modnm": "마스터", "svc": "MM_CHK_LIMIT", "desc": "기능: 한도 마스터 정합성 점검 (활성 가맹점 한도 누락 + 한도 체계 역전 검출).", - "in": [], + "in": [ + "T_MERCHANT", + "T_KEY1" + ], "out": [ "T_COUNT", "T_ARG1", @@ -12079,7 +18034,10 @@ const SVC_SCREENS = [ "modnm": "마스터", "svc": "MM_CHK_MCC", "desc": "기능: 정합성 점검 - MCC 마스터에 없는 업종코드를 참조하는 가맹점 적발 (외래키 무결성).", - "in": [], + "in": [ + "T_MERCHANT", + "T_KEY1" + ], "out": [ "T_COUNT", "T_ARG1", @@ -12091,7 +18049,10 @@ const SVC_SCREENS = [ "modnm": "마스터", "svc": "MM_CHK_ORPHAN", "desc": "기능: 정합성 점검 - 대표가 비활성/부재인 대표-하위 관계 적발 (MERCH_REL 고아 링크).", - "in": [], + "in": [ + "T_MERCHANT", + "T_KEY1" + ], "out": [ "T_COUNT", "T_ARG1", @@ -12103,7 +18064,10 @@ const SVC_SCREENS = [ "modnm": "마스터", "svc": "MM_CHK_TERM", "desc": "기능: 단말 마스터 정합성 점검 (소속 가맹점 없는 고아 단말 + 폐업 가맹점의 가동 단말).", - "in": [], + "in": [ + "T_MERCHANT", + "T_KEY1" + ], "out": [ "T_COUNT", "T_ARG1", @@ -12157,7 +18121,8 @@ const SVC_SCREENS = [ "svc": "MM_CODE_CACHE", "desc": "기능: 코드그룹의 활성 코드 수 (캐시 워밍 시뮬).", "in": [ - "T_STR1" + "T_STR1", + "T_KEY1" ], "out": [ "T_COUNT" @@ -12192,7 +18157,8 @@ const SVC_SCREENS = [ "svc": "MM_CONFIRM", "desc": "기능: 가맹점 승인 확정 (상태 ACTIVE + 이력).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [] }, @@ -12203,7 +18169,8 @@ const SVC_SCREENS = [ "desc": "기능: 유효 계약의 조건 변경 (만기일 연장/단축, 변경 전후를 이력에 함께 기록).", "in": [ "T_MERCHANT", - "T_ARG1" + "T_ARG1", + "T_KEY1" ], "out": [ "T_RC" @@ -12216,7 +18183,8 @@ const SVC_SCREENS = [ "desc": "기능: 가맹 계약 해지 (CONT_END 이력 + 가맹점 상태 HOLD 전환).", "in": [ "T_MERCHANT", - "T_REASON" + "T_REASON", + "T_KEY1" ], "out": [ "T_RC" @@ -12229,7 +18197,8 @@ const SVC_SCREENS = [ "desc": "기능: 만기 임박 점검 (최신 계약 만기일까지 남은 일수 산출, 갱신 필요 판정).", "in": [ "T_MERCHANT", - "T_ARG1" + "T_ARG1", + "T_KEY1" ], "out": [ "T_RC", @@ -12242,7 +18211,8 @@ const SVC_SCREENS = [ "svc": "MM_CONTRACT_HIST", "desc": "기능: 가맹점 계약 변경 이력 요약 (체결/변경 횟수 + 최신 만기일 반환).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_COUNT", @@ -12255,7 +18225,8 @@ const SVC_SCREENS = [ "svc": "MM_CONTRACT_INQ", "desc": "기능: 가맹 계약 현황 조회 (체결/갱신 횟수, 최종 만기일, 유효 여부).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC", @@ -12270,7 +18241,8 @@ const SVC_SCREENS = [ "desc": "기능: 가맹 계약 체결 (기간 개월수로 만기일 산출, CONTRACT 이력 기록).", "in": [ "T_MERCHANT", - "T_ARG1" + "T_ARG1", + "T_KEY1" ], "out": [ "T_RC" @@ -12282,7 +18254,8 @@ const SVC_SCREENS = [ "svc": "MM_CONTRACT_REACT", "desc": "기능: 정지된 계약 재개 (미해제 CONTRACT_SUSP가 있을 때만 CONTRACT_REACT 기록).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC" @@ -12295,7 +18268,8 @@ const SVC_SCREENS = [ "desc": "기능: 가맹 계약 갱신 (직전 만기일 기준 개월수 연장).", "in": [ "T_MERCHANT", - "T_ARG1" + "T_ARG1", + "T_KEY1" ], "out": [ "T_RC" @@ -12308,7 +18282,8 @@ const SVC_SCREENS = [ "desc": "기능: 계약 일시정지 (활성 가맹점을 유지하되 CONTRACT_SUSP 이력 마킹, 중복 정지 차단).", "in": [ "T_MERCHANT", - "T_REASON" + "T_REASON", + "T_KEY1" ], "out": [ "T_RC" @@ -12320,7 +18295,8 @@ const SVC_SCREENS = [ "svc": "MM_FEERATE_HIST", "desc": "기능: 수수료율 변경 이력 건수 (dbio).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_COUNT" @@ -12333,7 +18309,8 @@ const SVC_SCREENS = [ "desc": "기능: 수수료율 조회 (dbio).", "in": [ "T_MERCHANT", - "T_STR4" + "T_STR4", + "T_KEY1" ], "out": [ "T_ARG1" @@ -12347,7 +18324,8 @@ const SVC_SCREENS = [ "in": [ "T_MERCHANT", "T_STR4", - "T_ARG1" + "T_ARG1", + "T_KEY1" ], "out": [] }, @@ -12358,7 +18336,9 @@ const SVC_SCREENS = [ "desc": "기능: 환율 조회 + 금액 환산 (rate_bps는 100 기준 bps).", "in": [ "T_CCY", - "T_FX_AMT" + "T_FX_AMT", + "T_KEY1", + "T_ISSUER" ], "out": [ "T_ARG2", @@ -12372,7 +18352,9 @@ const SVC_SCREENS = [ "desc": "기능: 환율 마스터 upsert.", "in": [ "T_CCY", - "T_ARG2" + "T_ARG2", + "T_KEY1", + "T_ISSUER" ], "out": [] }, @@ -12384,7 +18366,8 @@ const SVC_SCREENS = [ "in": [ "T_MERCHANT", "T_STR2", - "T_REASON" + "T_REASON", + "T_KEY1" ], "out": [ "T_RC" @@ -12395,7 +18378,10 @@ const SVC_SCREENS = [ "modnm": "마스터", "svc": "MM_GRADE_DIST", "desc": "기능: 가맹점 등급 분포 조회 (최근 등급 통계일 기준 S/A/B/C 세분 현황).", - "in": [], + "in": [ + "T_MERCHANT", + "T_KEY1" + ], "out": [ "T_RC", "T_ARG1", @@ -12411,7 +18397,8 @@ const SVC_SCREENS = [ "svc": "MM_GRADE_HIST", "desc": "기능: 등급 변경 이력 요약 (등급 관련 이력 건수 + 마스터 현행 등급 반환).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC", @@ -12424,7 +18411,8 @@ const SVC_SCREENS = [ "svc": "MM_GRADE_PREVIEW", "desc": "기능: 가맹점 등급 산정 시뮬레이션 (누적 매출로 등급 산출, 마스터는 불변).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC", @@ -12439,7 +18427,10 @@ const SVC_SCREENS = [ "desc": "기능: 공휴일 마스터 등록.", "in": [ "T_BIZDATE", - "T_STR1" + "T_STR1", + "T_KEY1", + "T_ISSUER", + "T_CCY" ], "out": [] }, @@ -12449,7 +18440,9 @@ const SVC_SCREENS = [ "svc": "MM_ISSUER_INQ", "desc": "기능: 발급사 조회.", "in": [ - "T_ISSUER" + "T_ISSUER", + "T_KEY1", + "T_CCY" ], "out": [] }, @@ -12461,7 +18454,9 @@ const SVC_SCREENS = [ "in": [ "T_ISSUER", "T_STR1", - "T_STR2" + "T_STR2", + "T_KEY1", + "T_CCY" ], "out": [] }, @@ -12471,7 +18466,8 @@ const SVC_SCREENS = [ "svc": "MM_LIMIT_INQ", "desc": "기능: 가맹점 한도 조회.", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_AMT1", @@ -12488,7 +18484,8 @@ const SVC_SCREENS = [ "T_MERCHANT", "T_AMT1", "T_AMT2", - "T_AMT3" + "T_AMT3", + "T_KEY1" ], "out": [] }, @@ -12499,7 +18496,9 @@ const SVC_SCREENS = [ "desc": "기능: 가맹점 업종(MCC) 재배정 (마스터에 등록된 MCC로만 변경, 변경 이력 기록).", "in": [ "T_MERCHANT", - "T_STR1" + "T_STR1", + "T_CATEGORY", + "T_KEY1" ], "out": [ "T_RC" @@ -12511,7 +18510,9 @@ const SVC_SCREENS = [ "svc": "MM_MCC_CNT", "desc": "기능: 특정 MCC를 사용하는 가맹점 수 + 해당 MCC 리스크 등급 동시 반환.", "in": [ - "T_STR1" + "T_STR1", + "T_CATEGORY", + "T_KEY1" ], "out": [ "T_RC", @@ -12525,7 +18526,9 @@ const SVC_SCREENS = [ "svc": "MM_MCC_DEL", "desc": "기능: MCC 업종코드 삭제 (참조하는 가맹점이 없을 때만 물리 삭제, 안전 삭제).", "in": [ - "T_STR1" + "T_STR1", + "T_CATEGORY", + "T_KEY1" ], "out": [ "T_RC", @@ -12538,7 +18541,8 @@ const SVC_SCREENS = [ "svc": "MM_MCC_INQ", "desc": "기능: MCC 업종 조회 (명칭/위험도 + 해당 업종 활성 가맹점 수).", "in": [ - "T_CATEGORY" + "T_CATEGORY", + "T_KEY1" ], "out": [ "T_RC", @@ -12554,7 +18558,9 @@ const SVC_SCREENS = [ "in": [ "T_STR6", "T_STR1", - "T_ARG1" + "T_ARG1", + "T_CATEGORY", + "T_KEY1" ], "out": [] }, @@ -12564,7 +18570,9 @@ const SVC_SCREENS = [ "svc": "MM_MCC_RISKLIST", "desc": "기능: 고위험 MCC 현황 (기준 위험도 이상 MCC 수, 최고 위험도, 소속 활성 가맹점 수).", "in": [ - "T_ARG1" + "T_ARG1", + "T_CATEGORY", + "T_KEY1" ], "out": [ "T_COUNT", @@ -12580,7 +18588,9 @@ const SVC_SCREENS = [ "desc": "기능: MCC 리스크 등급 설정 (1~5 범위 검증 후 mm_mcc.risk_level 갱신).", "in": [ "T_STR1", - "T_ARG1" + "T_ARG1", + "T_CATEGORY", + "T_KEY1" ], "out": [ "T_RC", @@ -12595,7 +18605,8 @@ const SVC_SCREENS = [ "in": [ "T_CATEGORY", "T_STR1", - "T_ARG1" + "T_ARG1", + "T_KEY1" ], "out": [ "T_RC" @@ -12608,7 +18619,8 @@ const SVC_SCREENS = [ "desc": "기능: 정산계좌 변경 (직접 UPDATE + 이력).", "in": [ "T_MERCHANT", - "T_STR3" + "T_STR3", + "T_KEY1" ], "out": [] }, @@ -12620,7 +18632,8 @@ const SVC_SCREENS = [ "in": [ "T_MERCHANT", "T_STR2", - "T_COUNT" + "T_COUNT", + "T_KEY1" ], "out": [ "T_COUNT" @@ -12633,7 +18646,8 @@ const SVC_SCREENS = [ "desc": "기능: 가맹점 폐업 처리 (DELETED 전환 + 전체 단말 폐기 + 하위 관계 해제).", "in": [ "T_MERCHANT", - "T_REASON" + "T_REASON", + "T_KEY1" ], "out": [ "T_RC", @@ -12646,7 +18660,8 @@ const SVC_SCREENS = [ "svc": "MM_MERCH_DEL", "desc": "기능: 가맹점 논리 삭제 (상태 DELETED + 이력).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [] }, @@ -12657,7 +18672,8 @@ const SVC_SCREENS = [ "desc": "기능: 장기 무거래 가맹점 휴면 전환 (ACTIVE->DORMANT, 최근 집계 매출 0 확인).", "in": [ "T_MERCHANT", - "T_REASON" + "T_REASON", + "T_KEY1" ], "out": [ "T_RC", @@ -12671,7 +18687,8 @@ const SVC_SCREENS = [ "desc": "기능: 가맹점 등급 변경 (dbio + 이력).", "in": [ "T_MERCHANT", - "T_STR2" + "T_STR2", + "T_KEY1" ], "out": [] }, @@ -12681,7 +18698,8 @@ const SVC_SCREENS = [ "svc": "MM_MERCH_INQ", "desc": "기능: 가맹점 마스터 조회 (dbio).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [] }, @@ -12692,7 +18710,8 @@ const SVC_SCREENS = [ "desc": "기능: 가맹점 휴업 처리 (ACTIVE -> HOLD, 가동 단말 일괄 SUSPENDED).", "in": [ "T_MERCHANT", - "T_REASON" + "T_REASON", + "T_KEY1" ], "out": [ "T_RC", @@ -12708,7 +18727,8 @@ const SVC_SCREENS = [ "T_MERCHANT", "T_STR1", "T_STR2", - "T_STR3" + "T_STR3", + "T_KEY1" ], "out": [] }, @@ -12718,7 +18738,8 @@ const SVC_SCREENS = [ "svc": "MM_MERCH_RESUME", "desc": "기능: 가맹점 영업 재개 (HOLD -> ACTIVE, 정지 단말 재가동).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC", @@ -12732,7 +18753,8 @@ const SVC_SCREENS = [ "desc": "기능: 가맹점 상태 변경 (dbio + 이력).", "in": [ "T_MERCHANT", - "T_STATUS" + "T_STATUS", + "T_KEY1" ], "out": [] }, @@ -12742,7 +18764,9 @@ const SVC_SCREENS = [ "svc": "MM_MERCH_SUM", "desc": "기능: 상태별 가맹점 수 집계 (ACTIVE 기준).", "in": [ - "T_STATUS" + "T_STATUS", + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_COUNT" @@ -12756,7 +18780,8 @@ const SVC_SCREENS = [ "in": [ "T_MERCHANT", "T_STR1", - "T_STR3" + "T_STR3", + "T_KEY1" ], "out": [] }, @@ -12766,7 +18791,8 @@ const SVC_SCREENS = [ "svc": "MM_PARTNER_INQ", "desc": "기능: 제휴사 마스터 단건 조회 (명칭/상태/표시순서).", "in": [ - "T_KEY1" + "T_KEY1", + "T_STR1" ], "out": [ "T_RC", @@ -12778,7 +18804,10 @@ const SVC_SCREENS = [ "modnm": "마스터", "svc": "MM_PARTNER_LIST", "desc": "기능: 활성 제휴사 총수 조회 (PARTNER 코드그룹 중 ACTIVE 상태 집계).", - "in": [], + "in": [ + "T_KEY1", + "T_STR1" + ], "out": [ "T_COUNT", "T_ARG1", @@ -12806,7 +18835,8 @@ const SVC_SCREENS = [ "desc": "기능: 제휴사 사용 상태 변경 (ACTIVE/INACTIVE 만 허용).", "in": [ "T_KEY1", - "T_STATUS" + "T_STATUS", + "T_STR1" ], "out": [ "T_RC" @@ -12832,7 +18862,8 @@ const SVC_SCREENS = [ "svc": "MM_RATE_APPROVE", "desc": "기능: 수수료율 변경 승인 (대기 요청을 실제 요율 마스터에 반영, RATE_APV 이력).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC", @@ -12845,7 +18876,8 @@ const SVC_SCREENS = [ "svc": "MM_RATE_CANCEL", "desc": "기능: 대기중 요율 변경 요청 철회 (미처리 RATE_REQ가 있을 때만 RATE_REJ로 자체 종결).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC" @@ -12857,7 +18889,8 @@ const SVC_SCREENS = [ "svc": "MM_RATE_HIST", "desc": "기능: 요율 승인 워크플로 이력 집계 (요청/승인/반려 건수를 각각 반환).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_COUNT", @@ -12871,7 +18904,10 @@ const SVC_SCREENS = [ "modnm": "마스터", "svc": "MM_RATE_PENDING", "desc": "기능: 전 가맹점 미처리 요율 요청 수 집계 (승인/반려로 종결되지 않은 RATE_REQ 카운트).", - "in": [], + "in": [ + "T_MERCHANT", + "T_KEY1" + ], "out": [ "T_COUNT", "T_ARG1", @@ -12885,7 +18921,8 @@ const SVC_SCREENS = [ "desc": "기능: 수수료율 변경 반려 (대기 요청을 반려 처리, 요율 마스터는 불변).", "in": [ "T_MERCHANT", - "T_REASON" + "T_REASON", + "T_KEY1" ], "out": [ "T_RC" @@ -12900,7 +18937,8 @@ const SVC_SCREENS = [ "T_MERCHANT", "T_STR2", "T_REASON", - "T_ARG1" + "T_ARG1", + "T_KEY1" ], "out": [ "T_RC", @@ -12913,7 +18951,8 @@ const SVC_SCREENS = [ "svc": "MM_RATE_STATUS", "desc": "기능: 최신 요율 변경 요청의 진행 상태 판정 (요청 이후 승인/반려 이력 존재 여부).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_ARG1", @@ -12926,7 +18965,8 @@ const SVC_SCREENS = [ "svc": "MM_REL_CHILDREN", "desc": "기능: 대표 가맹점에 연결된 하위 가맹점 수 집계 (MERCH_REL code_name=대표 기준).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC", @@ -12952,7 +18992,8 @@ const SVC_SCREENS = [ "svc": "MM_REL_LIST", "desc": "기능: 대표가맹점의 하위가맹점 현황 (활성 연결 수 + 최초 하위 ID).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC", @@ -12978,7 +19019,8 @@ const SVC_SCREENS = [ "svc": "MM_REL_PARENT", "desc": "기능: 하위 가맹점의 대표 가맹점 조회 (MERCH_REL 코드그룹에서 code=하위 -> code_name=대표).", "in": [ - "T_KEY1" + "T_KEY1", + "T_MERCHANT" ], "out": [ "T_RC" @@ -12990,7 +19032,8 @@ const SVC_SCREENS = [ "svc": "MM_REL_UNLINK", "desc": "기능: 대표가맹점-하위가맹점 관계 해제 (관계 행 INACTIVE 전환 + 이력).", "in": [ - "T_KEY1" + "T_KEY1", + "T_MERCHANT" ], "out": [ "T_RC" @@ -13002,7 +19045,8 @@ const SVC_SCREENS = [ "svc": "MM_REL_VERIFY", "desc": "기능: 대표-하위 관계 정합성 검증 (대표가 활성 마스터에 존재하는지, 순환/자기참조 여부).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC", @@ -13016,7 +19060,8 @@ const SVC_SCREENS = [ "svc": "MM_RISK_INQ", "desc": "기능: 현행 리스크 등급 조회 (최신 RISK_SET 수동값 우선, 없으면 최신 RISK 산정 밴드).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC", @@ -13029,7 +19074,8 @@ const SVC_SCREENS = [ "svc": "MM_RISK_SCORE", "desc": "기능: 가맹점 리스크 점수 산정 (MCC 위험도*20 + 저등급 가산 + 비활성 상태 가산).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC", @@ -13043,7 +19089,8 @@ const SVC_SCREENS = [ "desc": "기능: 가맹점 리스크 등급 수동 설정 (1~5, 심사역 재량 오버라이드를 RISK_SET 이력에 기록).", "in": [ "T_MERCHANT", - "T_ARG1" + "T_ARG1", + "T_KEY1" ], "out": [ "T_RC" @@ -13054,7 +19101,10 @@ const SVC_SCREENS = [ "modnm": "마스터", "svc": "MM_STATUS", "desc": "기능: 서비스 상태 (전체 가맹점 수, RC=0).", - "in": [], + "in": [ + "T_MERCHANT", + "T_KEY1" + ], "out": [ "T_COUNT", "T_RC" @@ -13066,7 +19116,9 @@ const SVC_SCREENS = [ "svc": "MM_TERMINAL_INQ", "desc": "기능: 단말 조회.", "in": [ - "T_STR5" + "T_STR5", + "T_STR6", + "T_KEY1" ], "out": [] }, @@ -13078,7 +19130,8 @@ const SVC_SCREENS = [ "in": [ "T_STR5", "T_MERCHANT", - "T_STR6" + "T_STR6", + "T_KEY1" ], "out": [] }, @@ -13088,7 +19141,9 @@ const SVC_SCREENS = [ "svc": "MM_TERM_ACTIVATE", "desc": "기능: 단말 개통 (정지/비활성 단말을 ACTIVE로 전환).", "in": [ - "T_STR5" + "T_STR5", + "T_STR6", + "T_KEY1" ], "out": [ "T_RC" @@ -13100,7 +19155,10 @@ const SVC_SCREENS = [ "svc": "MM_TERM_CNT", "desc": "기능: 가맹점별 단말기 보유 현황 집계 (전체/가동/폐기 건수).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_STR5", + "T_STR6", + "T_KEY1" ], "out": [ "T_COUNT", @@ -13115,7 +19173,9 @@ const SVC_SCREENS = [ "svc": "MM_TERM_DEACT", "desc": "기능: 단말 개통 해지 (ACTIVE 단말을 INACTIVE로, 사유 기록).", "in": [ - "T_STR5" + "T_STR5", + "T_STR6", + "T_KEY1" ], "out": [ "T_RC", @@ -13128,7 +19188,9 @@ const SVC_SCREENS = [ "svc": "MM_TERM_HIST", "desc": "기능: 단말 상태 확인 조회 (현재 상태/소속/모델 반환, 존재 여부 판정).", "in": [ - "T_STR5" + "T_STR5", + "T_STR6", + "T_KEY1" ], "out": [ "T_RC" @@ -13141,7 +19203,9 @@ const SVC_SCREENS = [ "desc": "기능: 등록된 단말의 설치 확정 (상태 REGISTERED->ACTIVE, 가맹점 활성 여부 확인).", "in": [ "T_STR5", - "T_MERCHANT" + "T_MERCHANT", + "T_STR6", + "T_KEY1" ], "out": [ "T_RC" @@ -13153,7 +19217,10 @@ const SVC_SCREENS = [ "svc": "MM_TERM_LIST", "desc": "기능: 가맹점별 단말 현황 요약 (전체/활성/폐기 건수 집계).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_STR5", + "T_STR6", + "T_KEY1" ], "out": [ "T_COUNT", @@ -13169,7 +19236,8 @@ const SVC_SCREENS = [ "desc": "기능: 단말 모델(기종) 정보 변경. 출력: T_STR6에 이전 모델을 실어 반환.", "in": [ "T_STR5", - "T_STR6" + "T_STR6", + "T_KEY1" ], "out": [ "T_RC" @@ -13182,7 +19250,9 @@ const SVC_SCREENS = [ "desc": "기능: 단말기 가맹점간 이동 (소속 변경 + 반입 이력).", "in": [ "T_KEY1", - "T_MERCHANT" + "T_MERCHANT", + "T_STR5", + "T_STR6" ], "out": [ "T_RC" @@ -13195,7 +19265,9 @@ const SVC_SCREENS = [ "desc": "기능: 단말 소속 가맹점 재배정 (기존 소속에서 신규 활성 가맹점으로 이관).", "in": [ "T_STR5", - "T_MERCHANT" + "T_MERCHANT", + "T_STR6", + "T_KEY1" ], "out": [ "T_RC" @@ -13207,7 +19279,9 @@ const SVC_SCREENS = [ "svc": "MM_TERM_RETIRE", "desc": "기능: 단말 폐기 처리 (상태 RETIRED로 확정, 활성 단말은 폐기 전 해지 필요).", "in": [ - "T_STR5" + "T_STR5", + "T_STR6", + "T_KEY1" ], "out": [ "T_RC" @@ -13220,7 +19294,9 @@ const SVC_SCREENS = [ "desc": "기능: 단말기 폐기 (SCRAPPED 전환 + 폐기 사유 이력).", "in": [ "T_KEY1", - "T_REASON" + "T_REASON", + "T_STR5", + "T_STR6" ], "out": [ "T_RC" @@ -13232,7 +19308,9 @@ const SVC_SCREENS = [ "svc": "MM_TERM_SUSPEND", "desc": "기능: 단말기 사용정지/재개 토글 (ACTIVE<->SUSPENDED).", "in": [ - "T_KEY1" + "T_KEY1", + "T_STR5", + "T_STR6" ], "out": [ "T_RC" @@ -13245,7 +19323,8 @@ const SVC_SCREENS = [ "desc": "기능: 단말 교체 (구단말 폐기 + 신단말이 구단말의 가맹점/모델을 승계 등록).", "in": [ "T_STR5", - "T_STR6" + "T_STR6", + "T_KEY1" ], "out": [ "T_RC" @@ -13257,7 +19336,8 @@ const SVC_SCREENS = [ "svc": "MM_VALIDATE", "desc": "기능: 가맹점 마스터 정합성 검사 (존재+수수료율 존재).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC" @@ -13311,10 +19391,9 @@ const SVC_SCREENS = [ "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", - "T_ARG1", - "T_GROSS", - "T_RC" + "T_GROSS" ] }, { @@ -13350,12 +19429,13 @@ const SVC_SCREENS = [ "svc": "PAY_ADJUST", "desc": "지급 조정: py_adjust 에 증감(delta)을 기록하고 지급건 net 에 즉시 반영.", "in": [ - "T_ID1", - "T_DELTA", "T_BIZDATE", + "T_DELTA", + "T_ID1", "T_REASON" ], "out": [ + "T_RC", "T_ADJ" ] }, @@ -13365,12 +19445,13 @@ const SVC_SCREENS = [ "svc": "PAY_ADVANCE", "desc": "선지급(advance): 선지급 수수료(1%)를 공제한 지급건(ADVANCE 채널)을 생성.", "in": [ - "T_MERCHANT", - "T_KEY1", "T_AMOUNT", - "T_BIZDATE" + "T_BIZDATE", + "T_KEY1", + "T_MERCHANT" ], "out": [ + "T_RC", "T_ID1", "T_FEE", "T_NET" @@ -13382,15 +19463,15 @@ const SVC_SCREENS = [ "svc": "PAY_ADVCALC", "desc": "선지급 가능액 산정: 가맹점 미지급(R/A) 잔액에 지급률(bps)을 곱한 한도에서", "in": [ - "T_MERCHANT", - "T_ARG1" + "T_ARG1", + "T_MERCHANT" ], "out": [ "T_RC", - "T_GROSS", "T_AMT1", "T_AMT2", - "T_ARG1" + "T_ARG1", + "T_GROSS" ] }, { @@ -13412,9 +19493,9 @@ const SVC_SCREENS = [ "svc": "PAY_ADVREPAY", "desc": "선지급 상환: ADVANCE 채널 지급건의 잔여 net 에서 상환액을 차감하고", "in": [ - "T_ID1", "T_AMOUNT", - "T_BIZDATE" + "T_BIZDATE", + "T_ID1" ], "out": [ "T_RC", @@ -13428,14 +19509,14 @@ const SVC_SCREENS = [ "svc": "PAY_APPRBULK", "desc": "소액 일괄 승인: 당일 요청(R)건 중 net 이 승인한도 이하인 건을 한 번의", "in": [ - "T_BIZDATE", - "T_AMT1" + "T_AMT1", + "T_BIZDATE" ], "out": [ "T_RC", "T_COUNT", - "T_ARG1", - "T_AMT1" + "T_AMT1", + "T_ARG1" ] }, { @@ -13459,8 +19540,8 @@ const SVC_SCREENS = [ "T_ID1" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_COUNT" ] }, { @@ -13469,9 +19550,9 @@ const SVC_SCREENS = [ "svc": "PAY_APPRLIMIT", "desc": "고액 승인 통제: 한도 초과 건은 승인자 지정 없이는 거절(2). 승인 시", "in": [ + "T_AMT1", "T_ID1", - "T_KEY2", - "T_AMT1" + "T_KEY2" ], "out": [ "T_RC", @@ -13487,8 +19568,8 @@ const SVC_SCREENS = [ "T_ID1" ], "out": [ - "T_AMOUNT", - "T_RC" + "T_RC", + "T_AMOUNT" ] }, { @@ -13498,9 +19579,10 @@ const SVC_SCREENS = [ "desc": "승인 대기 집계: 아직 승인 전인 요청(R) 지급건을 가맹점별로 묶어 대기 건수/", "in": [], "out": [ + "T_RC", "T_COUNT", "T_ARG2", - "T_RC" + "T_ARG3" ] }, { @@ -13509,12 +19591,12 @@ const SVC_SCREENS = [ "svc": "PAY_APPRREQ", "desc": "승인 대기 큐 조회: 당일 요청(R)건을 금액 내림차순 상위 N건 커서로 훑어", "in": [ - "T_BIZDATE", - "T_ARG1" + "T_ARG1", + "T_BIZDATE" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_COUNT" ] }, { @@ -13523,13 +19605,13 @@ const SVC_SCREENS = [ "svc": "PAY_BANKFILE", "desc": "은행별 지급파일 생성: 지정 은행코드의 승인(A) 지급건만 py_account 조인으로", "in": [ - "T_KEY1", - "T_BIZDATE" + "T_BIZDATE", + "T_KEY1" ], "out": [ "T_RC", - "T_COUNT", "T_ID2", + "T_COUNT", "T_GROSS" ] }, @@ -13542,11 +19624,11 @@ const SVC_SCREENS = [ "T_ID2" ], "out": [ + "T_RC", + "T_COUNT", "T_AMT1", "T_AMT2", - "T_COUNT", - "T_DELTA", - "T_RC" + "T_DELTA" ] }, { @@ -13555,13 +19637,14 @@ const SVC_SCREENS = [ "svc": "PAY_BULK", "desc": "대량 지급 생성: 동일 금액의 지급건(BULK 채널)을 N건 반복 등록.", "in": [ - "T_MERCHANT", - "T_KEY1", "T_AMOUNT", + "T_BIZDATE", "T_COUNT", - "T_BIZDATE" + "T_KEY1", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -13571,12 +19654,13 @@ const SVC_SCREENS = [ "svc": "PAY_CANCEL", "desc": "지급 취소: 취소 조정(-amount)을 py_adjust 에 기록하고 지급건 상태 C 로 전이.", "in": [ - "T_ID1", "T_AMOUNT", "T_BIZDATE", + "T_ID1", "T_REASON" ], "out": [ + "T_RC", "T_ADJ" ] }, @@ -13586,16 +19670,14 @@ const SVC_SCREENS = [ "svc": "PAY_CANCELHIST", "desc": "취소/환입 이력 집계: 가맹점 당일 취소(C)와 역분개/환입(V)을 각각", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_MERCHANT" ], "out": [ - "T_ARG1", - "T_AMT1", - "T_ARG2", - "T_AMT2", + "T_RC", "T_COUNT", - "T_RC" + "T_AMT2", + "T_ARG2" ] }, { @@ -13610,6 +19692,7 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_COUNT", "T_ADJ" ] }, @@ -13619,8 +19702,8 @@ const SVC_SCREENS = [ "svc": "PAY_CHARGEBACK", "desc": "지급 되물림: 분쟁 등으로 지급완료(P) 건을 되물릴 때, 원건은 보존한 채", "in": [ - "T_ID1", - "T_BIZDATE" + "T_BIZDATE", + "T_ID1" ], "out": [ "T_RC", @@ -13637,6 +19720,7 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -13659,8 +19743,8 @@ const SVC_SCREENS = [ "svc": "PAY_COMBINE", "desc": "지급 합산: 같은 계좌의 당일 요청(R) 건들을 하나로 합산 지급한다. 합계", "in": [ - "T_KEY1", - "T_BIZDATE" + "T_BIZDATE", + "T_KEY1" ], "out": [ "T_RC", @@ -13677,7 +19761,9 @@ const SVC_SCREENS = [ "in": [ "T_ID2" ], - "out": [] + "out": [ + "T_RC" + ] }, { "mod": "py", @@ -13702,8 +19788,8 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_COUNT" ] }, { @@ -13716,8 +19802,8 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", - "T_ARG1", - "T_COUNT" + "T_COUNT", + "T_ARG1" ] }, { @@ -13739,8 +19825,8 @@ const SVC_SCREENS = [ "svc": "PAY_FAILRATE", "desc": "지급 실패율 산출: 가맹점 당일 전체 지급건 대비 반려(J) 비중을 bps 로", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_MERCHANT" ], "out": [ "T_RC", @@ -13758,8 +19844,11 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", - "T_RC" + "T_AMT2", + "T_ARG6", + "T_ARG7" ] }, { @@ -13771,10 +19860,10 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", "T_GROSS", - "T_ARG2", - "T_RC" + "T_ARG2" ] }, { @@ -13797,16 +19886,16 @@ const SVC_SCREENS = [ "svc": "PAY_FEECAP", "desc": "수수료 상한 적용: 미지급 건 수수료가 상한(T_AMOUNT)을 넘으면 상한까지", "in": [ - "T_ID1", "T_AMOUNT", "T_BIZDATE", + "T_ID1", "T_REASON" ], "out": [ "T_RC", - "T_ADJ", "T_FEE", - "T_NET" + "T_NET", + "T_ADJ" ] }, { @@ -13815,11 +19904,12 @@ const SVC_SCREENS = [ "svc": "PAY_FEEDED", "desc": "수수료 공제(fee deduction): bps 로 수수료를 산출해 fee/net 을 재계산·갱신.", "in": [ - "T_ID1", "T_AMOUNT", - "T_ARG1" + "T_ARG1", + "T_ID1" ], "out": [ + "T_RC", "T_FEE", "T_NET" ] @@ -13830,8 +19920,8 @@ const SVC_SCREENS = [ "svc": "PAY_FEEFIXED", "desc": "정액 수수료 적용: 미지급(R/A) 건에 요청 정액 수수료(T_AMOUNT)를 부과하고", "in": [ - "T_ID1", - "T_AMOUNT" + "T_AMOUNT", + "T_ID1" ], "out": [ "T_RC", @@ -13845,8 +19935,8 @@ const SVC_SCREENS = [ "svc": "PAY_FEEMIN", "desc": "최소수수료 보정: 당일 요청(R) 건 중 수수료가 최소수수료 미만인 건을", "in": [ - "T_BIZDATE", - "T_AMT1" + "T_AMT1", + "T_BIZDATE" ], "out": [ "T_RC", @@ -13893,9 +19983,9 @@ const SVC_SCREENS = [ "out": [ "T_RC", "T_COUNT", + "T_NET", "T_FEE", - "T_ADJ", - "T_NET" + "T_ADJ" ] }, { @@ -13904,9 +19994,9 @@ const SVC_SCREENS = [ "svc": "PAY_FEEWAIVE", "desc": "수수료 면제: 미지급(R/A) 건의 수수료를 0 으로 하고 net 을 amount 로 복원.", "in": [ + "T_BIZDATE", "T_ID1", - "T_REASON", - "T_BIZDATE" + "T_REASON" ], "out": [ "T_RC", @@ -13949,6 +20039,7 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_ID2", "T_COUNT", "T_GROSS" @@ -13977,11 +20068,10 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_ARG1", - "T_ARG2", + "T_RC", "T_COUNT", "T_GROSS", - "T_RC" + "T_ARG2" ] }, { @@ -13993,11 +20083,11 @@ const SVC_SCREENS = [ "T_ID2" ], "out": [ - "T_ARG1", - "T_AMT1", + "T_RC", "T_COUNT", "T_GROSS", - "T_RC" + "T_AMT1", + "T_ARG1" ] }, { @@ -14006,8 +20096,8 @@ const SVC_SCREENS = [ "svc": "PAY_GARNISH", "desc": "가압류 등록: 활성 계좌를 GARNISH 로 표시하고, 그 계좌 미지급(R/A) 잔액을", "in": [ - "T_KEY1", - "T_BIZDATE" + "T_BIZDATE", + "T_KEY1" ], "out": [ "T_RC", @@ -14024,6 +20114,7 @@ const SVC_SCREENS = [ "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -14063,8 +20154,10 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", - "T_RC" + "T_AMT4", + "T_ARG5" ] }, { @@ -14076,8 +20169,8 @@ const SVC_SCREENS = [ "T_KEY1" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_COUNT" ] }, { @@ -14101,9 +20194,9 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", - "T_GROSS", - "T_RC" + "T_GROSS" ] }, { @@ -14141,9 +20234,14 @@ const SVC_SCREENS = [ "T_ID1" ], "out": [ + "T_RC", + "T_ARG3", "T_AMOUNT", "T_FEE", - "T_NET" + "T_NET", + "T_ID2", + "T_ARG1", + "T_ARG2" ] }, { @@ -14157,6 +20255,7 @@ const SVC_SCREENS = [ "T_ARG2" ], "out": [ + "T_RC", "T_AMT1", "T_NET" ] @@ -14170,9 +20269,11 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", "T_GROSS", - "T_RC" + "T_AMT2", + "T_AMT3" ] }, { @@ -14181,14 +20282,14 @@ const SVC_SCREENS = [ "svc": "PAY_LIMITCHK", "desc": "지급한도 점검: 가맹점 일한도(merchant.daily_limit) 대비 당일 확정성", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_AMT1", "T_AMT2", - "T_AMT3", - "T_RC" + "T_AMT3" ] }, { @@ -14201,8 +20302,8 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", - "T_ARG1", - "T_COUNT" + "T_COUNT", + "T_ARG1" ] }, { @@ -14214,8 +20315,9 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", - "T_RC" + "T_AMT2" ] }, { @@ -14224,8 +20326,8 @@ const SVC_SCREENS = [ "svc": "PAY_LIMITSET", "desc": "지급 일한도 설정: 가맹점의 일 지급한도(merchant.daily_limit)를 새 값으로", "in": [ - "T_MERCHANT", - "T_AMOUNT" + "T_AMOUNT", + "T_MERCHANT" ], "out": [ "T_RC", @@ -14238,12 +20340,13 @@ const SVC_SCREENS = [ "svc": "PAY_LIMITWARN", "desc": "지급 한도 근접 경보: 당일 사용액이 일한도의 지정 비율(T_ARG1 %, 기본 80)", "in": [ - "T_BIZDATE", - "T_ARG1" + "T_ARG1", + "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", - "T_RC" + "T_AMT2" ] }, { @@ -14252,10 +20355,11 @@ const SVC_SCREENS = [ "svc": "PAY_MERCHSUM", "desc": "가맹점 당일 지급 집계: py_payout 집계 후 dbio 로 py_summary upsert.", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", "T_GROSS" ] @@ -14266,13 +20370,13 @@ const SVC_SCREENS = [ "svc": "PAY_MERGE", "desc": "지급 합산: 가맹점 당일 요청(R) STD 건이 2건 이상이면 전부 취소(C) 표시 후", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_MERCHANT" ], "out": [ "T_RC", - "T_COUNT", "T_ID2", + "T_COUNT", "T_NET" ] }, @@ -14282,10 +20386,11 @@ const SVC_SCREENS = [ "svc": "PAY_NETOFF", "desc": "상계(net-off): 지급건의 net 을 조회해 차감액을 반영하고 갱신.", "in": [ - "T_ID1", - "T_AMOUNT" + "T_AMOUNT", + "T_ID1" ], "out": [ + "T_RC", "T_NET" ] }, @@ -14295,14 +20400,14 @@ const SVC_SCREENS = [ "svc": "PAY_NETRECALC", "desc": "순지급액 정합 검증: 당일 건 중 amount - fee != net 인 불일치 건수와", "in": [ - "T_BIZDATE", - "T_ARG1" + "T_ARG1", + "T_BIZDATE" ], "out": [ "T_RC", "T_COUNT", - "T_DELTA", - "T_ARG2" + "T_ARG2", + "T_DELTA" ] }, { @@ -14325,8 +20430,8 @@ const SVC_SCREENS = [ "svc": "PAY_NOTIFYEMAIL", "desc": "지급내역 이메일 통지(시뮬): 가맹점의 당일 지급완료(P) 건수/합계를 집계해", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_MERCHANT" ], "out": [ "T_RC", @@ -14419,15 +20524,15 @@ const SVC_SCREENS = [ "svc": "PAY_OFFSETAPPLY", "desc": "상계 적용: 미지급(R/A) 건의 net 에서 상계액을 차감(net 미만으로 캡)하고,", "in": [ - "T_ID1", "T_AMOUNT", "T_BIZDATE", + "T_ID1", "T_REASON" ], "out": [ "T_RC", - "T_ADJ", - "T_NET" + "T_NET", + "T_ADJ" ] }, { @@ -14439,8 +20544,8 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_COUNT" ] }, { @@ -14461,8 +20566,8 @@ const SVC_SCREENS = [ "svc": "PAY_RECLAIM", "desc": "과지급 환수: 지급완료(P) 건에서 요청 환수액(T_AMOUNT, net 한도)만큼을", "in": [ - "T_ID1", - "T_AMOUNT" + "T_AMOUNT", + "T_ID1" ], "out": [ "T_RC", @@ -14478,6 +20583,7 @@ const SVC_SCREENS = [ "T_ID2" ], "out": [ + "T_RC", "T_COUNT", "T_GROSS" ] @@ -14505,7 +20611,9 @@ const SVC_SCREENS = [ "T_ID1", "T_REASON" ], - "out": [] + "out": [ + "T_RC" + ] }, { "mod": "py", @@ -14526,8 +20634,8 @@ const SVC_SCREENS = [ "desc": "실지급 송금: 계좌 잔액을 확인해 부족하면 거절, 충분하면 차감 후 상태 P.", "in": [ "T_ID1", - "T_NET", - "T_KEY1" + "T_KEY1", + "T_NET" ], "out": [ "T_RC" @@ -14539,9 +20647,9 @@ const SVC_SCREENS = [ "svc": "PAY_REPAYACCT", "desc": "계좌변경 재지급: 반려(J)된 지급건의 금액/수수료를 승계해 새 계좌(T_KEY2)로", "in": [ + "T_BIZDATE", "T_ID1", - "T_KEY2", - "T_BIZDATE" + "T_KEY2" ], "out": [ "T_RC", @@ -14608,8 +20716,9 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", - "T_RC" + "T_ARG2" ] }, { @@ -14618,8 +20727,8 @@ const SVC_SCREENS = [ "svc": "PAY_RESVMODIFY", "desc": "예약 변경: 아직 실행되지 않은 예약(S) 의 실행일(run_date)을 새 일자로", "in": [ - "T_ID2", - "T_BIZDATE" + "T_BIZDATE", + "T_ID2" ], "out": [ "T_RC" @@ -14656,11 +20765,13 @@ const SVC_SCREENS = [ "svc": "PAY_REVERSE", "desc": "지급 역분개(reverse): 상태 V(역분개)로 전이하고 계좌 잔액을 원복.", "in": [ - "T_ID1", "T_AMOUNT", + "T_ID1", "T_KEY1" ], - "out": [] + "out": [ + "T_RC" + ] }, { "mod": "py", @@ -14695,10 +20806,11 @@ const SVC_SCREENS = [ "svc": "PAY_SCHEDULE", "desc": "지급 예약 등록: 지정 실행일로 py_schedule 예약(S) 레코드를 생성.", "in": [ - "T_ID1", - "T_BIZDATE" + "T_BIZDATE", + "T_ID1" ], "out": [ + "T_RC", "T_ID2" ] }, @@ -14722,9 +20834,9 @@ const SVC_SCREENS = [ "svc": "PAY_SEIZEAMT", "desc": "압류 금액 상계: 압류 계좌의 보류(H) 지급건 net 합을 압류가능액으로 산정하고,", "in": [ - "T_KEY1", + "T_AMOUNT", "T_BIZDATE", - "T_AMOUNT" + "T_KEY1" ], "out": [ "T_RC", @@ -14741,9 +20853,9 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", - "T_GROSS", - "T_RC" + "T_GROSS" ] }, { @@ -14753,8 +20865,8 @@ const SVC_SCREENS = [ "desc": "압류 계좌 목록: 압류(SEIZED) 상태 계좌를 커서로 훑어 계좌번호/예금주와 그", "in": [], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_COUNT" ] }, { @@ -14776,13 +20888,14 @@ const SVC_SCREENS = [ "svc": "PAY_SPLIT", "desc": "지급 분할: 금액을 N건으로 나눠 각각의 지급건(SPLIT 채널)으로 생성.", "in": [ - "T_MERCHANT", - "T_KEY1", "T_AMOUNT", + "T_BIZDATE", "T_COUNT", - "T_BIZDATE" + "T_KEY1", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -14792,9 +20905,9 @@ const SVC_SCREENS = [ "svc": "PAY_SPLITEVEN", "desc": "균등 분할: 요청(R) 지급건을 N 건으로 균등 분할하되 나머지는 첫 건에 몰아", "in": [ - "T_ID1", + "T_BIZDATE", "T_COUNT", - "T_BIZDATE" + "T_ID1" ], "out": [ "T_RC", @@ -14808,15 +20921,15 @@ const SVC_SCREENS = [ "svc": "PAY_SPLITRATIO", "desc": "비율 분할: 요청(R) 건을 bps 비율로 두 건의 SPLIT 지급건으로 쪼개고", "in": [ - "T_ID1", - "T_ARG1" + "T_ARG1", + "T_ID1" ], "out": [ "T_RC", "T_ID2", - "T_ARG2", "T_AMT1", - "T_AMT2" + "T_AMT2", + "T_ARG2" ] }, { @@ -14828,8 +20941,9 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", - "T_RC" + "T_ARG8" ] }, { @@ -14841,8 +20955,10 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", - "T_RC" + "T_AMT2", + "T_ARG3" ] }, { @@ -14854,10 +20970,10 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", "T_GROSS", - "T_ARG2", - "T_RC" + "T_ARG2" ] }, { @@ -14866,14 +20982,15 @@ const SVC_SCREENS = [ "svc": "PAY_STATDAILY", "desc": "일자별 지급 추이: 시작일부터 N일 구간을 일자별 커서 집계해 occurrence 로", "in": [ - "T_BIZDATE", - "T_ARG1" + "T_ARG1", + "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", "T_GROSS", "T_AMT2", - "T_RC" + "T_ARG2" ] }, { @@ -14882,14 +20999,15 @@ const SVC_SCREENS = [ "svc": "PAY_STATEMENT", "desc": "가맹점 지급 명세: 당일 지급완료(P) 건의 건수/금액/수수료/순액을 집계.", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", "T_GROSS", - "T_FEE", - "T_NET" + "T_NET", + "T_FEE" ] }, { @@ -14901,8 +21019,10 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", - "T_RC" + "T_AMT2", + "T_AMT3" ] }, { @@ -14915,8 +21035,10 @@ const SVC_SCREENS = [ "T_KEY2" ], "out": [ + "T_RC", "T_COUNT", - "T_RC" + "T_AMT2", + "T_ARG3" ] }, { @@ -14928,11 +21050,10 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_ARG1", - "T_ARG2", - "T_ARG3", + "T_RC", "T_COUNT", - "T_RC" + "T_ARG2", + "T_ARG3" ] }, { @@ -14944,8 +21065,11 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", - "T_RC" + "T_AMT3", + "T_AMT4", + "T_ARG4" ] }, { @@ -14956,7 +21080,13 @@ const SVC_SCREENS = [ "in": [ "T_ID1" ], - "out": [] + "out": [ + "T_RC", + "T_ARG3", + "T_ID2", + "T_ARG1", + "T_ARG2" + ] }, { "mod": "py", @@ -14964,10 +21094,11 @@ const SVC_SCREENS = [ "svc": "PAY_SUMMARY", "desc": "가맹점 일별 지급 집계 조회: py_summary 에서 건수/금액을 읽어 회신(무행 시 0).", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", "T_GROSS" ] @@ -14978,10 +21109,10 @@ const SVC_SCREENS = [ "svc": "PAY_URGENT", "desc": "긴급지급: 긴급 수수료(0.5%) 공제 후 잔액 확인 -> 즉시 지급완료(P) 건을", "in": [ - "T_MERCHANT", - "T_KEY1", "T_AMOUNT", - "T_BIZDATE" + "T_BIZDATE", + "T_KEY1", + "T_MERCHANT" ], "out": [ "T_RC", @@ -14996,12 +21127,14 @@ const SVC_SCREENS = [ "svc": "PAY_URGENTQ", "desc": "긴급 지급 큐: 당일 요청(R) 건 중 금액이 임계치(T_AMOUNT) 이상인 건을 금액", "in": [ - "T_BIZDATE", - "T_AMOUNT" + "T_AMOUNT", + "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", - "T_RC" + "T_AMT1", + "T_ARG1" ] }, { @@ -15039,8 +21172,9 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", - "T_RC" + "T_AMT2" ] }, { @@ -15049,13 +21183,13 @@ const SVC_SCREENS = [ "svc": "PAY_XFERFILE", "desc": "이체유형별 지급파일 생성: 지정 채널(STD/SPLIT/BULK/ADVANCE)의 승인(A) 건만", "in": [ - "T_CHANNEL", - "T_BIZDATE" + "T_BIZDATE", + "T_CHANNEL" ], "out": [ "T_RC", - "T_COUNT", "T_ID2", + "T_COUNT", "T_GROSS" ] }, @@ -15065,11 +21199,13 @@ const SVC_SCREENS = [ "svc": "PAY_XFERLINK", "desc": "이체 연계: 지급건에 대한 XFER 원장 엔트리를 py_ledger 에 기록.", "in": [ - "T_ID1", "T_AMOUNT", - "T_BIZDATE" + "T_BIZDATE", + "T_ID1" ], - "out": [] + "out": [ + "T_RC" + ] }, { "mod": "py", @@ -15080,10 +21216,10 @@ const SVC_SCREENS = [ "T_ID1" ], "out": [ + "T_RC", "T_NET", "T_AMT1", - "T_DELTA", - "T_RC" + "T_DELTA" ] }, { @@ -15095,9 +21231,9 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", - "T_GROSS", - "T_RC" + "T_GROSS" ] }, { @@ -15109,8 +21245,10 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", - "T_RC" + "T_AMT3", + "T_ARG4" ] }, { @@ -15122,19 +21260,19 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_COUNT" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_3WAY", - "desc": "3-way 대사: 승인+매입+원장 모두 존재 시 MATCHED. */", + "desc": "업무: 3-way 대사: 승인+매입+원장 모두 존재 시 MATCHED.", "in": [ + "T_BIZDATE", "T_PURCHASE_ID", - "T_NET", - "T_BIZDATE" + "T_NET" ], "out": [ "T_RC" @@ -15144,26 +21282,27 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_4WAY", - "desc": "승인-매입-입금-지급 4-way 대사: 승인(approval), 입금(settlement), 지급", + "desc": "업무: 승인-매입-입금-지급 4-way 대사.", "in": [ + "T_BIZDATE", "T_PURCHASE_ID", - "T_NET", - "T_BIZDATE" + "T_NET" ], "out": [ - "T_ID1", - "T_RC" + "T_RC", + "T_ID1" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_ACQMATCH", - "desc": "매입-입금 축 대사: 당일 매입 순액 합계와 원장 RECON 입금 합계를 비교해 차액을 산출.", + "desc": "업무: 매입-입금 축 대사: 당일 매입 순액 합계와 원장 RECON 입금 합계를 비교해 차액을 산출.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", "T_DELTA" @@ -15173,26 +21312,27 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_ACQ_DEP", - "desc": "매입-입금(원장) 대사: 원장 합계와 net 비교. */", + "desc": "업무: 매입-입금(원장) 대사: 원장 합계와 net 비교.", "in": [ + "T_BIZDATE", "T_PURCHASE_ID", - "T_NET", - "T_BIZDATE" + "T_NET" ], "out": [ - "T_GROSS", - "T_RC" + "T_RC", + "T_GROSS" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_AGEING", - "desc": "미대사 에이징: 미해결 예외를 당일 / 최근 7일 / 7일 초과 세 버킷으로 나눠", + "desc": "업무: 미해결 예외 에이징 (당일/7일/초과).", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", "T_ARG3", @@ -15203,14 +21343,15 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_ALERT", - "desc": "* RC_ALERT.pgc - rc 모듈 서비스 RC_ALERT (one service = one file).", + "desc": "업무: 대사 경보 등록.", "in": [ - "T_PURCHASE_ID", "T_BIZDATE", + "T_PURCHASE_ID", "T_MSG", "T_STR1" ], "out": [ + "T_RC", "T_ID1" ] }, @@ -15218,13 +21359,14 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_AMTDIFF", - "desc": "금액 차이 대사: 기대(net) vs 실제(원장합) 차이 기록. */", + "desc": "업무: 금액 차이 대사: 기대(net) vs 실제(원장합) 차이 기록.", "in": [ + "T_BIZDATE", "T_PURCHASE_ID", - "T_NET", - "T_BIZDATE" + "T_NET" ], "out": [ + "T_RC", "T_DELTA" ] }, @@ -15232,11 +21374,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_APPRGAP", - "desc": "승인 누락 감지: 당일 매입 중 승인 원장이 없는 건을 예외로 등록하고 누락 건수를 반환.", + "desc": "업무: 승인 누락 감지: 당일 매입 중 승인 원장이 없는 건을 예외로 등록하고 누락 건수를 반환.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -15244,11 +21387,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_APPRMATCH", - "desc": "승인-매입 축 대사: 당일 매입 중 승인건과 매칭된 건수와 승인 누락 건수를 분리 집계.", + "desc": "업무: 승인-매입 축 대사: 당일 매입 중 승인건과 매칭된 건수와 승인 누락 건수를 분리 집계.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", "T_COUNT" @@ -15258,27 +21402,28 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_APPR_ACQ", - "desc": "* RC_APPR_ACQ.pgc - rc 모듈 서비스 RC_APPR_ACQ (one service = one file).", + "desc": "업무: 승인-매입 대사 (승인 존재 여부 확인 후 대사 로그).", "in": [ + "T_BIZDATE", "T_PURCHASE_ID", - "T_NET", - "T_BIZDATE" + "T_NET" ], "out": [ - "T_ID1", - "T_RC" + "T_RC", + "T_ID1" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_AUDITADD", - "desc": "대사 감사추적 기록: 지정 매입의 대사 처리 이력을 AUDIT 유형 로그로 감사 추적에 적재.", + "desc": "업무: 대사 감사추적 기록: 지정 매입의 대사 처리 이력을 AUDIT 유형 로그로 감사 추적에 적재.", "in": [ "T_BIZDATE", "T_PURCHASE_ID" ], "out": [ + "T_RC", "T_ID1" ] }, @@ -15286,11 +21431,10 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_AUDITLOG", - "desc": "대사 감사 로그 기록: 호출 서비스명과 사유를 받아 원장에 RC_AUDIT 증적을", + "desc": "업무: 감사 이벤트 이중 기록 (원장 + 경보).", "in": [ - "T_PURCHASE_ID", "T_BIZDATE", - "T_SVCNAME", + "T_PURCHASE_ID", "T_REASON" ], "out": [ @@ -15301,11 +21445,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_AUDITTRAIL", - "desc": "대사 감사추적 조회: 특정 매입의 대사 이력 건수, 대사 관련 원장 증적 건수,", + "desc": "업무: 매입별 대사 감사추적(이력/증적/최종상태).", "in": [ "T_PURCHASE_ID" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2" ] @@ -15314,25 +21459,28 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_AUTOFIX", - "desc": "자동 보정: 예외 해소 + 보정 원장. */", + "desc": "업무: 자동 보정: 예외 해소 + 보정 원장.", "in": [ + "T_BIZDATE", "T_PURCHASE_ID", - "T_AMOUNT", - "T_BIZDATE" + "T_AMOUNT" ], - "out": [] + "out": [ + "T_RC" + ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_BACKDATE", - "desc": "소급 대사: 지정 매입을 과거 영업일자로 소급하여 대사 로그를 MATCHED 로 기록.", + "desc": "업무: 소급 대사: 지정 매입을 과거 영업일자로 소급하여 대사 로그를 MATCHED 로 기록.", "in": [ "T_BIZDATE", "T_PURCHASE_ID", "T_STR1" ], "out": [ + "T_RC", "T_ID1" ] }, @@ -15340,70 +21488,73 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_BRANDDIFF", - "desc": "브랜드별 차이 분석: 업종(category) 단위 매입 총액과 수수료 합계 비율을 점검.", + "desc": "업무: 브랜드별 차이 분석: 업종(category) 단위 매입 총액과 수수료 합계 비율을 점검.", "in": [ "T_BIZDATE", "T_CATEGORY" ], "out": [ - "T_GROSS", + "T_RC", "T_FEE", - "T_RC" + "T_GROSS" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_BRANDRECON", - "desc": "브랜드별 대사: 업종/브랜드(category) 단위로 당일 매입 총액과 건수를 집계.", + "desc": "업무: 브랜드별 대사: 업종/브랜드(category) 단위로 당일 매입 총액과 건수를 집계.", "in": [ "T_BIZDATE", "T_CATEGORY" ], "out": [ - "T_GROSS", - "T_COUNT" + "T_RC", + "T_COUNT", + "T_GROSS" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_BYCATEGORY", - "desc": "업종(카테고리)별 대사 세분: 요청 업종의 당일 MATCHED 건수와 EXCEPTION", + "desc": "업무: 업종별 대사 집계 + 대사율.", "in": [ "T_BIZDATE", "T_CATEGORY" ], "out": [ + "T_RC", "T_ARG1", - "T_ARG2", - "T_RC" + "T_ARG2" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_BYCHANNEL", - "desc": "채널별 대사 세분 집계: 요청 채널(POS/EDC/EDI/FOREIGN)의 당일 MATCHED 대사", + "desc": "업무: 채널별 대사 집계.", "in": [ "T_BIZDATE", "T_CHANNEL" ], "out": [ + "T_RC", "T_COUNT", - "T_GROSS", - "T_FEE" + "T_FEE", + "T_GROSS" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_BYDATE", - "desc": "* RC_BYDATE.pgc - rc 모듈 서비스 RC_BYDATE (one service = one file).", + "desc": "업무: 일자별 대사(MATCHED) 집계.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", "T_GROSS" ] @@ -15412,12 +21563,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_BYISSUER", - "desc": "* RC_BYISSUER.pgc - rc 모듈 서비스 RC_BYISSUER (one service = one file).", + "desc": "업무: 발급사별 매입 건수 집계.", "in": [ "T_BIZDATE", "T_ISSUER" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -15425,12 +21577,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_BYMERCH", - "desc": "* RC_BYMERCH.pgc - rc 모듈 서비스 RC_BYMERCH (one service = one file).", + "desc": "업무: 가맹점별 net 합계 조회.", "in": [ "T_BIZDATE", "T_MERCHANT" ], "out": [ + "T_RC", "T_GROSS" ] }, @@ -15438,12 +21591,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_BYMGROUP", - "desc": "가맹점그룹별 대사 세분: 가맹점 ID prefix (그룹 키) 로 묶어 당일 MATCHED", + "desc": "업무: 가맹점그룹(prefix)별 대사 집계.", "in": [ "T_BIZDATE", "T_KEY1" ], "out": [ + "T_RC", "T_COUNT", "T_GROSS" ] @@ -15452,12 +21606,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_CANCELREC", - "desc": "취소분 대사: 매입 금액과 취소(ac_cancel) 누적 금액을 비교하여 전액 취소면", + "desc": "업무: 취소분 대사 (전액취소=MATCHED / 잔량=HOLD).", "in": [ - "T_PURCHASE_ID", - "T_BIZDATE" + "T_BIZDATE", + "T_PURCHASE_ID" ], "out": [ + "T_RC", "T_AMT1", "T_AMT2", "T_ID1" @@ -15467,25 +21622,27 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_CANCMATCH", - "desc": "취소분 대사: 당일 취소(ac_cancel) 금액 합계와 취소 건수를 유형 무관하게 집계.", + "desc": "업무: 취소분 대사: 당일 취소(ac_cancel) 금액 합계와 취소 건수를 유형 무관하게 집계.", "in": [ "T_BIZDATE" ], "out": [ - "T_COUNT", - "T_AMOUNT" + "T_RC", + "T_AMOUNT", + "T_COUNT" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_CARRYFWD", - "desc": "익일 이월 처리: 당일 미해결 예외를 지정 익일자로 복제 등록하여 이월시킨다.", + "desc": "업무: 익일 이월 처리: 당일 미해결 예외를 지정 익일자로 복제 등록하여 이월시킨다.", "in": [ "T_BIZDATE", "T_STR1" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -15493,12 +21650,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_CARRYIN", - "desc": "전일 이월 대사: 전 영업일의 미해결 예외 건수를 조회해 당일 이월 대상으로 반환.", + "desc": "업무: 전일 이월 대사: 전 영업일의 미해결 예외 건수를 조회해 당일 이월 대상으로 반환.", "in": [ "T_BIZDATE", "T_STR1" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -15506,12 +21664,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_CARRYOVER", - "desc": "이월 대사: 이전 영업일의 EXCEPTION 대사 건들을 다음 영업일 HOLD 건으로", + "desc": "업무: EXCEPTION 건 익일 이월 (INSERT..SELECT).", "in": [ - "T_STR1", - "T_BIZDATE" + "T_BIZDATE", + "T_STR1" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -15519,12 +21678,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_CHANDIFF", - "desc": "채널별 차이 분석: 지정 채널의 매입 순액과 원장 SETTLE 합계 차이를 rc_diff 에 기록.", + "desc": "업무: 채널별 차이 분석: 지정 채널의 매입 순액과 원장 SETTLE 합계 차이를 rc_diff 에 기록.", "in": [ "T_BIZDATE", "T_CHANNEL" ], "out": [ + "T_RC", "T_DELTA" ] }, @@ -15532,28 +21692,29 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_CHANRECON", - "desc": "채널별 대사: 지정 채널의 당일 매입 건수와 대사완료(M/S) 건수를 대조.", + "desc": "업무: 채널별 대사: 지정 채널의 당일 매입 건수와 대사완료(M/S) 건수를 대조.", "in": [ "T_BIZDATE", "T_CHANNEL" ], "out": [ - "T_COUNT", - "T_ARG1" + "T_RC", + "T_ARG1", + "T_COUNT" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_CLOSEDAY", - "desc": "일마감 대사 확정: 미해결 예외나 보류가 남아 있으면 마감을 거부하고, 모두", + "desc": "업무: 일마감 확정 (예외/보류 잔량 시 거부).", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", - "T_RC", "T_COUNT", "T_GROSS" ] @@ -15562,11 +21723,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_CNTDIFF", - "desc": "건수 차이 대사: 매입 건수 vs 대사 로그 건수. */", + "desc": "업무: 건수 차이 대사: 매입 건수 vs 대사 로그 건수.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -15574,23 +21736,26 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_CONFIRM", - "desc": "* RC_CONFIRM.pgc - rc 모듈 서비스 RC_CONFIRM (one service = one file).", + "desc": "업무: 대사 확정 (MATCHED -> CONFIRMED).", "in": [ "T_PURCHASE_ID" ], - "out": [] + "out": [ + "T_RC" + ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_DELAY", - "desc": "* RC_DELAY.pgc - rc 모듈 서비스 RC_DELAY (one service = one file).", + "desc": "업무: 지연 대사 로그(익일 반영).", "in": [ + "T_BIZDATE", "T_PURCHASE_ID", - "T_NET", - "T_BIZDATE" + "T_NET" ], "out": [ + "T_RC", "T_ID1" ] }, @@ -15598,11 +21763,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_DEPGAP", - "desc": "입금 누락 감지: 매입은 있으나 RECON 원장이 없는 미입금 건수를 집계.", + "desc": "업무: 입금 누락 감지: 매입은 있으나 RECON 원장이 없는 미입금 건수를 집계.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -15610,12 +21776,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_DEPMATCH", - "desc": "입금(정산) 대사: 매입 net 금액과 settlement 입금 합계를 비교하여 일치 시", + "desc": "업무: 입금(정산) 대사.", "in": [ - "T_PURCHASE_ID", - "T_BIZDATE" + "T_BIZDATE", + "T_PURCHASE_ID" ], "out": [ + "T_RC", "T_AMT1", "T_AMT2", "T_ID1" @@ -15625,25 +21792,27 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_DEPPAY", - "desc": "입금-지급 축 대사: 원장 XFER 이체 합계와 정산 순액 합계의 지급 정합성을 대조.", + "desc": "업무: 입금-지급 축 대사: 원장 XFER 이체 합계와 정산 순액 합계의 지급 정합성을 대조.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_ARG1", - "T_GROSS", - "T_DELTA" + "T_DELTA", + "T_GROSS" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_DIFFCAUSE", - "desc": "대사 차이 원인분석: 당일 차이를 (1) 수수료 계산 불일치, (2) 취소 존재,", + "desc": "업무: 차이 원인(수수료/취소/승인부재)별 분해.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", "T_ARG3", @@ -15654,24 +21823,26 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_DIFFRANK", - "desc": "차이 순위 분석: 당일 절대 차이금액이 가장 큰 매입 1건과 그 차액을 반환.", + "desc": "업무: 차이 순위 분석: 당일 절대 차이금액이 가장 큰 매입 1건과 그 차액을 반환.", "in": [ "T_BIZDATE" ], "out": [ - "T_PURCHASE_ID", - "T_DELTA" + "T_RC", + "T_DELTA", + "T_PURCHASE_ID" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_DIFFRPT", - "desc": "* RC_DIFFRPT.pgc - rc 모듈 서비스 RC_DIFFRPT (one service = one file).", + "desc": "업무: 당일 차이 라인 건수 조회.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", "T_DELTA" ] @@ -15680,25 +21851,26 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_DIFFTREND", - "desc": "대사 차이 추이 분석: 당일 평균 |차이금액| 과 직전 7영업일 평균을 비교하여", + "desc": "업무: 당일 vs 직전 7일 평균 차이 추이.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_AMT1", - "T_AMT2", - "T_RC" + "T_AMT2" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_DUPCHK", - "desc": "중복 대사 검출: 당일 대사 로그에서 같은 매입이 두 번 이상 대사된 매입", + "desc": "업무: 중복 대사 매입 검출 + WARN 경보.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -15706,40 +21878,43 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_EDCRECON", - "desc": "EDC 채널 대사: EDC 단말 매입의 순액 합계와 미대사 건수를 함께 반환.", + "desc": "업무: EDC 채널 대사: EDC 단말 매입의 순액 합계와 미대사 건수를 함께 반환.", "in": [ "T_BIZDATE" ], "out": [ - "T_NET", - "T_ARG1" + "T_RC", + "T_ARG1", + "T_NET" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_EDIRECON", - "desc": "EDI 대사: 가맹점의 당일 EDI 전문 금액 합계와 매입(취소 제외) 금액 합계를", + "desc": "업무: 가맹점 EDI 전문 대 매입 합계 대사.", "in": [ "T_BIZDATE", "T_MERCHANT" ], "out": [ + "T_RC", "T_AMT1", "T_AMT2", - "T_DELTA", - "T_COUNT" + "T_COUNT", + "T_DELTA" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_ESCALATE", - "desc": "예외 에스컬레이션: 미해결 예외가 걸린 매입의 당일 WARN 경보들을 IN-서브", + "desc": "업무: 미해결 예외 경보 WARN -> CRIT 일괄 승격.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -15747,13 +21922,14 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_EXCEPTION", - "desc": "* RC_EXCEPTION.pgc - rc 모듈 서비스 RC_EXCEPTION (one service = one file).", + "desc": "업무: 대사 예외 등록.", "in": [ - "T_PURCHASE_ID", "T_BIZDATE", + "T_PURCHASE_ID", "T_REASON" ], "out": [ + "T_RC", "T_ID1" ] }, @@ -15761,11 +21937,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_EXCNOTIFY", - "desc": "대사 예외 알림: 당일 미해결 예외 건수를 집계해 임계 초과 시 rc_alert 경보를 발생.", + "desc": "업무: 대사 예외 알림: 당일 미해결 예외 건수를 집계해 임계 초과 시 rc_alert 경보를 발생.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -15773,25 +21950,27 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_FEEBRK", - "desc": "수수료 내역 대사: 당일 매입 수수료 원계와 수수료조정 누계를 비교해 순수수료 산출.", + "desc": "업무: 수수료 내역 대사: 당일 매입 수수료 원계와 수수료조정 누계를 비교해 순수수료 산출.", "in": [ "T_BIZDATE" ], "out": [ - "T_FEE", + "T_RC", + "T_ARG1", "T_DELTA", - "T_ARG1" + "T_FEE" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_FEEDIFF", - "desc": "수수료 차이 일괄 집계: 당일 매입 전체에서 MDR 기대치와 다른 수수료 건수와", + "desc": "업무: 당일 수수료 불일치 건수/차액 합계.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", "T_DELTA" ] @@ -15800,14 +21979,15 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_FEERECON", - "desc": "수수료 대사: 매입 수수료를 가맹점 MDR(bp) 기준 기대 수수료와 비교한다.", + "desc": "업무: MDR 기준 수수료 대사.", "in": [ - "T_PURCHASE_ID", - "T_BIZDATE" + "T_BIZDATE", + "T_PURCHASE_ID" ], "out": [ - "T_FEE", + "T_RC", "T_DELTA", + "T_FEE", "T_ID1" ] }, @@ -15815,11 +21995,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_FIXDUP", - "desc": "중복 자동보정: 동일 매입에 SETTLE 원장이 2건 이상 중복된 매입 건수를 탐지.", + "desc": "업무: 중복 자동보정: 동일 매입에 SETTLE 원장이 2건 이상 중복된 매입 건수를 탐지.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -15827,27 +22008,29 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_FIXFEE", - "desc": "수수료 자동보정: 매입 수수료가 MDR 이론치와 다르면 차이를 조정 델타로 반환.", + "desc": "업무: 수수료 자동보정: 매입 수수료가 MDR 이론치와 다르면 차이를 조정 델타로 반환.", "in": [ "T_BIZDATE", "T_PURCHASE_ID" ], "out": [ - "T_FEE", + "T_RC", "T_ARG1", - "T_DELTA" + "T_DELTA", + "T_FEE" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_FIXFX", - "desc": "외화 자동보정: ac_fx 의 원화환산액과 원통화*환율 이론치의 차이를 산출.", + "desc": "업무: 외화 자동보정: ac_fx 의 원화환산액과 원통화*환율 이론치의 차이를 산출.", "in": [ "T_BIZDATE", "T_PURCHASE_ID" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", "T_DELTA" @@ -15857,11 +22040,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_FIXROUND", - "desc": "반올림 자동보정: 절대차 1 이하 단수 차이를 수수료조정(ac_fee_adj)으로 상계 기록.", + "desc": "업무: 반올림 자동보정: 절대차 1 이하 단수 차이를 수수료조정(ac_fee_adj)으로 상계 기록.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -15869,11 +22053,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_FIXSIGN", - "desc": "부호 자동보정: 부호 반전 rc_diff 라인의 차액 부호를 정정하여 재기록.", + "desc": "업무: 부호 자동보정: 부호 반전 rc_diff 라인의 차액 부호를 정정하여 재기록.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -15881,11 +22066,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_FXDIFF", - "desc": "외화 환산 차이: 당일 ac_fx 원화환산 합계와 원통화*환율 이론합계의 차이를 집계.", + "desc": "업무: 외화 환산 차이: 당일 ac_fx 원화환산 합계와 원통화*환율 이론합계의 차이를 집계.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", "T_DELTA" @@ -15895,27 +22081,28 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_FXRATECHK", - "desc": "외화 환율 검증: 최신 외화 거래의 원통화 금액 x 환율(bp) 재계산 값이 저장된", + "desc": "업무: 환율 재계산 검증.", "in": [ "T_PURCHASE_ID" ], "out": [ - "T_FX_AMT", + "T_RC", "T_AMT1", "T_AMT2", - "T_RC" + "T_FX_AMT" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_FXRECON", - "desc": "외화 대사: 외화 거래(ac_fx)의 원화 환산 금액 합계와 매입 금액을 비교하여", + "desc": "업무: 외화 원화환산 대사.", "in": [ - "T_PURCHASE_ID", - "T_BIZDATE" + "T_BIZDATE", + "T_PURCHASE_ID" ], "out": [ + "T_RC", "T_AMT1", "T_AMT2", "T_ID1" @@ -15925,11 +22112,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_GAPSCAN", - "desc": "대사 누락 스캔: 대사완료(M) 상태인데 대사 로그가 하나도 없는 당일 매입", + "desc": "업무: 상태 M 인데 대사 로그 없는 누락 스캔.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", "T_ID1" ] @@ -15938,23 +22126,26 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_HOLD", - "desc": "* RC_HOLD.pgc - rc 모듈 서비스 RC_HOLD (one service = one file).", + "desc": "업무: 대사 보류 원장 기록. (KEEP)", "in": [ + "T_BIZDATE", "T_PURCHASE_ID", - "T_NET", - "T_BIZDATE" + "T_NET" ], - "out": [] + "out": [ + "T_RC" + ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_HOLDAGE", - "desc": "보류 장기화 조회: 기준일 이전부터 남아 있는 HOLD 대사 건수와 가장 오래된", + "desc": "업무: 장기 보류(HOLD) 건수 + 최고령 보류일.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -15962,12 +22153,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_HOLDCLR", - "desc": "대사 보류 해제: 보류(H) 매입을 접수(A)로 되돌리고 관련 예외를 해결 처리.", + "desc": "업무: 대사 보류 해제: 보류(H) 매입을 접수(A)로 되돌리고 관련 예외를 해결 처리.", "in": [ "T_BIZDATE", "T_PURCHASE_ID" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -15975,11 +22167,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_HOLDLIST", - "desc": "보류 목록 현황: 당일 보류(H) 상태 매입의 건수와 순액 합계를 반환.", + "desc": "업무: 보류 목록 현황: 당일 보류(H) 상태 매입의 건수와 순액 합계를 반환.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", "T_NET" ] @@ -15988,12 +22181,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_HOLDRSN", - "desc": "대사 보류 사유별 현황: 특정 사유의 당일 예외 건수를 미해결/해결로 나누어", + "desc": "업무: 보류 사유별 미해결/해결 건수.", "in": [ "T_BIZDATE", "T_REASON" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", "T_COUNT" @@ -16003,13 +22197,14 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_HOLDSET", - "desc": "대사 보류 설정: 지정 매입 상태를 보류(H)로 변경하고 보류 예외를 사유와 함께 등록.", + "desc": "업무: 대사 보류 설정: 지정 매입 상태를 보류(H)로 변경하고 보류 예외를 사유와 함께 등록.", "in": [ "T_BIZDATE", "T_PURCHASE_ID", "T_REASON" ], "out": [ + "T_RC", "T_ID1" ] }, @@ -16017,28 +22212,30 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_INSTRECON", - "desc": "할부 대사: 할부 개월수(months) 대비 등록된 회차(seq_no) 수를 비교하여", + "desc": "업무: 할부 회차 완납 대사.", "in": [ - "T_PURCHASE_ID", - "T_BIZDATE" + "T_BIZDATE", + "T_PURCHASE_ID" ], "out": [ - "T_INSTALL_N", + "T_RC", "T_COUNT", "T_GROSS", - "T_ID1" + "T_ID1", + "T_INSTALL_N" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_ISSDIFF", - "desc": "발급사별 차이 분석: 발급사 단위 매입 순액과 정산 순액 차이를 계산해 반환.", + "desc": "업무: 발급사별 차이 분석: 발급사 단위 매입 순액과 정산 순액 차이를 계산해 반환.", "in": [ "T_BIZDATE", "T_ISSUER" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", "T_DELTA" @@ -16048,12 +22245,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_ISSRECON", - "desc": "발급사별 대사: 지정 발급사의 당일 매입 순액과 정산 순액을 발급사 단위로 대조.", + "desc": "업무: 발급사별 대사: 지정 발급사의 당일 매입 순액과 정산 순액을 발급사 단위로 대조.", "in": [ "T_BIZDATE", "T_ISSUER" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", "T_DELTA" @@ -16063,11 +22261,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_KINDSPLIT", - "desc": "차이 유형 분류: 당일 rc_diff 를 금액(AMT)/건수(CNT) 유형으로 나누어 각각 집계.", + "desc": "업무: 차이 유형 분류: 당일 rc_diff 를 금액(AMT)/건수(CNT) 유형으로 나누어 각각 집계.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", "T_COUNT" @@ -16077,11 +22276,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_LEDGERBAL", - "desc": "원장 축 잔액 대사: 당일 RECON/SETTLE/XFER 기표 합계의 순잔액이 0 인지 확인.", + "desc": "업무: 원장 축 잔액 대사: 당일 RECON/SETTLE/XFER 기표 합계의 순잔액이 0 인지 확인.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", "T_ARG3", @@ -16092,39 +22292,42 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_LEDGERLINK", - "desc": "* RC_LEDGERLINK.pgc - rc 모듈 서비스 RC_LEDGERLINK (one service = one file).", - "in": [ - "T_PURCHASE_ID", - "T_NET", - "T_BIZDATE" - ], - "out": [] - }, - { - "mod": "rc", - "modnm": "대사", - "svc": "RC_LIMITCHK", - "desc": "가맹점 한도 대비 대사: 가맹점 일한도(daily_limit)와 당일 매입(취소 제외)", + "desc": "업무: 대사 원장 연계 기록 (dbio).", "in": [ "T_BIZDATE", - "T_MERCHANT" + "T_PURCHASE_ID", + "T_NET" ], "out": [ - "T_AMT1", - "T_AMT2", "T_RC" ] }, { "mod": "rc", "modnm": "대사", - "svc": "RC_MANAPPRV", - "desc": "수작업 대사 승인: HOLD 상태의 MANREG 수작업 대사 1건을 MATCHED 로 확정하고", + "svc": "RC_LIMITCHK", + "desc": "업무: 가맹점 일한도 초과 검증.", "in": [ - "T_ID1", - "T_BIZDATE" + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", + "T_AMT1", + "T_AMT2" + ] + }, + { + "mod": "rc", + "modnm": "대사", + "svc": "RC_MANAPPRV", + "desc": "업무: 수작업 대사 승인 (HOLD -> MATCHED).", + "in": [ + "T_BIZDATE", + "T_ID1" + ], + "out": [ + "T_RC", "T_COUNT", "T_PURCHASE_ID" ] @@ -16133,12 +22336,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_MANCANCEL", - "desc": "수작업 신청 취소: 아직 확정되지 않은 수작업 HOLD 신청 로그를 삭제.", + "desc": "업무: 수작업 신청 취소: 아직 확정되지 않은 수작업 HOLD 신청 로그를 삭제.", "in": [ "T_BIZDATE", "T_PURCHASE_ID" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -16146,11 +22350,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_MANLIST", - "desc": "수작업 신청 현황: 당일 수작업 대사 로그를 상태별(HOLD/확정)로 나누어 건수 집계.", + "desc": "업무: 수작업 신청 현황: 당일 수작업 대사 로그를 상태별(HOLD/확정)로 나누어 건수 집계.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2" ] @@ -16159,12 +22364,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_MANNG", - "desc": "수작업 대사 반려: 해당 매입의 수작업 HOLD 로그를 ROLLBACK 으로 반려 처리.", + "desc": "업무: 수작업 대사 반려: 해당 매입의 수작업 HOLD 로그를 ROLLBACK 으로 반려 처리.", "in": [ "T_BIZDATE", "T_PURCHASE_ID" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -16172,12 +22378,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_MANOK", - "desc": "수작업 대사 확정: 해당 매입의 수작업 HOLD 로그를 CONFIRMED 로 승인 처리.", + "desc": "업무: 수작업 대사 확정: 해당 매입의 수작업 HOLD 로그를 CONFIRMED 로 승인 처리.", "in": [ "T_BIZDATE", "T_PURCHASE_ID" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -16185,14 +22392,15 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_MANREG", - "desc": "수작업 대사 등록: 담당자가 입력한 수작업 대사를 HOLD 상태(승인 대기)로", + "desc": "업무: 수작업 대사 등록 (승인 대기 HOLD).", "in": [ - "T_PURCHASE_ID", "T_BIZDATE", - "T_REASON", - "T_NET" + "T_PURCHASE_ID", + "T_NET", + "T_REASON" ], "out": [ + "T_RC", "T_ID1" ] }, @@ -16200,13 +22408,14 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_MANREJECT", - "desc": "수작업 대사 반려: 승인 대기(HOLD) 중인 수작업 대사를 ROLLBACK 처리하고,", + "desc": "업무: 수작업 대사 반려 (HOLD -> ROLLBACK + 예외).", "in": [ - "T_ID1", "T_BIZDATE", - "T_REASON" + "T_REASON", + "T_ID1" ], "out": [ + "T_RC", "T_ID2", "T_PURCHASE_ID" ] @@ -16215,12 +22424,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_MANREQ", - "desc": "수작업 대사 신청: 지정 매입에 대해 수작업 대사 요청을 HOLD 상태 로그로 등록.", + "desc": "업무: 수작업 대사 신청: 지정 매입에 대해 수작업 대사 요청을 HOLD 상태 로그로 등록.", "in": [ "T_BIZDATE", "T_PURCHASE_ID" ], "out": [ + "T_RC", "T_ID1" ] }, @@ -16228,13 +22438,14 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_MANUAL", - "desc": "* RC_MANUAL.pgc - rc 모듈 서비스 RC_MANUAL (one service = one file).", + "desc": "업무: 수동 대사 로그.", "in": [ + "T_BIZDATE", "T_PURCHASE_ID", - "T_NET", - "T_BIZDATE" + "T_NET" ], "out": [ + "T_RC", "T_ID1" ] }, @@ -16242,12 +22453,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_MGRPDIFF", - "desc": "가맹점그룹별 차이 분석: 가맹점 접두 그룹의 매입 순액과 정산 순액 차액을 집계.", + "desc": "업무: 가맹점그룹별 차이 분석: 가맹점 접두 그룹의 매입 순액과 정산 순액 차액을 집계.", "in": [ "T_BIZDATE", "T_KEY1" ], "out": [ + "T_RC", "T_DELTA" ] }, @@ -16255,12 +22467,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_MGRPRECON", - "desc": "가맹점그룹별 대사: 가맹점 ID 접두 그룹(prefix) 단위로 매입 순액을 묶어 대사.", + "desc": "업무: 가맹점그룹별 대사: 가맹점 ID 접두 그룹(prefix) 단위로 매입 순액을 묶어 대사.", "in": [ "T_BIZDATE", "T_KEY1" ], "out": [ + "T_RC", "T_NET" ] }, @@ -16268,11 +22481,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_NOTIFY", - "desc": "대사 결과 통보: 당일 대사 요약(rc_summary)을 읽어 통보 메시지를 구성하고,", + "desc": "업무: 대사 결과 요약 통보 (경보 발송).", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", "T_GROSS" @@ -16282,11 +22496,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_ORPHAN", - "desc": "고아 매입 검출: 승인(approval) 이 전혀 없는 당일 매입을 NOT EXISTS 로", + "desc": "업무: 승인 없는 고아 매입 적재.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -16294,11 +22509,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_OVERAMT", - "desc": "과다 차이 분석: 당일 rc_diff 중 실제가 기대를 초과(diff_amt<0)한 과다 건수/합계.", + "desc": "업무: 과다 차이 분석: 당일 rc_diff 중 실제가 기대를 초과(diff_amt<0)한 과다 건수/합계.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", "T_DELTA" ] @@ -16307,14 +22523,15 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_PARTACQ", - "desc": "부분매입 대사: 분할 매입(ac_install 회차) 합계가 원매입 금액과 일치하는지", + "desc": "업무: 부분매입(회차 합계) 대사.", "in": [ - "T_PURCHASE_ID", - "T_BIZDATE" + "T_BIZDATE", + "T_PURCHASE_ID" ], "out": [ - "T_COUNT", + "T_RC", "T_AMT2", + "T_COUNT", "T_ID1" ] }, @@ -16322,11 +22539,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_PARTDEP", - "desc": "부분 입금 대사: 매입 순액보다 RECON 입금 누계가 작은 부분입금 건수를 집계.", + "desc": "업무: 부분 입금 대사: 매입 순액보다 RECON 입금 누계가 작은 부분입금 건수를 집계.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -16334,13 +22552,14 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_PARTIAL", - "desc": "* RC_PARTIAL.pgc - rc 모듈 서비스 RC_PARTIAL (one service = one file).", + "desc": "업무: 부분 대사 로그.", "in": [ + "T_BIZDATE", "T_PURCHASE_ID", - "T_AMOUNT", - "T_BIZDATE" + "T_AMOUNT" ], "out": [ + "T_RC", "T_ID1" ] }, @@ -16348,11 +22567,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_PAYGAP", - "desc": "지급 누락 감지: 대사완료(M) 매입 중 정산 레코드가 없는 미지급 건수를 반환.", + "desc": "업무: 지급 누락 감지: 대사완료(M) 매입 중 정산 레코드가 없는 미지급 건수를 반환.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -16360,26 +22580,28 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_PAYLEDGER", - "desc": "지급-원장 축 대사: 정산 순액과 원장 SETTLE 기표 합계가 일치하는지 검증하고 차액 반환.", + "desc": "업무: 지급-원장 축 대사: 정산 순액과 원장 SETTLE 기표 합계가 일치하는지 검증하고 차액 반환.", "in": [ "T_BIZDATE" ], "out": [ - "T_GROSS", + "T_RC", "T_ARG1", - "T_DELTA" + "T_DELTA", + "T_GROSS" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_PAYMATCH", - "desc": "지급 대사: 매입 net 대비 원장(ledger) SETTLE/XFER 지급 합계를 비교하고,", + "desc": "업무: 지급(원장) 대사 + 차이 적재.", "in": [ - "T_PURCHASE_ID", - "T_BIZDATE" + "T_BIZDATE", + "T_PURCHASE_ID" ], "out": [ + "T_RC", "T_DELTA", "T_ID1" ] @@ -16388,20 +22610,20 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_POSRECON", - "desc": "POS 채널 대사: POS 채널 매입의 대사완료율을 건수 기준으로 산출(백분율).", + "desc": "업무: POS 채널 대사: POS 채널 매입의 대사완료율을 건수 기준으로 산출(백분율).", "in": [ "T_BIZDATE" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_COUNT" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_PROGRESS", - "desc": "* RC_PROGRESS.pgc - rc 모듈 서비스 RC_PROGRESS (one service = one file).", + "desc": "업무: 진행률(%) = MATCHED / 전체매입 * 100.", "in": [ "T_BIZDATE" ], @@ -16413,36 +22635,36 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_RATEBYISS", - "desc": "발급사별 대사율: 요청 발급사의 당일 대사 로그를 매입과 조인하여 총건수와", + "desc": "업무: 발급사별 대사율(bp).", "in": [ "T_BIZDATE", "T_ISSUER" ], "out": [ - "T_COUNT", + "T_RC", "T_ARG1", - "T_RC" + "T_COUNT" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_RATECHAN", - "desc": "채널별 대사율: 지정 채널의 당일 매입 대비 대사완료 비율을 백분율로 산출.", + "desc": "업무: 채널별 대사율: 지정 채널의 당일 매입 대비 대사완료 비율을 백분율로 산출.", "in": [ "T_BIZDATE", "T_CHANNEL" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_COUNT" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_RATEDAY", - "desc": "일별 대사율: 당일 매입 대비 대사완료 건수 비율을 백분율로 산출.", + "desc": "업무: 일별 대사율: 당일 매입 대비 대사완료 건수 비율을 백분율로 산출.", "in": [ "T_BIZDATE" ], @@ -16454,66 +22676,67 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_RATEMERCH", - "desc": "가맹점별 대사율: 지정 가맹점의 당일 매입 대비 대사완료 비율을 백분율로 산출.", + "desc": "업무: 가맹점별 대사율: 지정 가맹점의 당일 매입 대비 대사완료 비율을 백분율로 산출.", "in": [ "T_BIZDATE", "T_MERCHANT" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_COUNT" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_RATEMON", - "desc": "월별 대사율: 기준일이 속한 달 전체 매입 대비 대사완료 비율을 백분율로 산출.", + "desc": "업무: 월별 대사율: 기준일이 속한 달 전체 매입 대비 대사완료 비율을 백분율로 산출.", "in": [ "T_BIZDATE" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_COUNT" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_RATESTAT", - "desc": "대사율 통계: 당일 전체 대사 로그 대비 MATCHED 비율을 bp(1/10000) 단위로", + "desc": "업무: 당일 대사율(bp) 통계.", "in": [ "T_BIZDATE" ], "out": [ - "T_COUNT", + "T_RC", "T_ARG1", - "T_RC" + "T_COUNT" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_RATEWEEK", - "desc": "주별 대사율: 기준일 포함 최근 7영업일 매입 대비 대사완료 비율을 백분율로 산출.", + "desc": "업무: 주별 대사율: 기준일 포함 최근 7영업일 매입 대비 대사완료 비율을 백분율로 산출.", "in": [ "T_BIZDATE" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_COUNT" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_RECORR", - "desc": "정정후 재대사: 수수료조정이 반영된 매입을 대상으로 순액 재검증 후 로그를 남긴다.", + "desc": "업무: 정정후 재대사: 수수료조정이 반영된 매입을 대상으로 순액 재검증 후 로그를 남긴다.", "in": [ "T_BIZDATE", "T_PURCHASE_ID" ], "out": [ + "T_RC", "T_NET" ] }, @@ -16521,12 +22744,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_REDIFF", - "desc": "차이발생 재대사: 지정 매입의 기대 순액과 SETTLE 실적 차이를 재산출해 rc_diff 갱신.", + "desc": "업무: 차이발생 재대사: 지정 매입의 기대 순액과 SETTLE 실적 차이를 재산출해 rc_diff 갱신.", "in": [ "T_BIZDATE", "T_PURCHASE_ID" ], "out": [ + "T_RC", "T_DELTA" ] }, @@ -16534,38 +22758,40 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_REFULL", - "desc": "전체 재대사: 당일 전체 매입 대비 대사완료(M/S) 비율을 재계산해 진척률로 반환.", + "desc": "업무: 전체 재대사: 당일 전체 매입 대비 대사완료(M/S) 비율을 재계산해 진척률로 반환.", "in": [ "T_BIZDATE" ], "out": [ - "T_COUNT", + "T_RC", "T_ARG1", - "T_RC" + "T_COUNT" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_REFUNDREC", - "desc": "환불분 대사: 취소 유형이 REFUND 인 환불 금액과 건수를 별도로 대사 집계.", + "desc": "업무: 환불분 대사: 취소 유형이 REFUND 인 환불 금액과 건수를 별도로 대사 집계.", "in": [ "T_BIZDATE" ], "out": [ - "T_COUNT", - "T_AMOUNT" + "T_RC", + "T_AMOUNT", + "T_COUNT" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_RELATE", - "desc": "지연도착 재대사: 원장 기표가 매입일보다 늦게 도착한 지연 건을 재대사 대상으로 집계.", + "desc": "업무: 지연도착 재대사: 원장 기표가 매입일보다 늦게 도착한 지연 건을 재대사 대상으로 집계.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -16573,33 +22799,38 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_RELEASE", - "desc": "* RC_RELEASE.pgc - rc 모듈 서비스 RC_RELEASE (one service = one file).", + "desc": "업무: 대사 보류 해제 (HOLD -> RELEASED).", "in": [ "T_PURCHASE_ID" ], - "out": [] + "out": [ + "T_RC" + ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_REMATCH", - "desc": "* RC_REMATCH.pgc - rc 모듈 서비스 RC_REMATCH (one service = one file).", + "desc": "업무: 재대사 원장 기록. (KEEP)", "in": [ + "T_BIZDATE", "T_PURCHASE_ID", - "T_NET", - "T_BIZDATE" + "T_NET" ], - "out": [] + "out": [ + "T_RC" + ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_REOPEN", - "desc": "마감 재개: 확정(CONFIRMED)된 당일 대사 건을 다시 MATCHED 로 되돌려 마감을", + "desc": "업무: 일마감 해제 (CONFIRMED -> MATCHED).", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -16607,22 +22838,25 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_REPROC", - "desc": "재처리: EXCEPTION 대사 로그를 MATCHED 로 되돌림. */", + "desc": "업무: 재처리: EXCEPTION 대사 로그를 MATCHED 로 되돌림.", "in": [ "T_PURCHASE_ID" ], - "out": [] + "out": [ + "T_RC" + ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_RERUN", - "desc": "임계초과 재대사 실행: 임계값을 넘는 당일 차이 건들을 커서로 순회하며", + "desc": "업무: 임계초과 건 재대사 등록 (커서 순회).", "in": [ - "T_AMT1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMT1" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -16630,14 +22864,15 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_RETRO", - "desc": "소급 대사: 과거 영업일에 보류/예외로 남은 특정 매입의 대사를 새 영업일로", + "desc": "업무: 과거 보류/예외 건 소급 확정.", "in": [ + "T_BIZDATE", "T_PURCHASE_ID", "T_NET", - "T_STR1", - "T_BIZDATE" + "T_STR1" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -16645,13 +22880,14 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_RETURN", - "desc": "반품 대사: 반품 원장 + 음수 대사 로그. */", + "desc": "업무: 반품 대사: 반품 원장 + 음수 대사 로그.", "in": [ + "T_BIZDATE", "T_PURCHASE_ID", - "T_AMOUNT", - "T_BIZDATE" + "T_AMOUNT" ], "out": [ + "T_RC", "T_ID1" ] }, @@ -16659,21 +22895,24 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_ROLLBACK", - "desc": "* RC_ROLLBACK.pgc - rc 모듈 서비스 RC_ROLLBACK (one service = one file).", + "desc": "업무: 대사 롤백 (-> ROLLBACK 상태).", "in": [ "T_PURCHASE_ID" ], - "out": [] + "out": [ + "T_RC" + ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_ROUNDDIFF", - "desc": "반올림 차이 분석: 절대차가 1 이하인 단수 차이 건을 반올림 오차로 분류해 집계.", + "desc": "업무: 반올림 차이 분석: 절대차가 1 이하인 단수 차이 건을 반올림 오차로 분류해 집계.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -16681,11 +22920,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_SIGNDIFF", - "desc": "부호 오류 차이 분석: 기대와 실제의 부호가 서로 반대인 rc_diff 라인을 검출.", + "desc": "업무: 부호 오류 차이 분석: 기대와 실제의 부호가 서로 반대인 rc_diff 라인을 검출.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -16693,14 +22933,15 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_SNAPSHOT", - "desc": "대사 스냅샷: 당일 대사 로그의 총건수/MATCHED 건수/MATCHED 금액을 CASE", + "desc": "업무: 현재 시점 대사 현황 스냅샷 upsert.", "in": [ "T_BIZDATE" ], "out": [ - "T_COUNT", + "T_RC", "T_ARG1", "T_ARG2", + "T_COUNT", "T_GROSS" ] }, @@ -16708,48 +22949,51 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_STATS", - "desc": "* RC_STATS.pgc - rc 모듈 서비스 RC_STATS (one service = one file).", + "desc": "업무: MATCHED/EXCEPTION 통계 (dbio count).", "in": [ "T_BIZDATE" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_COUNT" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_STATUS", - "desc": "* RC_STATUS.pgc - rc 모듈 서비스 RC_STATUS (one service = one file).", + "desc": "업무: 최근 대사 상태 조회.", "in": [ "T_PURCHASE_ID" ], - "out": [] - }, - { - "mod": "rc", - "modnm": "대사", - "svc": "RC_SUMCHK", - "desc": "요약-상세 정합성 검증: rc_summary 의 확정 matched 건수와 대사 로그 실측", - "in": [ - "T_BIZDATE" - ], "out": [ - "T_ARG1", - "T_ARG2", "T_RC" ] }, { "mod": "rc", "modnm": "대사", - "svc": "RC_SUMMARY", - "desc": "* RC_SUMMARY.pgc - rc 모듈 서비스 RC_SUMMARY (one service = one file).", + "svc": "RC_SUMCHK", + "desc": "업무: 요약 대 실측 정합성 검증.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG1", + "T_ARG2" + ] + }, + { + "mod": "rc", + "modnm": "대사", + "svc": "RC_SUMMARY", + "desc": "업무: 대사 요약 upsert (dbio).", + "in": [ + "T_BIZDATE" + ], + "out": [ + "T_RC", "T_COUNT", "T_GROSS" ] @@ -16758,12 +23002,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_TAXRECON", - "desc": "세금계산서 대사: 발행된 세금계산서(공급가+부가세) 합계와 매입 금액을", + "desc": "업무: 세금계산서(공급가+VAT) 대사.", "in": [ - "T_PURCHASE_ID", - "T_BIZDATE" + "T_BIZDATE", + "T_PURCHASE_ID" ], "out": [ + "T_RC", "T_DELTA", "T_ID1" ] @@ -16772,12 +23017,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_THRESHOLD", - "desc": "임계초과 검출: 당일 차이 중 |차이금액| 이 임계값을 초과하는 건수를 세고,", + "desc": "업무: 임계초과 차이 검출 + CRIT 경보.", "in": [ - "T_AMT1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMT1" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -16785,11 +23031,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_TIMEDIFF", - "desc": "기표시점 차이 분석: 매입 영업일과 원장 기표 영업일이 다른 이월/선기표 건수를 집계.", + "desc": "업무: 기표시점 차이 분석: 매입 영업일과 원장 기표 영업일이 다른 이월/선기표 건수를 집계.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -16797,12 +23044,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_TOLERANCE", - "desc": "허용오차 대사: |차이| <= 허용치면 MATCHED. */", + "desc": "업무: 허용오차 대사: |차이| <= 허용치면 MATCHED.", "in": [ + "T_BIZDATE", "T_PURCHASE_ID", "T_DELTA", - "T_ARG1", - "T_BIZDATE" + "T_ARG1" ], "out": [ "T_RC" @@ -16812,11 +23059,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_TOPDIFF", - "desc": "차이 상위 조회: 당일 |차이금액| 의 최대값과 평균값을 구한 뒤 평균을 넘는", + "desc": "업무: 최대/평균 차이 + 평균 초과 건수.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_AMT1", "T_AMT2", "T_COUNT" @@ -16826,12 +23074,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_TRAILQRY", - "desc": "대사 감사추적 조회: 지정 매입의 전체 대사 로그 이력 건수와 최종 로그 ID 를 반환.", + "desc": "업무: 대사 감사추적 조회: 지정 매입의 전체 대사 로그 이력 건수와 최종 로그 ID 를 반환.", "in": [ "T_BIZDATE", "T_PURCHASE_ID" ], "out": [ + "T_RC", "T_COUNT", "T_ID1" ] @@ -16840,11 +23089,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_UNDERAMT", - "desc": "과소 차이 분석: 당일 rc_diff 중 실제가 기대에 미달(diff_amt>0)한 과소 건수/합계.", + "desc": "업무: 과소 차이 분석: 당일 rc_diff 중 실제가 기대에 미달(diff_amt>0)한 과소 건수/합계.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", "T_DELTA" ] @@ -16853,51 +23103,55 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_UNMATCH", - "desc": "* RC_UNMATCH.pgc - rc 모듈 서비스 RC_UNMATCH (one service = one file).", + "desc": "업무: 미대사 건수 조회. (KEEP)", "in": [ "T_BIZDATE" ], - "out": [] - }, - { - "mod": "rc", - "modnm": "대사", - "svc": "RC_VOIDREC", - "desc": "VOID 매입 대사: 매입 상태가 V(void) 인지와 VOID 유형 취소 증빙 존재를", - "in": [ - "T_PURCHASE_ID", - "T_BIZDATE" - ], "out": [ - "T_ID1", - "T_RC" + "T_RC", + "T_COUNT" ] }, { "mod": "rc", "modnm": "대사", - "svc": "RC_WHTRECON", - "desc": "원천세 대사: 매입의 원천징수(ac_wht) 세액 합계가 정산 net 금액을 넘지", + "svc": "RC_VOIDREC", + "desc": "업무: VOID 매입 대사 (상태 V + VOID 취소 증빙).", "in": [ - "T_PURCHASE_ID", - "T_BIZDATE" + "T_BIZDATE", + "T_PURCHASE_ID" ], "out": [ - "T_TAX", - "T_NET", "T_RC", "T_ID1" ] }, + { + "mod": "rc", + "modnm": "대사", + "svc": "RC_WHTRECON", + "desc": "업무: 원천세 한도 대사.", + "in": [ + "T_BIZDATE", + "T_PURCHASE_ID" + ], + "out": [ + "T_RC", + "T_ID1", + "T_NET", + "T_TAX" + ] + }, { "mod": "rc", "modnm": "대사", "svc": "RC_ZEROAMT", - "desc": "이상금액 검출: 당일 매입 중 금액 0원 또는 음수 net 인 이상 거래를 찾아", + "desc": "업무: 0원/음수 net 이상 거래 예외 적재.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -16905,13 +23159,18 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RECONCILE", - "desc": "* RECONCILE.pgc - rc 모듈 서비스 RECONCILE (one service = one file).", + "desc": "업무: RECONCILE 대사 처리", "in": [ + "T_BIZDATE", "T_PURCHASE_ID", "T_NET", - "T_BIZDATE" + "T_AMOUNT", + "T_FEE", + "T_MERCHANT" ], - "out": [] + "out": [ + "T_RC" + ] }, { "mod": "st", @@ -16932,957 +23191,1832 @@ const SVC_SCREENS = [ "mod": "st", "modnm": "정산/수수료", "svc": "ST_ADJHIST", - "desc": "조정 이력 조회: 매입에 걸린 정산 조정을 최신순 커서로 순회해 건수·증감", + "desc": "매입별 조정 이력 요약. (심화판).", "in": [ - "T_PURCHASE_ID" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", + "T_DELTA", + "T_AMT1", + "T_AMT2", "T_COUNT", - "T_DELTA" + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_ADJREVERSE", - "desc": "조정 취소(역분개): 기존 정산 조정(T_ADJ) 행을 조회해 동일 금액의 반대 부호 조정을", + "desc": "기존 조정을 반대부호로 상쇄. (심화판).", "in": [ - "T_ADJ", - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", "T_ADJ", - "T_DELTA" + "T_DELTA", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_ADJUST", - "desc": "* ST_ADJUST.pgc - st 모듈 서비스 ST_ADJUST (one service = one file).", + "desc": "정산 조정 (가감) 기록. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_DELTA", + "T_MERCHANT", "T_BIZDATE", - "T_REASON" + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_ADJ" + "T_RC", + "T_ARG6", + "T_ADJ", + "T_DELTA", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_ADVANCE", - "desc": "선지급 예약: 지급 레코드(SCHEDULED) 생성 (dbio). */", + "desc": "선지급 예약: 지급 레코드(SCHEDULED) 생성. (심화판).", "in": [ - "T_NET", "T_MERCHANT", "T_BIZDATE", - "T_KEY1" + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_ID1" + "T_RC", + "T_ARG6", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_APPEALSTAT", - "desc": "이의신청 통계: 정산 조정 중 이의신청 관련(reason LIKE 'DISPUTE%') 건수와 청구", + "desc": "이의신청 조정 건수/금액 통계. (심화판).", "in": [ - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_COUNT", + "T_RC", + "T_ARG6", "T_DELTA", - "T_RC" + "T_AMT1", + "T_AMT2", + "T_COUNT", + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_APPROVE", - "desc": "정산 승인 워크플로: 최신 정산 상태를 검증해 CONFIRMED 인 건만 APPROVED 로", + "desc": "정산 승인 (CONFIRMED -> APPROVED). (심화판).", "in": [ - "T_PURCHASE_ID" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_RC" + "T_RC", + "T_ARG6", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_APPROVEALL", - "desc": "정산 일괄 승인: 당일 지급 예정(SCHEDULED) 전 건을 한 번에 지급 완료(PAID)로", + "desc": "당일 지급 예정 일괄 승인(->PAID). (심화판).", "in": [ - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG6", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_BADDEBT", - "desc": "대손 상각: 특정 가맹점의 장기 보류(HELD) 지급 건을 커서로 순회해 총 미수액을", + "desc": "장기 미수 대손 상각(상계+취소). (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", "T_ADJ", - "T_COUNT", - "T_DELTA" + "T_DELTA", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_BANDRATE", - "desc": "* ST_BANDRATE.pgc - st 모듈 서비스 ST_BANDRATE (one service = one file).", + "desc": "금액 구간 요율(bps) 조회. (심화판).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", "T_AMOUNT", - "T_STR1" + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_RC" + "T_RC", + "T_ARG6", + "T_FEE", + "T_AMT1", + "T_AMT2", + "T_COUNT", + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_BANDSIM", - "desc": "구간별 요율 시뮬레이션: 입력 금액에 적용 가능한(band_min <= amount) 모든 구간 중", + "desc": "적용가능 구간 중 최고 요율 시뮬. (심화판).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", "T_AMOUNT", - "T_STR1" + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", + "T_FEE", + "T_AMT1", + "T_AMT2", "T_COUNT", - "T_FEE" + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_BIWEEKLY", - "desc": "격주 정산: 가맹점 당일 정산 net 합계를 집계하고 영업일 기준 10일 뒤(약 2주)", + "desc": "2주 주기 net 합계 지급 예정. (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", "T_ID1", - "T_NET" + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_BYMERCH", - "desc": "* ST_BYMERCH.pgc - st 모듈 서비스 ST_BYMERCH (one service = one file).", + "desc": "가맹점별 정산 집계 upsert. (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", + "T_AMT1", + "T_AMT2", "T_COUNT", - "T_GROSS" + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_CANCEL", - "desc": "정산 취소: 상태 CANCELLED + 취소 조정 (dbio). */", + "desc": "정산 취소: 상태 CANCELLED + 취소 조정. (심화판).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", "T_PURCHASE_ID", - "T_BIZDATE" + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], - "out": [] + "out": [ + "T_RC", + "T_ARG6", + "T_ARG3", + "T_ARG5", + "T_ARG4" + ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_CAP", - "desc": "수수료 상한 적용: 상한 초과분을 감액 조정 (dbio). */", + "desc": "수수료 상한 적용: 상한 초과분을 감액 조정. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", "T_FEE", - "T_ARG1", - "T_BIZDATE" + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_FEE", - "T_DELTA" + "T_RC", + "T_ARG6", + "T_ADJ", + "T_DELTA", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_CAPCHECK", - "desc": "수수료 상한 도달 점검: 수수료가 상한을 초과하는지 판정만 하고 초과분을", + "desc": "상한 초과 여부 판정 (조정 없음). (심화판).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", "T_FEE", - "T_ARG1" + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_DELTA", - "T_AMT1" + "T_ARG6", + "T_FEE", + "T_AMT1", + "T_AMT2", + "T_COUNT", + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_CARRYOVER", - "desc": "이월 정산: 대사완료(M) 상태이지만 정산 레코드가 없는 매입(NOT EXISTS)을", + "desc": "미정산분 익영업일 이월 예약. (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_COUNT", + "T_ARG6", "T_ID1", - "T_NET" + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_CASHBACK", - "desc": "캐시백 정산: 거래 금액에 캐시백율(T_ARG1 bps)을 적용한 캐시백을 가맹점 지급", + "desc": "캐시백 지급 예정 등록. (심화판).", "in": [ - "T_AMOUNT", - "T_ARG1", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", "T_ID1", - "T_AMOUNT" + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_CHAINSET", - "desc": "체인점 통합 정산: 업종/분류(T_CATEGORY)로 묶인 체인 매입의 정산 net 을 조인 집계해", + "desc": "업종 체인 통합 net 지급. (심화판).", "in": [ "T_MERCHANT", - "T_CATEGORY", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", "T_ID1", - "T_COUNT", - "T_NET" + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_CHARGEBACK", - "desc": "차지백 정산 처리: 차지백 금액(T_AMOUNT)을 음(-)의 정산 조정으로 회수하고 해당", + "desc": "차지백 회수 조정 + 지급 보류. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_AMOUNT", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_ADJ", - "T_DELTA" + "T_ARG6", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_CHNLSTAT", - "desc": "채널별 정산 통계 세분: 정산-매입 조인 커서로 채널(POS/EDC/EDI/FOREIGN)별", + "desc": "채널별 정산 net 분해 통계. (심화판).", "in": [ - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", + "T_AMT1", "T_COUNT", "T_GROSS", - "T_AMT1" + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_COLLATERAL", - "desc": "담보/보증금 잔액: 정산 상세의 DEPOSIT 라인을 합산해 가맹점 누적 보증금(담보)", + "desc": "가맹점 누적 보증금 잔액 조회. (심화판).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_AMOUNT", + "T_RC", + "T_ARG6", + "T_AMT1", + "T_AMT2", "T_COUNT", - "T_RC" + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_CONFIRM", - "desc": "* ST_CONFIRM.pgc - st 모듈 서비스 ST_CONFIRM (one service = one file).", + "desc": "정산 확정 (SETTLED -> CONFIRMED). (심화판).", "in": [ - "T_PURCHASE_ID" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], - "out": [] + "out": [ + "T_RC", + "T_ARG6", + "T_ARG3", + "T_ARG5", + "T_ARG4" + ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_COUPONADJ", - "desc": "쿠폰 조정 정산: 발급 쿠폰 금액(T_ARG1)을 가맹점 부담분으로 양(+)의 정산 조정에", + "desc": "쿠폰 사용액 정산 조정(가산). (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_ARG1", + "T_MERCHANT", "T_BIZDATE", - "T_STR1" + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", "T_ADJ", - "T_DELTA" + "T_DELTA", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_CXLRECALC", - "desc": "정산 취소분 재계산: ac_cancel 누계를 매입 net 에서 차감해 정산 net 을", + "desc": "취소 누계 반영 정산 net 재계산. (심화판).", "in": [ - "T_PURCHASE_ID" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", "T_NET", - "T_DELTA" + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_CYCLED1", - "desc": "D+1 정산주기: 당일 SETTLED 정산 net 합계를 산출해 익영업일(pay_date=D+1)", + "desc": "D+1 주기 지급 예약. (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", "T_ID1", - "T_NET" + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_CYCLED2", - "desc": "D+2 정산주기: 당일 CONFIRMED 확정 정산만 집계해 2영업일 뒤(D+2, 주말 스킵)", + "desc": "D+2 주기 지급 예약 (확정분만). (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_COUNT", + "T_ARG6", "T_ID1", - "T_NET" + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_CYCLED3", - "desc": "D+3 정산 주기: 영업일(T_BIZDATE)로부터 영업일 기준 3일 뒤 지급일을 산출해", + "desc": "D+3 영업일 지급 예정 등록. (심화판).", "in": [ - "T_NET", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_ID1" + "T_RC", + "T_ARG6", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_CYCLEMON", - "desc": "월간 정산주기: 대상 월(YYYY-MM)의 가맹점 정산 net 을 합산해 지급일 기준으로", + "desc": "월간 통합 지급 예약 (직접 INSERT). (심화판).", "in": [ "T_MERCHANT", "T_BIZDATE", - "T_KEY1" + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_COUNT", + "T_ARG6", "T_ID1", - "T_GROSS" + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_CYCLEQTR", - "desc": "분기 정산 주기: 기준일(T_BIZDATE) 이하의 미지급 정산 net 을 누적 합계로 집계해", + "desc": "기준일까지 누적 net 분기 지급. (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", "T_ID1", - "T_COUNT", - "T_NET" + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_CYCLESET", - "desc": "정산 주기 설정 조회: 가맹점 한도(daily_limit)를 기준으로 적용 정산 주기 코드", + "desc": "가맹점 한도 기반 정산주기 코드 파생. (심화판).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_AMOUNT" + "T_ARG6", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_CYCLEWEEK", - "desc": "주간 정산주기: 주 종료일 기준 직전 7일(취소 제외) 정산 net 을 합산해", + "desc": "주간(7일 윈도) 통합 지급 예약. (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", "T_ID1", - "T_COUNT", - "T_GROSS" + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_DAILYCLOSE", - "desc": "정산 일마감: 당일 정산·지급·조정 건수를 각각 검증 집계하고 정산 net 총액으로", + "desc": "정산 일마감 선언 (3원 집계 + 마감 원장). (심화판).", "in": [ - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", - "T_ARG1", - "T_ARG2", - "T_GROSS" + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_DEBTCOLLECT", - "desc": "채권 회수 정산: 회수 입금액을 양수 조정(COLLECT)으로 기록하고 원장에 COLLECT", + "desc": "채권 회수 입금 정산. (심화판).", "in": [ - "T_AMOUNT", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_ADJ", - "T_DELTA" + "T_RC", + "T_ARG6", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_DEBTNET", - "desc": "채권 상계: 가맹점의 지급 예정(SCHEDULED) 채무와 보류(HELD) 미수 채권을 각각", + "desc": "지급 채무 대 미수 채권 상계. (심화판).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_AMT1", - "T_AMT2", - "T_NET", - "T_RC" + "T_RC", + "T_ARG6", + "T_ADJ", + "T_DELTA", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_DELAYINT", - "desc": "* ST_DELAYINT.pgc - st 모듈 서비스 ST_DELAYINT (one service = one file).", + "desc": "지연이자(net * bps/10000 * 일수) 정산 상세 기록. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_NET", - "T_ARG1", - "T_ARG2", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL", + "T_ARG1" ], "out": [ - "T_AMOUNT" + "T_RC", + "T_ARG6", + "T_FEE", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_DEPOSITCUT", - "desc": "보증금 차감: 보증금 납부 예정액과 SCHEDULED 지급 가용액 중 작은 금액을", + "desc": "지급액 보증금 원천 차감. (심화판).", "in": [ - "T_AMT1", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", + "T_ADJ", "T_DELTA", - "T_ADJ" + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_DEPOSITSET", - "desc": "보증금 설정/차감: 가맹점 보증금 예치액(T_ARG1)을 정산 상세에 DEPOSIT 라인으로", + "desc": "보증금 예치 기록 + net 차감. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_NET", - "T_ARG1", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_AMOUNT", - "T_NET" + "T_ARG6", + "T_ADJ", + "T_DELTA", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_DIFFCHK", - "desc": "* ST_DIFFCHK.pgc - st 모듈 서비스 ST_DIFFCHK (one service = one file).", + "desc": "정산 net vs 매입 net 차이 점검. (심화판).", "in": [ - "T_PURCHASE_ID" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", "T_DELTA", - "T_RC" + "T_NET", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_DISPUTE", - "desc": "정산 이의신청 처리: 대상 매입의 가맹점을 역추적해 예정 지급을 보류(HELD)로", + "desc": "정산 이의신청 접수 (지급 보류 + 조정). (심화판).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", "T_DELTA", "T_PURCHASE_ID", - "T_BIZDATE", - "T_REASON" + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_ADJ" + "T_RC", + "T_ARG6", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_DISPUTERSV", - "desc": "이의신청 결과 반영: 판정(T_STATUS: A=인용/R=기각)에 따라 인용 시 청구액을 양(+)의", + "desc": "이의신청 인용/기각 후속 처리. (심화판).", "in": [ - "T_PURCHASE_ID", - "T_DELTA", "T_MERCHANT", - "T_STATUS", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_ADJ" + "T_RC", + "T_ARG6", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_DTLSUM", - "desc": "정산 상세 합계: 가맹점+수수료종류+영업일 단위로 상세 라인의 건수·합계·", + "desc": "정산 상세 다중 집계 (건수/합/min/max/평균). (심화판).", "in": [ "T_MERCHANT", + "T_BIZDATE", + "T_REASON", "T_STR1", - "T_BIZDATE" + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_COUNT", - "T_GROSS", + "T_RC", + "T_ARG6", "T_AMT1", "T_AMT2", - "T_AMT3" + "T_AMT3", + "T_COUNT", + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_EDIRECON", - "desc": "EDI 정산 대사: 가맹점 당일 EDI 문서(ac_edi) 청구 합계와 정산 집계", + "desc": "EDI 청구 vs 정산 집계 대사. (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", + "T_AMT1", + "T_AMT2", "T_COUNT", "T_GROSS", - "T_NET", - "T_DELTA", - "T_RC" + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_EVENTFEE", - "desc": "이벤트 수수료 감면: 이벤트 기간 거래에 대해 정상 요율의 절반을 적용한 감면", + "desc": "이벤트 반값 수수료 감면 기록. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_AMOUNT", - "T_ARG1", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", "T_FEE", - "T_RC" + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_FEEPREVIEW", - "desc": "수수료 사전 미리보기: 가맹점 등록 요율(mdr_bps)로 수수료·부가세·원천세와", + "desc": "정산 실수령 예상 견적. (심화판).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", "T_AMOUNT", - "T_MERCHANT" + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", "T_FEE", - "T_TAX", "T_AMT1", - "T_NET", - "T_RC" + "T_AMT2", + "T_COUNT", + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_FEERATIO", - "desc": "수수료율 실효 검증: 매입-가맹점 조인 한 번으로 실제 부과 수수료의 실효", + "desc": "실효 수수료율 vs 계약 요율 편차 검증. (심화판).", "in": [ - "T_PURCHASE_ID" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_ARG1", - "T_ARG2", - "T_DELTA", - "T_RC" + "T_RC", + "T_ARG6", + "T_FEE", + "T_AMT1", + "T_AMT2", + "T_COUNT", + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_FLATFEE", - "desc": "정액 수수료 적용: 거래 건마다 고정 금액 수수료를 정산 상세(st_settle_dtl)에", + "desc": "정액 수수료 적용 (고정액 정산 상세 기록). (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_ARG1", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", "T_FEE", - "T_ID1" + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_FLATVSPCT", - "desc": "정액 대 정률 비교: 정액(T_ARG1)과 정률(금액 x T_ARG2 bps)을 각각 계산해 더 저렴한", + "desc": "정액/정률 중 저가 방식 선택. (심화판).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", "T_AMOUNT", - "T_ARG1", - "T_ARG2" + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_AMT1", - "T_AMT2", + "T_RC", + "T_ARG6", "T_FEE", - "T_RC" + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_FXCONV", - "desc": "외화 환산: fx * rate/100 = KRW. */", + "desc": "외화 환산: fx * rate/100 = KRW. (심화판).", "in": [ - "T_FX_AMT", - "T_ARG1" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL", + "T_FX_AMT" ], "out": [ - "T_AMOUNT" + "T_RC", + "T_ARG6", + "T_FEE", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_FXSETTLE", - "desc": "해외 매입 정산: 매입에 걸린 외화 내역(ac_fx)의 원화 환산액을 합산해 FX", + "desc": "외화 환산액 정산 상세 기록. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL", + "T_FX_AMT" ], "out": [ "T_RC", - "T_AMOUNT", - "T_COUNT" + "T_ARG6", + "T_FEE", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_GRADESTAT", - "desc": "가맹점 등급별 통계: 가맹점 요율(mdr_bps) 그룹별 가맹점 수를 커서로 집계해 등급", - "in": [], - "out": [ - "T_COUNT", - "T_ARG1", + "desc": "mdr_bps 등급별 가맹점 분포. (심화판).", + "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", "T_ARG2", - "T_RC" + "T_CHANNEL" + ], + "out": [ + "T_RC", + "T_ARG6", + "T_AMT1", + "T_COUNT", + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_GROWTHINC", - "desc": "성장 인센티브: 당일 매출과 비교일(T_STR1) 매출을 비교해 증가분(성장분)에 대해서만", + "desc": "전기 대비 매출 성장분 인센티브. (심화판).", "in": [ - "T_ARG1", "T_MERCHANT", "T_BIZDATE", - "T_STR1" + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_DELTA", + "T_ARG6", "T_ADJ", - "T_AMT1" + "T_DELTA", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_GRPALLOC", - "desc": "그룹 정산 배분: 그룹 통합 지급총액(T_AMOUNT)을 구성원의 net 비중에 따라 안분해", + "desc": "net 비중 안분 후 구성원별 지급 등록. (심화판).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", "T_AMOUNT", - "T_KEY1", - "T_BIZDATE" + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_COUNT", - "T_AMOUNT" + "T_ARG6", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_GRPROLLUP", - "desc": "가맹점 그룹 집계 rollup: prefix(T_KEY1) 로 묶인 그룹 구성원의 당일 매출/수수료/net", + "desc": "그룹 구성원 매출/수수료/net rollup. (심화판).", "in": [ - "T_KEY1", - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", + "T_AMT1", "T_COUNT", "T_GROSS", - "T_FEE", - "T_NET" + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_GRPSETTLE", - "desc": "가맹점그룹 통합정산: 그룹 prefix(LIKE) 소속 가맹점별 정산 net 을 커서로", + "desc": "그룹 prefix 통합정산 지급. (심화판).", "in": [ - "T_KEY1", - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", "T_ID1", - "T_COUNT", - "T_GROSS" + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_HOLD", - "desc": "* ST_HOLD.pgc - st 모듈 서비스 ST_HOLD (one service = one file).", + "desc": "가맹점 지급 보류 (SCHEDULED -> HELD). (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], - "out": [] + "out": [ + "T_RC", + "T_ARG6", + "T_GROSS", + "T_ARG5", + "T_ARG4" + ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_HOLDAMT", - "desc": "금액 조건부 보류: 임계액(threshold) 이상의 SCHEDULED 지급을 집합 UPDATE 로", + "desc": "임계액 이상 지급 일괄 보류. (심화판).", "in": [ - "T_AMT1", - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG6", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_INSTANTPAY", - "desc": "즉시 정산 지급: 정산 net 을 당일 지급으로 즉시 확정(status=PAID)해 지급", + "desc": "즉시 지급(PAID) 등록. (심화판).", "in": [ - "T_NET", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_ID1" + "T_ARG6", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_ISSUERSTAT", - "desc": "발급사별 수수료 통계 세분: 당일 정산완료(S) 매입을 발급사(issuer) 단위로", + "desc": "발급사별 수수료 분해 통계. (심화판).", "in": [ - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", + "T_AMT1", + "T_AMT2", + "T_AMT3", "T_COUNT", - "T_FEE" + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_LEDGERLINK", - "desc": "* ST_LEDGERLINK.pgc - st 모듈 서비스 ST_LEDGERLINK (one service = one file).", + "desc": "정산 연계 원장 기록. (심화판).", "in": [ - "T_PURCHASE_ID", + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", "T_NET", - "T_BIZDATE" + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], - "out": [] + "out": [ + "T_RC", + "T_ARG6", + "T_COUNT", + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" + ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_LEDGERSNAP", - "desc": "정산 스냅샷: 당일 정산 건수·net 총액을 집계해 원장에 STSNAP 스냅샷 엔트리로", + "desc": "당일 정산 총액 원장 스냅샷. (심화판).", "in": [ - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_ID1", + "T_RC", + "T_ARG6", "T_COUNT", - "T_GROSS" + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_LIMITCHK", - "desc": "가맹점 한도 점검: 가맹점 일 한도(daily_limit)와 당일 매입 누계(취소 제외)를", + "desc": "일 매입 한도 잔여 점검. (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", "T_AMT1", + "T_AMT2", + "T_COUNT", "T_GROSS", - "T_DELTA", - "T_RC" + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_LOCKBATCH", - "desc": "정산 배치 잠금: 당일 확정된 정산(status=SETTLED)을 일괄 LOCKED 로 전환해 이후", + "desc": "당일 정산 일괄 잠금(SETTLED->LOCKED). (심화판).", "in": [ - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG6", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_LOYALTY", - "desc": "로열티 인센티브: 가맹점의 전체 기간 누적 매출(st_merch_settle 전 일자 합계)에", + "desc": "누적 매출 기반 로열티 보상. (심화판).", "in": [ - "T_ARG1", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", "T_ADJ", - "T_GROSS", - "T_COUNT", - "T_DELTA" + "T_DELTA", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { @@ -17901,1051 +25035,1993 @@ const SVC_SCREENS = [ "mod": "st", "modnm": "정산/수수료", "svc": "ST_MERCHRATE", - "desc": "가맹점 개별 요율 적용: 구간요율표 대신 가맹점 마스터의 계약 요율(mdr_bps)로", + "desc": "가맹점 계약 요율 수수료 산출/기록. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_AMOUNT", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", "T_FEE", - "T_RC" + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_MINFEE", - "desc": "최저 수수료 보정: 산출 수수료가 최저 수수료에 미달하면 부족분을 ADJ 정산", + "desc": "최저 수수료 미달분 보정. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_FEE", - "T_ARG1", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", + "T_AMT3", "T_FEE", - "T_DELTA" + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_MKTGSHARE", - "desc": "마케팅 분담금 정산: 가맹점 일별 집계(st_merch_settle)의 총매출에 분담율", + "desc": "매출 기반 마케팅 분담금 부과. (심화판).", "in": [ - "T_ARG1", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", + "T_FEE", + "T_ID1", "T_GROSS", - "T_FEE" + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_MONTHLY", - "desc": "* ST_MONTHLY.pgc - st 모듈 서비스 ST_MONTHLY (one service = one file).", + "desc": "월별 정산 net 합계 (biz_date LIKE 'YYYY-MM%'). (심화판).", "in": [ "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL", "T_KEY1" ], "out": [ - "T_GROSS" + "T_RC", + "T_ARG6", + "T_COUNT", + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_MONTHSTAT", - "desc": "월별 정산 통계: 대상 월(T_STR1, YYYY-MM)에 속하는 정산의 건수/net 합계/최대 net 을", + "desc": "월(YYYY-MM) 정산 집계. (심화판).", "in": [ - "T_STR1" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL", + "T_KEY1" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_GROSS", - "T_AMT1" + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_NEGSWEEP", - "desc": "음수 정산 정리: 당일 net 이 음수인 SETTLED 정산의 합계를 먼저 파악한 뒤", + "desc": "음수 net 정산 일괄 취소. (심화판).", "in": [ - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_COUNT", - "T_GROSS", - "T_RC" + "T_RC", + "T_ARG6", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_NETTING", - "desc": "* ST_NETTING.pgc - st 모듈 서비스 ST_NETTING (one service = one file).", + "desc": "당일 net 합계 조회. (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_GROSS" + "T_RC", + "T_ARG6", + "T_COUNT", + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_NEXTPAYDT", - "desc": "다음 지급 영업일 계산: 입력 영업일의 다음 영업일(주말 스킵)을 공통 유틸", + "desc": "다음 영업일(지급일) 계산. (심화판).", "in": [ - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_RC" + "T_RC", + "T_ARG6", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_OFFSET", - "desc": "상계: 음수 조정으로 채권 상계 기록 (dbio). */", + "desc": "상계: 음수 조정으로 채권 상계 기록. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", "T_AMOUNT", - "T_BIZDATE" + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_ADJ" + "T_RC", + "T_ARG6", + "T_ADJ", + "T_DELTA", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_PARTIAL", - "desc": "부분정산: 매입 net 에서 기정산 합계를 뺀 잔여 한도 내에서 요청 금액만큼", + "desc": "부분정산 (잔여 한도 검증 후 PARTIAL 정산 생성). (심화판).", "in": [ - "T_PURCHASE_ID", - "T_AMOUNT", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", "T_NET", - "T_SETTLE_ID" + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_PARTNERSET", - "desc": "제휴사 정산 배분: 정산 net 금액에서 제휴 지분율(T_ARG1 bps)만큼을 제휴사", + "desc": "제휴사 지분 지급 예정 등록. (심화판).", "in": [ - "T_NET", - "T_ARG1", + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", "T_STR1", - "T_BIZDATE" + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", "T_ID1", - "T_AMOUNT" + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_PARTPAY", - "desc": "부분 지급 처리: 정산 net 중 일부(T_ARG1)만 당일 지급(PAID)으로 확정하고", + "desc": "net 일부 부분 지급(PAID). (심화판).", "in": [ - "T_NET", - "T_ARG1", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", "T_ID1", - "T_AMOUNT", - "T_NET" + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_PAYDATE", - "desc": "* ST_PAYDATE.pgc - st 모듈 서비스 ST_PAYDATE (one service = one file).", + "desc": "지급일 산정(익영업일) 후 지급일 갱신. (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], - "out": [] + "out": [ + "T_RC", + "T_ARG6", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" + ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_PAYRETRY", - "desc": "지급 재시도: 이체 실패 등으로 CANCELLED 된 지급을 익영업일 지급일로 갱신해", + "desc": "취소 지급 익영업일 재예약. (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_COUNT" + "T_ARG6", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_PAYSTATUS", - "desc": "지급 상태 조회: 가맹점의 가장 최근 지급 레코드에서 상태·금액·지급 예정일을", + "desc": "가맹점 최신 지급 상태 조회. (심화판).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL", + "T_STR2" ], "out": [ "T_RC", - "T_NET" + "T_ARG6", + "T_COUNT", + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_PCTFEE", - "desc": "정률 수수료 계산: 거래 금액에 입력 요율(bps, T_ARG1)을 곱해(acq_fee) 수수료를", + "desc": "정률 수수료 계산 (금액 x bps). (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_AMOUNT", - "T_ARG1", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_FEE", "T_RC", - "T_ID1" + "T_ARG6", + "T_FEE", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_PRECONFIRM", - "desc": "정산 확정 전 검증: 당일 지급 중 보류(HELD) 건이 남아있는지 확인해 확정 가능", + "desc": "보류 잔여 여부로 확정 가능 판정. (심화판).", "in": [ - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_AMT1", - "T_AMT2", - "T_RC" + "T_RC", + "T_ARG6", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_PROMOAPPLY", - "desc": "프로모션 할인 적용: 산정된 수수료(T_FEE)에 프로모션 할인율(T_ARG1 bps)을 적용해", + "desc": "수수료 프로모션 할인(음수 조정). (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", "T_FEE", - "T_ARG1", - "T_BIZDATE" + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", "T_ADJ", "T_DELTA", - "T_NET" + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_PROMOCALC", - "desc": "프로모션/이벤트 정산: 이벤트 코드와 할인율(%)을 받아 수수료 할인액을 계산하고", + "desc": "프로모션/이벤트 수수료 할인 정산. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_FEE", - "T_ARG1", + "T_MERCHANT", "T_BIZDATE", - "T_STR1" + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_DELTA", - "T_ADJ", - "T_FEE" + "T_ARG6", + "T_FEE", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_RATEAPPLY", - "desc": "* ST_RATEAPPLY.pgc - st 모듈 서비스 ST_RATEAPPLY (one service = one file).", + "desc": "구간요율 적용 수수료 계산 + 정산 상세 기록. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_AMOUNT", "T_MERCHANT", "T_BIZDATE", - "T_STR1" + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", "T_FEE", - "T_RC" + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_RATECARD", - "desc": "요율 카드 조회: 특정 수수료 유형의 등록 구간 개수와 최소/최대 요율을 집계해", + "desc": "요율표 요약(구간수/최소/최대 bps). (심화판).", "in": [ - "T_STR1" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_COUNT", + "T_ARG6", + "T_FEE", "T_AMT1", - "T_AMT2" + "T_AMT2", + "T_COUNT", + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_RATECMP", - "desc": "요율 유형 비교: 두 수수료 유형(T_STR1/T_STR2)의 입력 금액 적용 요율을", + "desc": "두 요율 유형 적용 bps 비교. (심화판).", "in": [ - "T_AMOUNT", + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", "T_STR1", - "T_STR2" + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_ARG1", - "T_ARG2", - "T_DELTA", - "T_FEE" + "T_RC", + "T_ARG6", + "T_FEE", + "T_AMT1", + "T_AMT2", + "T_COUNT", + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_RATEFLOOR", - "desc": "요율 하한 적용: 정률 수수료(금액 x bps)가 하한액(T_ARG2)에 미달하면 하한으로", + "desc": "정률 수수료 하한 보정 후 기록. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_AMOUNT", - "T_ARG1", - "T_ARG2", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", "T_FEE", - "T_RC" + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_RATEMAX", - "desc": "요율 상한 적용: 정률 수수료(금액 x bps)를 계산하되 상한액(T_ARG2)을 초과하면", + "desc": "정률 수수료 상한 절삭 후 기록. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_AMOUNT", - "T_ARG1", - "T_ARG2", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", "T_FEE", - "T_RC" + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_RATESIM", - "desc": "요율 시뮬레이션: 요율표(st_fee_rate)의 전 구간을 커서로 순회하며 입력 금액에", + "desc": "요율 시뮬레이션 (구간 판정 + 예상 수수료). (심화판).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", "T_AMOUNT", - "T_STR1" + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", + "T_FEE", + "T_AMT1", + "T_AMT2", "T_COUNT", - "T_FEE" + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_REBATE", - "desc": "리베이트 정산: 부과 수수료(T_FEE)의 일정 비율(T_ARG1 bps)을 가맹점에 돌려주는", + "desc": "수수료 기반 리베이트 환급 조정. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", "T_FEE", - "T_ARG1", - "T_BIZDATE" + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", "T_ADJ", - "T_DELTA" + "T_DELTA", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_RECALC", - "desc": "* ST_RECALC.pgc - st 모듈 서비스 ST_RECALC (one service = one file).", + "desc": "매입 수수료 기준 정산 net 재계산 + 갱신. (심화판).", "in": [ - "T_PURCHASE_ID" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_NET" + "T_RC", + "T_ARG6", + "T_NET", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_RECVAGE", - "desc": "미수금 연령 분석: 기준일(T_BIZDATE) 이전 지급일의 보류(HELD) 지급 건수와 금액을", + "desc": "기준일 이전 보류 미수금 집계. (심화판).", "in": [ - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL", + "T_STR2" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_GROSS", - "T_RC" + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_RECVOFFSET", - "desc": "미수금 상계: 매입 조인으로 가맹점의 누적 미수금(음수 조정 절대값)을 산출하고", + "desc": "미수금-지급 상계 처리. (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_AMOUNT", - "T_AMT1" + "T_ARG6", + "T_ADJ", + "T_DELTA", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_RECVSTAT", - "desc": "미수금 통계: 지급(st_payment) 상태별 건수와 금액을 커서로 순회 집계해 보류/예정/", + "desc": "지급 상태별 분포 통계. (심화판).", "in": [ - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL", + "T_STR2" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_GROSS", - "T_AMT1" + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_REFERRAL", - "desc": "추천 인센티브: 추천으로 유치된 가맹점 수(prefix T_KEY1 매칭 건수)에 건당 보상액", + "desc": "추천 유치 건당 보상 지급. (심화판).", "in": [ - "T_ARG1", "T_MERCHANT", - "T_KEY1", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_ID1", - "T_COUNT", - "T_AMOUNT" + "T_ARG6", + "T_ADJ", + "T_DELTA", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_REFUNDCALC", - "desc": "환불 재계산: 환불액(T_ARG1)에 비례한 수수료 환급분을 계산해 정산 상세에", + "desc": "환불 비례 수수료 환급 기록. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_ARG1", - "T_ARG2", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_FEE", - "T_ID1" + "T_ARG6", + "T_ADJ", + "T_DELTA", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_RELAYFEE", - "desc": "중계 수수료: 구간요율 조회 후 정산 상세 기록 (dbio). */", + "desc": "중계 수수료: 구간요율 조회 후 정산 상세 기록. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_AMOUNT", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_FEE" + "T_RC", + "T_ARG6", + "T_FEE", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_RELAYSPLIT", - "desc": "중계수수료 분배: 중계 수수료(T_FEE)를 지분율(T_ARG1 bps)에 따라 중계사 몫과", + "desc": "중계수수료 2자 분배 기록. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_FEE", - "T_ARG1", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_AMT1", - "T_AMT2" + "T_RC", + "T_ARG6", + "T_FEE", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_RELEASE", - "desc": "* ST_RELEASE.pgc - st 모듈 서비스 ST_RELEASE (one service = one file).", + "desc": "지급 보류 해제 (HELD -> SCHEDULED). (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], - "out": [] + "out": [ + "T_RC", + "T_ARG6", + "T_GROSS", + "T_ARG5", + "T_ARG4" + ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_REOPEN", - "desc": "정산 마감 해제: 당일 마감 원장(STCLOSE)을 삭제해 마감을 철회한다.", + "desc": "정산 마감 철회 (STCLOSE 삭제). (심화판).", "in": [ - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG6", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_REPARTIAL", - "desc": "부분 재정산: 부분취소액(T_DELTA)만큼 줄어든 잔여 거래금액으로 수수료를 재계산해", + "desc": "부분취소 후 수수료 재계산 조정. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", "T_AMOUNT", + "T_NET", + "T_FEE", "T_DELTA", - "T_ARG1", - "T_BIZDATE" + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", "T_ADJ", - "T_FEE", - "T_DELTA" + "T_DELTA", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_REPORT", - "desc": "* ST_REPORT.pgc - st 모듈 서비스 ST_REPORT (one service = one file).", + "desc": "당일 정산 건수/합계 리포트. (심화판).", "in": [ - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", + "T_AMT1", + "T_AMT2", "T_COUNT", - "T_GROSS" + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_RERUN", - "desc": "정산 재실행: 해당 매입의 정산 상세를 전량 삭제하고 매입 원본(fee)을 기준으로", + "desc": "정산 재실행 (상세 삭제 + 원본 기준 재생성). (심화판).", "in": [ - "T_PURCHASE_ID" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", + "T_AMT1", + "T_AMT2", "T_COUNT", - "T_FEE" + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_RETRORATE", - "desc": "요율 소급적용: 신규 요율(bps)로 매입 금액의 수수료를 재계산하고, 기계상된", + "desc": "요율 소급적용 (차액 조정 기록). (심화판).", "in": [ - "T_SETTLE_ID", - "T_ARG1", - "T_PURCHASE_ID", + "T_MERCHANT", "T_BIZDATE", - "T_STR1" + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_DELTA", + "T_ARG6", "T_ADJ", - "T_FEE" + "T_DELTA", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_ROUNDING", - "desc": "원단위 절사: 최신 정산 net 의 10원 미만 끝자리를 절사해 갱신하고 절사분을", + "desc": "정산 net 원단위 절사. (심화판).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", "T_PURCHASE_ID", - "T_BIZDATE" + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_DELTA", - "T_ADJ", - "T_NET" + "T_ARG6", + "T_NET", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_SLABFEE", - "desc": "슬라브(누진 구간) 요율 계산: 요율표의 각 구간에 대해 금액이 걸치는 부분에만", + "desc": "슬라브 누진 요율 수수료 합산. (심화판).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", "T_AMOUNT", - "T_STR1" + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", "T_FEE", - "T_COUNT" + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_SPLIT", - "desc": "분할 지급: net 을 n 회로 나눠 지급 레코드 생성 (dbio 루프). */", + "desc": "분할 지급: net 을 n 회로 나눠 지급 레코드 생성 (dbio 루프). (심화판).", "in": [ - "T_NET", - "T_INSTALL_N", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_COUNT" + "T_RC", + "T_ARG6", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_SPLITEVEN", - "desc": "분할 지급 잔여 보정: net 을 n 회로 균등 분할하되 나눗셈 끝수를 마지막", + "desc": "끝수 보정 균등 분할 지급. (심화판).", "in": [ - "T_NET", - "T_INSTALL_N", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_COUNT", - "T_AMT1", - "T_AMT2" + "T_RC", + "T_ARG6", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_STATDETAIL", - "desc": "정산 통계 세분: 당일 정산 상세를 수수료 종류(fee_type)별로 분해 집계하는", + "desc": "수수료 종류별 세분 통계. (심화판).", "in": [ - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", + "T_AMT1", + "T_AMT2", + "T_AMT3", "T_COUNT", "T_GROSS", - "T_AMT1" + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_STATS", - "desc": "* ST_STATS.pgc - st 모듈 서비스 ST_STATS (one service = one file).", + "desc": "정산 통계(건수/net 합계) 조회. (심화판).", "in": [ - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", + "T_AMT1", + "T_AMT2", "T_COUNT", "T_GROSS", - "T_AMT1" + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_STATUS", - "desc": "* ST_STATUS.pgc - st 모듈 서비스 ST_STATUS (one service = one file).", + "desc": "정산 상태 조회. (심화판).", "in": [ - "T_PURCHASE_ID" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], - "out": [] + "out": [ + "T_RC", + "T_ARG6", + "T_ARG3", + "T_ARG5", + "T_ARG4" + ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_SUPPLYCALC", - "desc": "공급가액 역산: 부가세 포함 총액(T_AMOUNT)에서 공급가액과 부가세를 분리", + "desc": "VAT 포함 총액에서 공급가액 역산. (심화판).", "in": [ - "T_AMOUNT" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", + "T_AMT2", + "T_TAX", + "T_ID1", + "T_FEE", "T_GROSS", - "T_NET", - "T_TAX" + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_TAXINVCHK", - "desc": "세금계산서 대사: 가맹점 당일 세금계산서(ac_tax_invoice) 공급가·세액 합계와", + "desc": "세금계산서-정산 VAT 대사. (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_GROSS", + "T_RC", + "T_ARG6", "T_AMT1", - "T_TAX", - "T_DELTA", - "T_RC" + "T_AMT2", + "T_COUNT", + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_TAXSUMMARY", - "desc": "세금 요약: 입력 수수료(T_FEE)를 공급가액으로 부가세(acq_vat)와 원천세", + "desc": "부가세+원천세 통합 요약. (심화판).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", "T_FEE", - "T_ARG1" + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", "T_AMT1", "T_AMT2", - "T_TAX", - "T_NET" + "T_AMT3", + "T_COUNT", + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_TIERFEE", - "desc": "구간별 정률 적용: 입력 금액에 해당하는 최상위 구간(band_min <= amount)의 요율을", + "desc": "해당 구간 요율로 정률 수수료 산출. (심화판).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", "T_AMOUNT", - "T_STR1" + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_FEE" + "T_ARG6", + "T_FEE", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_TIERINCENT", - "desc": "구간별 인센티브: 가맹점 일 거래대금(gross) 구간에 따라 인센티브 요율을", + "desc": "거래대금 구간별 인센티브 정산. (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_AMOUNT", - "T_ADJ" + "T_ARG6", + "T_FEE", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_TOPMERCH", - "desc": "상위 가맹점 조회: 당일 정산 net 합계 1위 가맹점을 GROUP BY + ORDER BY", + "desc": "당일 정산 1위 가맹점 조회. (심화판).", "in": [ - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_GROSS" + "T_ARG6", + "T_AMT1", + "T_COUNT", + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_UNLOCK", - "desc": "정산 잠금 해제: 잠금된 정산(status=LOCKED)을 다시 SETTLED 로 되돌려 조정/재계산을", + "desc": "정산 잠금 해제(LOCKED->SETTLED). (심화판).", "in": [ - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG6", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_UNPAIDSUM", - "desc": "미지급 집계: 기준일 이전이 지급 예정일인데 아직 완료(PAID)되지 않은", + "desc": "예정일 경과 미지급 집계. (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL", + "T_STR2" ], "out": [ "T_RC", + "T_ARG6", "T_COUNT", - "T_GROSS" + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_VANDETAIL", - "desc": "VAN 수수료 세분 명세: 당일 정산 상세 중 VAN 라인의 건수/합계/평균을 집계해", + "desc": "VAN 수수료 건수/합계/평균 명세. (심화판).", "in": [ - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", + "T_AMT1", + "T_AMT2", + "T_AMT3", "T_COUNT", - "T_FEE", - "T_AMT1" + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_VANFEE", - "desc": "VAN 수수료: 구간요율 조회 후 정산 상세 기록 (dbio). */", + "desc": "VAN 수수료: 구간요율 조회 후 정산 상세 기록. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_AMOUNT", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_FEE" + "T_RC", + "T_ARG6", + "T_FEE", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_VAT", - "desc": "* ST_VAT.pgc - st 모듈 서비스 ST_VAT (one service = one file).", + "desc": "정산 부가세 원장 기록. (심화판).", "in": [ - "T_PURCHASE_ID", + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", "T_FEE", - "T_BIZDATE" + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_TAX" + "T_RC", + "T_ARG6", + "T_TAX", + "T_ID1", + "T_FEE", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_VATADJUST", - "desc": "부가세 조정: 조정된 공급가액(T_ARG1)에 대한 재산정 부가세(acq_vat)를 정산 상세에", + "desc": "공급가액 조정 재부가세 기록. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_ARG1", - "T_TAX", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", "T_TAX", - "T_DELTA", - "T_ID1" + "T_ID1", + "T_FEE", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_VATDETAIL", - "desc": "부가세 명세: 가맹점 당일 수수료 상세(MDR/VAN/RELAY 합계)를 공급가액으로 보고", + "desc": "수수료 합계 기준 부가세 명세. (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", + "T_AMT1", + "T_AMT2", + "T_AMT3", + "T_COUNT", "T_GROSS", - "T_TAX", - "T_COUNT" + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_VATREPORT", - "desc": "부가세 신고자료: 대상 월의 정산 상세 VAT 합계와 세금계산서(ac_tax_invoice)", + "desc": "월별 부가세 신고자료 집계. (심화판).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL", "T_KEY1" ], "out": [ - "T_TAX", + "T_RC", + "T_ARG6", "T_COUNT", "T_GROSS", - "T_AMT1", - "T_DELTA" + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_VOIDRECALC", - "desc": "void(전체 취소) 재계산: 매입 정산 net 전액을 역분개(음수 조정)하고 해당", + "desc": "전체 취소 역분개 + 지급 취소. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_NET", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_ADJ", - "T_DELTA" + "T_RC", + "T_ARG6", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_VOLREBATE", - "desc": "거래량 리베이트: 가맹점 일별 집계의 거래 건수 구간에 따라 리베이트율을 차등", + "desc": "거래건수 구간별 리베이트. (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", "T_ADJ", - "T_COUNT", - "T_DELTA" + "T_DELTA", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_WHT", - "desc": "* ST_WHT.pgc - st 모듈 서비스 ST_WHT (one service = one file).", + "desc": "원천징수(수수료의 22%) 정산 상세 기록. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_FEE", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_TAX" + "T_RC", + "T_ARG6", + "T_TAX", + "T_ID1", + "T_FEE", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_WHTDETAIL", - "desc": "원천세 명세: 가맹점 당일 수수료 합계에 원천징수율(T_ARG1 bps)을 적용해", + "desc": "수수료 합계 기준 원천세 명세. (심화판).", "in": [ - "T_ARG1", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", + "T_AMT1", + "T_AMT2", + "T_AMT3", + "T_COUNT", "T_GROSS", - "T_TAX", - "T_RC" + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_WHTREPORT", - "desc": "원천세 명세: 가맹점의 대상 월 정산 상세(WHT) 합계와 원천 대장(ac_wht) 합계를", + "desc": "가맹점 월별 원천세 명세 대사. (심화판).", "in": [ "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL", "T_KEY1" ], "out": [ - "T_TAX", - "T_AMT1", - "T_DELTA", - "T_RC" + "T_RC", + "T_ARG6", + "T_COUNT", + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_WORKFLOW", - "desc": "정산 워크플로 상태 전이: 요청 전이(from=T_STR1 -> to=T_STR2)가 허용 전이표", + "desc": "허용 전이 검증 후 정산 상태 갱신. (심화판).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", "T_STR1", - "T_STR2", - "T_BIZDATE" + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_COUNT" + "T_ARG6", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_XFERLINK", - "desc": "* ST_XFERLINK.pgc - st 모듈 서비스 ST_XFERLINK (one service = one file).", + "desc": "이체 연계 원장 기록. (심화판).", "in": [ - "T_PURCHASE_ID", + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", "T_NET", - "T_BIZDATE" + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], - "out": [] + "out": [ + "T_RC", + "T_ARG6", + "T_COUNT", + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" + ] }, { "mod": "vl", @@ -18957,9 +27033,12 @@ const SVC_SCREENS = [ "T_ARG2" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_COUNT", - "T_ARG3", - "T_RC" + "T_ARG3" ] }, { @@ -18968,12 +27047,17 @@ const SVC_SCREENS = [ "svc": "VL_AGEDOPEN", "desc": "기능: 장기미결 의심거래 검출 (OPEN 상태로 N일 초과 경과, RC 1=존재).", "in": [ + "T_ID1", "T_ARG1" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", "T_COUNT", - "T_ID1", - "T_RC" + "T_ID1" ] }, { @@ -18986,6 +27070,10 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", "T_ARG1" ] }, @@ -18995,11 +27083,19 @@ const SVC_SCREENS = [ "svc": "VL_ANOMALY", "desc": "기능: 이상거래 탐지 후 의심거래 적재 (dbio).", "in": [ + "T_BIZDATE", "T_PURCHASE_ID", - "T_BIZDATE" + "T_ID1" ], "out": [ "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ID2", + "T_ARG2", + "T_ARG1", "T_ID1" ] }, @@ -19012,10 +27108,15 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_GROSS", "T_AMT1", - "T_DELTA", - "T_RC" + "T_DELTA" ] }, { @@ -19024,13 +27125,19 @@ const SVC_SCREENS = [ "svc": "VL_APPRAMT", "desc": "기능: 건별 매입-승인 금액/존재 정합 검증 (RC 1=승인없음, 2=거절승인만 존재, 3=매입없음).", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_AMOUNT" ], "out": [ "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_AMOUNT", - "T_COUNT", - "T_ARG1" + "T_COUNT" ] }, { @@ -19043,8 +27150,12 @@ const SVC_SCREENS = [ "T_ARG1" ], "out": [ - "T_ARG2", - "T_RC" + "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2" ] }, { @@ -19052,11 +27163,18 @@ const SVC_SCREENS = [ "modnm": "정합성검증", "svc": "VL_APPRNOPUR", "desc": "기능: 미매입 승인 검출 (매입 원거래 없는 고아 승인 전수, RC 1=존재; 최소 승인ID 반환).", - "in": [], + "in": [ + "T_ID1" + ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT", - "T_ID1", - "T_RC" + "T_ID1" ] }, { @@ -19068,8 +27186,14 @@ const SVC_SCREENS = [ "T_PURCHASE_ID" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1", + "T_COUNT" ] }, { @@ -19081,10 +27205,15 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_AMT1", "T_AMT2", - "T_AMT3", - "T_RC" + "T_AMT3" ] }, { @@ -19096,10 +27225,15 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_AMT1", "T_AMT2", - "T_DELTA", - "T_RC" + "T_DELTA" ] }, { @@ -19111,11 +27245,16 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_AMT1", "T_AMT2", "T_COUNT", - "T_DELTA", - "T_RC" + "T_DELTA" ] }, { @@ -19127,10 +27266,15 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_AMT1", "T_AMT2", - "T_DELTA", - "T_RC" + "T_DELTA" ] }, { @@ -19139,14 +27283,21 @@ const SVC_SCREENS = [ "svc": "VL_BALANCE", "desc": "기능: 가맹점 순액 대차 검증 (sum(purchase.net) vs sum(settlement.net)).", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_AMT3", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_GROSS", "T_NET", - "T_DELTA", - "T_RC" + "T_DELTA" ] }, { @@ -19158,9 +27309,14 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT", - "T_GROSS", - "T_RC" + "T_GROSS" ] }, { @@ -19172,7 +27328,12 @@ const SVC_SCREENS = [ "T_KEY1" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -19181,10 +27342,17 @@ const SVC_SCREENS = [ "svc": "VL_CANCELMIS", "desc": "기능: 취소 정합 검증 (RC 비트합 1=취소합>매입금액, 2=취소 존재하나 매입상태 미반영, 3=매입없음).", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_AMOUNT" ], "out": [ "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_AMOUNT", "T_AMT1", "T_COUNT" @@ -19200,10 +27368,13 @@ const SVC_SCREENS = [ "T_ARG1" ], "out": [ - "T_COUNT", + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2", - "T_ARG3", - "T_RC" + "T_COUNT", + "T_ARG3" ] }, { @@ -19215,7 +27386,12 @@ const SVC_SCREENS = [ "T_STR1" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -19228,10 +27404,13 @@ const SVC_SCREENS = [ "T_ARG1" ], "out": [ - "T_COUNT", + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2", - "T_ARG3", - "T_RC" + "T_COUNT", + "T_ARG3" ] }, { @@ -19244,10 +27423,13 @@ const SVC_SCREENS = [ "T_ARG1" ], "out": [ - "T_COUNT", + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2", - "T_ARG3", - "T_RC" + "T_COUNT", + "T_ARG3" ] }, { @@ -19260,7 +27442,12 @@ const SVC_SCREENS = [ "T_KEY1" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -19272,7 +27459,12 @@ const SVC_SCREENS = [ "T_ID1" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -19284,7 +27476,13 @@ const SVC_SCREENS = [ "T_PURCHASE_ID" ], "out": [ - "T_RC" + "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -19296,7 +27494,12 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -19304,11 +27507,18 @@ const SVC_SCREENS = [ "modnm": "정합성검증", "svc": "VL_DATEFUT", "desc": "기능: 미래일자 매입 검출 (영업일이 오늘 이후인 매입 수, RC 1=존재).", - "in": [], + "in": [ + "T_ID1" + ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT", - "T_ID1", - "T_RC" + "T_ID1" ] }, { @@ -19318,9 +27528,14 @@ const SVC_SCREENS = [ "desc": "기능: 매입-정산 일자 역전 검출 (정산일<매입일 건수, RC 1=존재; 최소 정산ID 반환).", "in": [], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT", - "T_SETTLE_ID", - "T_RC" + "T_SETTLE_ID" ] }, { @@ -19333,6 +27548,12 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT" ] }, @@ -19342,11 +27563,18 @@ const SVC_SCREENS = [ "svc": "VL_DAYLIMIT", "desc": "기능: 가맹점 일한도 위반 검증 (일자 매입합 > daily_limit → RC 1, 3=가맹점 없음).", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_MERCHANT" ], "out": [ "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_AMT3", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_AMT1", "T_GROSS", "T_DELTA" @@ -19358,13 +27586,20 @@ const SVC_SCREENS = [ "svc": "VL_DUP", "desc": "기능: 중복 매입 검증 (동일 가맹점/금액/영업일 2건 이상 → RC 1).", "in": [ - "T_MERCHANT", "T_BIZDATE", + "T_MERCHANT", "T_AMOUNT" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_AMT3", + "T_ARG6", + "T_ARG2", + "T_ARG1", + "T_COUNT" ] }, { @@ -19373,12 +27608,18 @@ const SVC_SCREENS = [ "svc": "VL_DUPAPPR", "desc": "기능: 중복 승인 검출 건별 (동일 매입 승인가결 2건 이상, RC 1=중복; 검증로그 기록).", "in": [ - "T_PURCHASE_ID", - "T_BIZDATE" + "T_BIZDATE", + "T_PURCHASE_ID" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1", + "T_COUNT" ] }, { @@ -19390,8 +27631,14 @@ const SVC_SCREENS = [ "T_PURCHASE_ID" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1", + "T_COUNT" ] }, { @@ -19403,8 +27650,14 @@ const SVC_SCREENS = [ "T_PURCHASE_ID" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1", + "T_COUNT" ] }, { @@ -19413,13 +27666,19 @@ const SVC_SCREENS = [ "svc": "VL_DUPPAY", "desc": "기능: 건별 중복지급 검출 (ledger XFER 엔트리 2건 이상 → RC 1, 검증로그 기록).", "in": [ - "T_PURCHASE_ID", - "T_BIZDATE" + "T_BIZDATE", + "T_PURCHASE_ID" ], "out": [ + "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT", - "T_GROSS", - "T_RC" + "T_GROSS" ] }, { @@ -19431,9 +27690,13 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_COUNT", + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2", - "T_RC" + "T_ARG1", + "T_COUNT" ] }, { @@ -19445,8 +27708,14 @@ const SVC_SCREENS = [ "T_PURCHASE_ID" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1", + "T_COUNT" ] }, { @@ -19455,14 +27724,21 @@ const SVC_SCREENS = [ "svc": "VL_DUPSETTLE", "desc": "기능: 건별 중복 정산 검출 (동일 매입에 정산 2건 이상 → RC 1, 최초/최종 정산 ID 반환).", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_ID1" ], "out": [ + "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT", "T_NET", "T_ID1", - "T_ID2", - "T_RC" + "T_ID2" ] }, { @@ -19471,12 +27747,19 @@ const SVC_SCREENS = [ "svc": "VL_DUPSETTMR", "desc": "기능: 가맹점 중복 정산 검출 (동일 가맹점+영업일 settlement 2건 이상, RC 1=중복).", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_MERCHANT" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_AMT3", + "T_ARG6", + "T_ARG2", + "T_ARG1", + "T_COUNT" ] }, { @@ -19485,11 +27768,16 @@ const SVC_SCREENS = [ "svc": "VL_EXPIRY", "desc": "기능: 카드 유효기간(YYYYMM) vs 영업일 검증 (RC 1=만료).", "in": [ - "T_STR2", - "T_BIZDATE" + "T_BIZDATE", + "T_STR2" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -19502,6 +27790,12 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_FEE", "T_DELTA" ] @@ -19516,6 +27810,12 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_FEE", "T_DELTA", "T_COUNT" @@ -19532,8 +27832,12 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", - "T_FEE", - "T_ARG2" + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_FEE" ] }, { @@ -19547,6 +27851,11 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", "T_FEE", "T_AMT1", "T_DELTA" @@ -19561,8 +27870,15 @@ const SVC_SCREENS = [ "T_MERCHANT" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_AMT3", + "T_ARG6", + "T_ARG2", + "T_ARG1", + "T_COUNT" ] }, { @@ -19576,6 +27892,11 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", "T_FX_AMT", "T_AMT1", "T_DELTA" @@ -19592,6 +27913,11 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", "T_AMT1", "T_INSTALL_N", "T_COUNT", @@ -19609,6 +27935,9 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2", "T_ARG3" ] @@ -19622,11 +27951,16 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_AMT1", "T_AMT2", "T_COUNT", - "T_DELTA", - "T_RC" + "T_DELTA" ] }, { @@ -19635,12 +27969,19 @@ const SVC_SCREENS = [ "svc": "VL_LEDGDATE", "desc": "기능: 건별 원장 일자 역전 검출 (ledger.biz_date < purchase.biz_date 엔트리 수, RC 1=역전 존재).", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_ID1" ], "out": [ + "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT", - "T_ID1", - "T_RC" + "T_ID1" ] }, { @@ -19649,10 +27990,17 @@ const SVC_SCREENS = [ "svc": "VL_LEDGENTRY", "desc": "기능: 건별 원장 대사 엔트리 검증 (purchase.amount vs ledger RECON 합, RC 1=금액불일치 2=원장없음 3=매입없음", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_AMOUNT" ], "out": [ "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_AMOUNT", "T_AMT1", "T_COUNT" @@ -19664,11 +28012,18 @@ const SVC_SCREENS = [ "svc": "VL_LEDGER", "desc": "기능: 원장 합계 정합 검증 (sum(ledger) vs purchase.net, RC 1=불일치).", "in": [ - "T_PURCHASE_ID", - "T_BIZDATE" + "T_BIZDATE", + "T_PURCHASE_ID" ], "out": [ "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ID2", + "T_ARG2", + "T_ARG1", "T_GROSS" ] }, @@ -19682,7 +28037,14 @@ const SVC_SCREENS = [ "T_AMOUNT" ], "out": [ - "T_RC" + "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_AMT3", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -19691,15 +28053,20 @@ const SVC_SCREENS = [ "svc": "VL_LIMITNEAR", "desc": "기능: 가맹점 일한도 임박 검출 (사용률>=T_ARG1% → RC 1, 초과 → RC 2, 3=가맹점 없음).", "in": [ - "T_ARG1", + "T_BIZDATE", "T_MERCHANT", - "T_BIZDATE" + "T_ARG1" ], "out": [ "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_AMT3", + "T_ARG6", + "T_ARG2", "T_AMT1", - "T_GROSS", - "T_ARG2" + "T_GROSS" ] }, { @@ -19708,12 +28075,17 @@ const SVC_SCREENS = [ "svc": "VL_LIMITPCT", "desc": "기능: 한도 사용률 검증 (가맹점 일 매입합/한도 %가 T_ARG1 기본90 이상, RC 1=근접 8=가맹점없음).", "in": [ - "T_MERCHANT", "T_BIZDATE", + "T_MERCHANT", "T_ARG1" ], "out": [ "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_AMT3", + "T_ARG6", "T_ARG2", "T_AMT1" ] @@ -19724,13 +28096,17 @@ const SVC_SCREENS = [ "svc": "VL_LONGOPEN", "desc": "기능: 장기 미결 의심거래 검출 (OPEN 상태로 N일 초과 경과, 기준 T_ARG1 기본7, RC 1=존재).", "in": [ + "T_ID1", "T_ARG1" ], "out": [ - "T_COUNT", - "T_ID1", + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2", - "T_RC" + "T_COUNT", + "T_ID1" ] }, { @@ -19740,9 +28116,13 @@ const SVC_SCREENS = [ "desc": "기능: 가맹점 요율 위반 검출 (mdr_bps<0 또는 규칙 MDR_MAX 초과, RC 1=위반 존재).", "in": [], "out": [ - "T_COUNT", + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", "T_ARG1", - "T_RC" + "T_COUNT" ] }, { @@ -19754,7 +28134,14 @@ const SVC_SCREENS = [ "T_MERCHANT" ], "out": [ - "T_RC" + "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_AMT3", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -19766,9 +28153,14 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT", - "T_AMT1", - "T_RC" + "T_AMT1" ] }, { @@ -19777,12 +28169,18 @@ const SVC_SCREENS = [ "svc": "VL_NEGFEE", "desc": "기능: 음수 수수료 매입 검출 (일자별 fee<0 매입 수, RC 1=존재; 최소 매입ID 반환).", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_ID1" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT", - "T_ID1", - "T_RC" + "T_ID1" ] }, { @@ -19794,10 +28192,13 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_COUNT", - "T_ARG1", + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2", - "T_RC" + "T_ARG1", + "T_COUNT" ] }, { @@ -19810,10 +28211,13 @@ const SVC_SCREENS = [ "T_ARG1" ], "out": [ - "T_COUNT", + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2", - "T_ARG3", - "T_RC" + "T_COUNT", + "T_ARG3" ] }, { @@ -19826,8 +28230,12 @@ const SVC_SCREENS = [ "T_ARG1" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_COUNT" ] }, { @@ -19839,8 +28247,13 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", + "T_COUNT" ] }, { @@ -19849,12 +28262,18 @@ const SVC_SCREENS = [ "svc": "VL_PURNOAPPR", "desc": "기능: 무승인 매입 검출 (일자별 승인 전무 매입 수, 취소분 제외, RC 1=존재).", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_ID1" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT", - "T_ID1", - "T_RC" + "T_ID1" ] }, { @@ -19868,8 +28287,11 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", - "T_ARG2", "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", "T_DELTA" ] }, @@ -19882,7 +28304,13 @@ const SVC_SCREENS = [ "T_PURCHASE_ID" ], "out": [ - "T_RC" + "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -19891,10 +28319,16 @@ const SVC_SCREENS = [ "svc": "VL_REPORT", "desc": "기능: 심각도별 의심거래 건수 리포트.", "in": [ - "T_STR4", - "T_BIZDATE" + "T_BIZDATE", + "T_STR4" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT" ] }, @@ -19908,7 +28342,14 @@ const SVC_SCREENS = [ "T_AMOUNT" ], "out": [ - "T_RC" + "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_AMT3", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -19918,8 +28359,13 @@ const SVC_SCREENS = [ "desc": "기능: 참조무결성 승인 (취소/무효 매입에 승인가결 존재 건수, RC 1=위반).", "in": [], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", + "T_COUNT" ] }, { @@ -19931,9 +28377,14 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT", - "T_CANCEL_ID", - "T_RC" + "T_CANCEL_ID" ] }, { @@ -19943,9 +28394,14 @@ const SVC_SCREENS = [ "desc": "기능: 참조무결성 해외 (매입 원거래 없는 해외 내역 수+환산합, RC 1=위반).", "in": [], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT", - "T_AMT1", - "T_RC" + "T_AMT1" ] }, { @@ -19955,8 +28411,13 @@ const SVC_SCREENS = [ "desc": "기능: 참조무결성 할부 (매입 원거래 없는 할부 내역 수, NOT IN, RC 1=위반).", "in": [], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", + "T_COUNT" ] }, { @@ -19968,11 +28429,14 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_ARG1", + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2", + "T_ARG1", "T_ARG3", - "T_COUNT", - "T_RC" + "T_COUNT" ] }, { @@ -19981,12 +28445,18 @@ const SVC_SCREENS = [ "svc": "VL_RIMERCH", "desc": "기능: 참조무결성 세분 검증 - 매입→가맹점 (일자별 고아 매입 수, RC 1=고아 존재).", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_ID1" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT", - "T_ID1", - "T_RC" + "T_ID1" ] }, { @@ -19994,11 +28464,18 @@ const SVC_SCREENS = [ "modnm": "정합성검증", "svc": "VL_RIPAY", "desc": "기능: 참조무결성 지급 (지급 XFER 원장의 매입 원거래 부재 건수, RC 1=위반).", - "in": [], + "in": [ + "T_ID1" + ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT", - "T_ID1", - "T_RC" + "T_ID1" ] }, { @@ -20007,13 +28484,19 @@ const SVC_SCREENS = [ "svc": "VL_RIPURCH", "desc": "기능: 참조무결성 세분 검증 - 정산→매입 (일자별 고아 정산 수/금액, RC 1=고아 존재).", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_ID1" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT", "T_ID1", - "T_NET", - "T_RC" + "T_NET" ] }, { @@ -20022,12 +28505,19 @@ const SVC_SCREENS = [ "svc": "VL_RISETTLE", "desc": "기능: 참조무결성 세분 검증 - 정산 가맹점 불일치 (settlement.merchant_id <> purchase.merchant_id, RC", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_PURCHASE_ID" ], "out": [ + "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT", - "T_PURCHASE_ID", - "T_RC" + "T_PURCHASE_ID" ] }, { @@ -20037,8 +28527,13 @@ const SVC_SCREENS = [ "desc": "기능: 참조무결성 세금계산서 (매입 원거래 없는 세금계산서 수, LEFT JOIN, RC 1=위반).", "in": [], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", + "T_COUNT" ] }, { @@ -20051,10 +28546,13 @@ const SVC_SCREENS = [ "T_ARG1" ], "out": [ - "T_COUNT", + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2", - "T_ARG3", - "T_RC" + "T_COUNT", + "T_ARG3" ] }, { @@ -20068,20 +28566,31 @@ const SVC_SCREENS = [ "T_STR4", "T_ARG1" ], - "out": [] + "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2" + ] }, { "mod": "vl", "modnm": "정합성검증", "svc": "VL_RULECOUNT", "desc": "기능: 검증규칙 심각도 분포 (INFO/WARN/ERROR/CRIT 건수, RC 1=CRIT 규칙 존재).", - "in": [], + "in": [ + "T_STR3" + ], "out": [ - "T_ARG1", + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2", + "T_ARG1", "T_ARG3", - "T_ARG4", - "T_RC" + "T_ARG4" ] }, { @@ -20094,6 +28603,10 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", "T_ARG1" ] }, @@ -20103,12 +28616,18 @@ const SVC_SCREENS = [ "svc": "VL_RULEHIST", "desc": "기능: 검증 규칙 변경이력 조회 (vl_check_log의 VL_RULEMOD 이력 건수/최종 로그 ID, RC 1=이력 없음).", "in": [ - "T_STR3" + "T_STR3", + "T_ID1" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT", - "T_ID1", - "T_RC" + "T_ID1" ] }, { @@ -20117,13 +28636,16 @@ const SVC_SCREENS = [ "svc": "VL_RULEMOD", "desc": "기능: 검증 규칙 변경 + 변경이력 기록 (임계값/심각도 UPDATE 후 vl_check_log에 이력 적재, RC 3=규칙 없음).", "in": [ + "T_BIZDATE", "T_STR3", "T_ARG1", - "T_STR4", - "T_BIZDATE" + "T_STR4" ], "out": [ "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2" ] }, @@ -20137,7 +28659,12 @@ const SVC_SCREENS = [ "T_STR5" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -20149,7 +28676,12 @@ const SVC_SCREENS = [ "T_STR3" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -20164,7 +28696,11 @@ const SVC_SCREENS = [ "T_ARG1" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2" ] }, { @@ -20177,7 +28713,12 @@ const SVC_SCREENS = [ "T_STR1" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -20190,7 +28731,12 @@ const SVC_SCREENS = [ "T_STR4" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -20198,12 +28744,17 @@ const SVC_SCREENS = [ "modnm": "정합성검증", "svc": "VL_RULESTAT", "desc": "기능: 검증 규칙 현황 집계 (전체/ACTIVE/CRIT 건수, RC 1=활성 규칙 전무).", - "in": [], + "in": [ + "T_STR3" + ], "out": [ - "T_COUNT", - "T_ARG1", + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2", - "T_RC" + "T_ARG1", + "T_COUNT" ] }, { @@ -20217,6 +28768,9 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2" ] }, @@ -20226,11 +28780,17 @@ const SVC_SCREENS = [ "svc": "VL_SALES", "desc": "기능: 매입 원거래 존재 검증 (RC 1=미존재).", "in": [ - "T_PURCHASE_ID", - "T_BIZDATE" + "T_BIZDATE", + "T_PURCHASE_ID" ], "out": [ - "T_RC" + "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -20243,6 +28803,12 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_NET" ] }, @@ -20255,9 +28821,14 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT", - "T_AMT1", - "T_RC" + "T_AMT1" ] }, { @@ -20270,10 +28841,14 @@ const SVC_SCREENS = [ "T_ARG1" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", "T_AMT1", "T_AMT2", - "T_COUNT", - "T_RC" + "T_COUNT" ] }, { @@ -20286,10 +28861,13 @@ const SVC_SCREENS = [ "T_ARG1" ], "out": [ - "T_COUNT", + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2", - "T_ARG3", - "T_RC" + "T_COUNT", + "T_ARG3" ] }, { @@ -20299,8 +28877,13 @@ const SVC_SCREENS = [ "desc": "기능: 서비스 상태 (전체 OPEN 의심거래 수, RC=0).", "in": [], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", + "T_COUNT" ] }, { @@ -20312,7 +28895,13 @@ const SVC_SCREENS = [ "T_PURCHASE_ID" ], "out": [ - "T_RC" + "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -20321,11 +28910,18 @@ const SVC_SCREENS = [ "svc": "VL_SUMFEE", "desc": "기능: 통계-원천 수수료/순액 대사 (merchant_summary fee/net vs purchase 재집계, RC 비트합 1=수수료 2=순액", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_MERCHANT" ], "out": [ "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_AMT3", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_FEE", "T_AMT1", "T_NET", @@ -20342,8 +28938,13 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", + "T_COUNT" ] }, { @@ -20352,11 +28953,18 @@ const SVC_SCREENS = [ "svc": "VL_SUMGROSS", "desc": "기능: 통계-원천 총액 대사 (merchant_summary 총액 vs 원천 매입금액 합, RC 1=불일치 8=통계없음).", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_MERCHANT" ], "out": [ "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_AMT3", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_GROSS", "T_AMT1", "T_DELTA" @@ -20371,8 +28979,13 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", + "T_COUNT" ] }, { @@ -20381,11 +28994,18 @@ const SVC_SCREENS = [ "svc": "VL_SUMNETCHK", "desc": "기능: 통계-원천 순액 대사 (merchant_summary 순액 vs 원천 매입순액 합, RC 1=불일치 8=통계없음).", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_MERCHANT" ], "out": [ "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_AMT3", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_NET", "T_AMT1" ] @@ -20396,13 +29016,19 @@ const SVC_SCREENS = [ "svc": "VL_SUMRECON", "desc": "기능: 통계-원천 대사 검증 (merchant_summary 건수/총액 vs purchase 재집계, RC 비트합 1=건수 2=총액, 8=통계 ", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_MERCHANT" ], "out": [ "T_RC", - "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_AMT3", + "T_ARG6", + "T_ARG2", "T_ARG1", + "T_COUNT", "T_GROSS", "T_AMT1" ] @@ -20413,13 +29039,19 @@ const SVC_SCREENS = [ "svc": "VL_SUMTXNCNT", "desc": "기능: 통계-원천 건수 대사 (merchant_summary 건수 vs 원천 매입 건수, RC 1=불일치 8=통계없음).", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_MERCHANT" ], "out": [ "T_RC", - "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_AMT3", + "T_ARG6", + "T_ARG2", "T_ARG1", + "T_COUNT", "T_DELTA" ] }, @@ -20433,8 +29065,12 @@ const SVC_SCREENS = [ "T_STR1" ], "out": [ - "T_ARG1", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -20443,12 +29079,19 @@ const SVC_SCREENS = [ "svc": "VL_SUSPACT", "desc": "기능: 의심거래 워크플로 2단계 조치 등록 (조치내용 detail 부기 + 검증로그 적재, RC 2=이미 종결 3=없음 4=조치내용 누락).", "in": [ + "T_BIZDATE", + "T_PURCHASE_ID", "T_ID1", - "T_REASON", - "T_BIZDATE" + "T_REASON" ], "out": [ "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_PURCHASE_ID" ] }, @@ -20461,9 +29104,12 @@ const SVC_SCREENS = [ "T_ARG1" ], "out": [ - "T_COUNT", + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2", - "T_RC" + "T_COUNT" ] }, { @@ -20476,8 +29122,12 @@ const SVC_SCREENS = [ "T_STR1" ], "out": [ - "T_ARG1", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -20489,8 +29139,13 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", + "T_COUNT" ] }, { @@ -20502,7 +29157,12 @@ const SVC_SCREENS = [ "T_ID1" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -20515,6 +29175,10 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", "T_ARG1" ] }, @@ -20528,7 +29192,12 @@ const SVC_SCREENS = [ "T_MSG" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -20541,8 +29210,12 @@ const SVC_SCREENS = [ "T_STR1" ], "out": [ - "T_ARG1", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -20555,7 +29228,12 @@ const SVC_SCREENS = [ "T_REASON" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -20567,11 +29245,14 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_COUNT", - "T_ARG1", + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2", - "T_ARG3", - "T_RC" + "T_ARG1", + "T_COUNT", + "T_ARG3" ] }, { @@ -20585,6 +29266,10 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", "T_ARG1" ] }, @@ -20593,11 +29278,16 @@ const SVC_SCREENS = [ "modnm": "정합성검증", "svc": "VL_THRAUDIT", "desc": "기능: 임계값 설정 감사 (RC 비트합 1=ACTIVE ERROR/CRIT 규칙의 임계값 미설정, 2=CRIT 규칙 비활성 방치).", - "in": [], + "in": [ + "T_STR3" + ], "out": [ - "T_ARG1", + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2", - "T_RC" + "T_ARG1" ] }, { @@ -20611,7 +29301,11 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2", + "T_ARG1", "T_ARG3" ] }, @@ -20626,6 +29320,9 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2" ] }, @@ -20635,11 +29332,15 @@ const SVC_SCREENS = [ "svc": "VL_THRESHOLD", "desc": "기능: 규칙 임계치 검사 (dbio get_rule; amount>threshold → RC 1).", "in": [ - "T_STR3", - "T_AMOUNT" + "T_AMOUNT", + "T_STR3" ], "out": [ "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", "T_ARG1" ] }, @@ -20653,6 +29354,10 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", "T_ARG1" ] }, @@ -20665,7 +29370,12 @@ const SVC_SCREENS = [ "T_STR3" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -20679,6 +29389,9 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2", "T_ARG3" ] @@ -20694,6 +29407,9 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2" ] }, @@ -20708,6 +29424,11 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", "T_DELTA" ] }, @@ -20720,11 +29441,16 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_AMT1", "T_AMT2", "T_AMT3", - "T_DELTA", - "T_RC" + "T_DELTA" ] }, { @@ -20737,6 +29463,12 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_NET", "T_AMT1", "T_AMT2" @@ -20748,10 +29480,17 @@ const SVC_SCREENS = [ "svc": "VL_TXNREV", "desc": "기능: 역전거래 검출 건별 (RC 비트 1=음수금액 2=음수순액 4=순액>금액, 8=매입없음).", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_AMOUNT" ], "out": [ "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_AMOUNT", "T_NET" ] @@ -20761,11 +29500,18 @@ const SVC_SCREENS = [ "modnm": "정합성검증", "svc": "VL_UNACQ", "desc": "기능: 미매입 승인 검출 (매입 원장에 없는 승인 레코드 수, RC 1=고아 승인 존재).", - "in": [], + "in": [ + "T_ID1" + ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT", - "T_ID1", - "T_RC" + "T_ID1" ] }, { @@ -20774,13 +29520,17 @@ const SVC_SCREENS = [ "svc": "VL_UNACQAGE", "desc": "기능: 장기 미확정 매입 검출 (접수 A/보류 H 상태로 N일 초과 경과, RC 1=존재).", "in": [ + "T_ID1", "T_ARG1" ], "out": [ - "T_COUNT", - "T_ID1", + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2", - "T_RC" + "T_COUNT", + "T_ID1" ] }, { @@ -20793,8 +29543,12 @@ const SVC_SCREENS = [ "T_ARG1" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_COUNT" ] }, { @@ -20806,8 +29560,13 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", + "T_COUNT" ] } ]; diff --git a/app/ui/screens.js b/app/ui/screens.js index 2ba769b..3d081da 100644 --- a/app/ui/screens.js +++ b/app/ui/screens.js @@ -4,48 +4,66 @@ const SVC_SCREENS = [ "mod": "ac", "modnm": "매입", "svc": "ACQUIRE", - "desc": "* ACQUIRE.pgc - ac 모듈 서비스 ACQUIRE (one service = one file).", + "desc": "매입 접수(원팁): 가맹점/금액 검증 -> MDR 정책 조회 -> 금액구간·한도 규칙 판정", "in": [ "T_MERCHANT", "T_AMOUNT", "T_BIZDATE" ], "out": [ + "T_RC", "T_PURCHASE_ID", "T_FEE", - "T_NET" + "T_NET", + "T_ARG1", + "T_ARG2", + "T_COUNT" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_ALERTAMT", - "desc": "매입 금액초과 알림: 가맹점의 영업일 매입 중 임계금액(T_AMOUNT)을 초과한", + "desc": "매입 금액초과 알림: 가맹점 영업일 매입 중 임계금액(T_AMOUNT)을 초과한", "in": [ - "T_AMOUNT", "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", - "T_AMT1", - "T_RC" + "T_AMOUNT", + "T_GROSS", + "T_ARG1", + "T_ARG2", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_ALERTCHK", - "desc": "매입 알림 점검: 가맹점의 당일 고액거래(임계금액 이상)와 보류 적체 건수를", + "desc": "매입 알림 점검: 가맹점의 당일 고액거래와 보류 적체 건수를 점검해", "in": [ - "T_AMT1", "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ "T_RC", + "T_ARG6", "T_ARG1", - "T_ARG2" + "T_ARG2", + "T_ARG3", + "T_AMT1", + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -58,8 +76,16 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", - "T_RC" + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -71,10 +97,18 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_ARG1", "T_ARG2", "T_ARG3", - "T_COUNT" + "T_ARG4", + "T_ARG5", + "T_COUNT", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -83,65 +117,103 @@ const SVC_SCREENS = [ "svc": "ACQ_ALERTVEL", "desc": "매입 속도 알림: 채널의 최근 5분간 유입 건수를 재어 분당 허용치를 넘으면", "in": [ + "T_MERCHANT", + "T_CHANNEL", "T_ARG1", - "T_CHANNEL" + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_COUNT", "T_ARG2", "T_ARG3", - "T_RC" + "T_ARG1", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_AMTFIX", - "desc": "금액 확정 정정: 직전 금액 조회 후 하우스 25bps 로 절대 재계산, 증감(delta) 회신.", + "desc": "금액 확정 정정: 직전 금액을 조회한 뒤 하우스 기준 MDR 로 수수료를 절대", "in": [ "T_PURCHASE_ID", - "T_AMOUNT" + "T_AMOUNT", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_AMOUNT", "T_DELTA", "T_FEE", - "T_NET" + "T_NET", + "T_ARG1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_APPRGAP", - "desc": "승인-매입 갭 점검: 승인 없는 매입(무승인 매입)과 매입 없는 승인(고아", + "desc": "승인-매입 갭 점검: 승인 없는 매입(무승인 매입)과 매입 없는 승인(고아 승인)을", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_ARG1", - "T_AMT1", "T_ARG2", - "T_RC" + "T_ARG3", + "T_ARG4", + "T_COUNT", + "T_GROSS", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_APPRLINK", - "desc": "* ACQ_APPRLINK.pgc - ac 모듈 서비스 ACQ_APPRLINK (one service = one file).", + "desc": "승인 연계: 매입 건에 승인 원장 행을 연결한다. 이미 승인이 있으면 추가", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_BIZDATE" ], - "out": [] + "out": [ + "T_RC", + "T_ARG6", + "T_COUNT", + "T_ID1", + "T_ARG1", + "T_ARG2" + ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_APPRMAP", - "desc": "* ACQ_APPRMAP.pgc - ac 모듈 서비스 ACQ_APPRMAP (one service = one file).", + "desc": "승인 매핑 조회: 한 매입에 연결된 승인 원장의 총 건수와 승인/거절 분포를", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_BIZDATE" ], "out": [ - "T_COUNT" + "T_RC", + "T_ARG6", + "T_COUNT", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -154,68 +226,113 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_GROSS", "T_FEE", - "T_RC" + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_NET", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_BRANDRANK", - "desc": "매입 브랜드별 순위: 발급사 매출 상위 순으로 커서 조회하며 누적 점유율을", + "desc": "매입 브랜드별 순위: 발급사 매출 상위 순으로 1~3위를 뽑아 점유율을 계산하고", "in": [ "T_BIZDATE" ], "out": [ - "T_GROSS", + "T_RC", + "T_ARG6", + "T_AMT1", + "T_AMT2", "T_ARG1", - "T_ARG2" + "T_ARG2", + "T_ARG3", + "T_COUNT", + "T_GROSS", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_BRANDSUM", - "desc": "매입 브랜드별 집계: 발급사(카드 브랜드) 기준으로 매출 합계를 그룹 집계해", + "desc": "매입 브랜드별 집계: 발급사(카드 브랜드) 기준 매출 합계를 그룹 집계해", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_COUNT", + "T_ARG1", + "T_AMT1", + "T_ARG2", + "T_ARG3", "T_GROSS", - "T_ARG1" + "T_FEE", + "T_ARG4", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_BULKCHK", - "desc": "대량매입 배치검증: 영업일 접수(A) 매입을 커서로 순회하며 금액 양수 및", + "desc": "대량매입 배치검증: 영업일 접수(A) 매입의 금액 양수 및 수수료 정합", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_COUNT", "T_ARG1", "T_ARG2", - "T_RC" + "T_ARG3", + "T_ARG4", + "T_ARG5", + "T_GROSS", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_BULKRPT", - "desc": "대량매입 리포트: 영업일 매입을 상태(접수/대사/정산/취소)별로 한 번에 분해", + "desc": "대량매입 리포트: 영업일 매입을 상태(접수/대사/정산/취소)별로 한 번에", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_COUNT", "T_ARG1", "T_ARG2", "T_ARG3", "T_ARG4", + "T_ARG5", "T_GROSS", - "T_NET" + "T_NET", + "T_FEE", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -229,16 +346,25 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG6", "T_COUNT", "T_ARG1", - "T_ARG2" + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_GROSS", + "T_FEE", + "T_NET", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_CANCEL", - "desc": "* ACQ_CANCEL.pgc - ac 모듈 서비스 ACQ_CANCEL (one service = one file).", + "desc": "매입 전액 취소: 취소 가능 상태를 확인하고 ac_cancel 에 FULL 취소행을 만든 뒤", "in": [ "T_PURCHASE_ID", "T_AMOUNT", @@ -246,21 +372,32 @@ const SVC_SCREENS = [ "T_REASON" ], "out": [ - "T_CANCEL_ID" + "T_RC", + "T_CANCEL_ID", + "T_AMOUNT", + "T_FEE", + "T_ARG1", + "T_COUNT", + "T_GROSS" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_CB_ARB", - "desc": "지불거절 중재: 라운드2까지 미해결 건을 카드사 중재로 넘긴다. 해당 CB", + "desc": "지불거절 중재: 라운드2까지 미해결 건을 카드사 중재로 넘긴다. 해당 CB 행", "in": [ - "T_CANCEL_ID", - "T_REASON" + "T_PURCHASE_ID", + "T_REASON", + "T_BIZDATE" ], "out": [ "T_RC", - "T_PURCHASE_ID" + "T_ARG6", + "T_CANCEL_ID", + "T_AMOUNT", + "T_ARG1", + "T_ARG2" ] }, { @@ -269,12 +406,17 @@ const SVC_SCREENS = [ "svc": "ACQ_CB_DEFEND", "desc": "지불거절 방어: 접수된 CB 건에 방어 증빙 사유를 덧붙이고, 보류 중이던", "in": [ - "T_CANCEL_ID", - "T_REASON" + "T_PURCHASE_ID", + "T_REASON", + "T_BIZDATE" ], "out": [ "T_RC", - "T_PURCHASE_ID" + "T_ARG6", + "T_CANCEL_ID", + "T_COUNT", + "T_ARG1", + "T_ARG2" ] }, { @@ -283,54 +425,81 @@ const SVC_SCREENS = [ "svc": "ACQ_CB_FINAL", "desc": "지불거절 최종확정: 중재 결과로 매입사 패소가 확정되면 원매입을 취소(C)로", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_REASON", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", - "T_RC" + "T_AMOUNT", + "T_DELTA", + "T_ARG1", + "T_ID1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_CB_OPEN", - "desc": "지불거절(chargeback) 접수: 대사완료/정산완료 매입만 접수 가능, ac_cancel 에", + "desc": "지불거절(chargeback) 접수: 대사완료/정산완료 매입만 접수 가능하며", "in": [ "T_PURCHASE_ID", "T_AMOUNT", - "T_BIZDATE", - "T_REASON" + "T_REASON", + "T_BIZDATE" ], "out": [ "T_RC", - "T_CANCEL_ID" + "T_ARG6", + "T_CANCEL_ID", + "T_AMOUNT", + "T_GROSS", + "T_ARG1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_CB_QRY", - "desc": "지불거절 목록조회: 가맹점·영업일 기준 CB 접수 건을 커서로 훑어 건수/합계를", + "desc": "지불거절 목록조회: 가맹점·영업일 기준 CB 접수 건을 집계해 건수/합계와", "in": [ "T_MERCHANT", "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", - "T_AMOUNT" + "T_GROSS", + "T_CANCEL_ID", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_CB_REVERSE", - "desc": "지불거절 역전: 방어 승소로 CB 를 무효화한다. 최근 CB/CB2 행을 조회해", + "desc": "지불거절 역전: 방어 승소로 CB 를 무효화한다. 최근 CB/CB2 행 사유를", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_BIZDATE" ], "out": [ "T_RC", - "T_CANCEL_ID" + "T_ARG6", + "T_COUNT", + "T_ARG1", + "T_AMOUNT", + "T_ARG2", + "T_ID1" ] }, { @@ -346,23 +515,36 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", - "T_CANCEL_ID" + "T_ARG6", + "T_CANCEL_ID", + "T_AMOUNT", + "T_AMT1", + "T_ARG1", + "T_COUNT" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_CB_STAT", - "desc": "지불거절 일별 통계: 영업일 기준 CB 건수·합계·평균·최대 금액과 방어 완료", + "desc": "지불거절 일별 통계: 영업일 기준 CB 건수·합계·평균·최대 금액과", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_GROSS", - "T_AMT1", - "T_AMT2", - "T_ARG1" + "T_AMOUNT", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ARG5", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -371,24 +553,44 @@ const SVC_SCREENS = [ "svc": "ACQ_CCYQRY", "desc": "매입 통화별 조회: 지정 통화(T_CCY)의 원통화 합계와 원화 환산 합계를", "in": [ - "T_CCY" + "T_CCY", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_COUNT", "T_FX_AMT", "T_GROSS", - "T_DELTA" + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_CCYRANK", - "desc": "매입 통화별 순위: 해외매입 통화 그룹을 원화 합계 내림차순으로 커서 조회해", + "desc": "매입 통화별 순위: 해외매입 통화를 원화 합계 내림차순으로 정렬해 1위 통화와", "in": [ "T_BIZDATE" ], "out": [ - "T_ARG1" + "T_RC", + "T_ARG6", + "T_COUNT", + "T_AMT1", + "T_AMT2", + "T_ARG1", + "T_ARG2", + "T_GROSS", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -401,51 +603,91 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", + "T_ARG1", + "T_GROSS", "T_FX_AMT", - "T_AMOUNT", - "T_ARG1" + "T_ARG2", + "T_ARG3", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_CHANDETAIL", - "desc": "매입 채널별 상세 조회: 영업일 채널 그룹의 건수/총액/평균을 커서로 훑어", + "desc": "매입 채널별 상세 조회: 지정 채널의 건수/총액/평균 단가와 전체 평균 대비", "in": [ + "T_CHANNEL", "T_BIZDATE" ], "out": [ - "T_GROSS", + "T_RC", + "T_ARG6", "T_COUNT", + "T_GROSS", "T_ARG1", - "T_AMT2" + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ARG5", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_CHANRANK", - "desc": "매입 채널별 순위: 영업일 채널을 건수 내림차순으로 정렬해 커서 조회하고", + "desc": "매입 채널별 순위: 영업일 채널을 건수 내림차순으로 정렬해 최다 유입 채널과", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", - "T_ARG1" + "T_AMT1", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_GROSS", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_CHANSTAT", - "desc": "매입 채널별 통계: 영업일 채널 그룹 집계를 커서로 순회하며 채널 종수와", + "desc": "매입 채널별 통계: 영업일 채널을 POS/EDC/EDI/해외 4군으로 분해 집계해", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_AMT1", - "T_ARG1" + "T_AMT2", + "T_AMT3", + "T_AMT4", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_COUNT", + "T_GROSS", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -454,26 +696,39 @@ const SVC_SCREENS = [ "svc": "ACQ_CLAIMPROC", "desc": "클레임 처리(종결): 등록된 클레임 행을 삭제하고 처리 결과를 원장에", "in": [ - "T_ID1", - "T_STR1", + "T_PURCHASE_ID", + "T_REASON", "T_BIZDATE" ], "out": [ "T_RC", - "T_PURCHASE_ID" + "T_ARG6", + "T_COUNT", + "T_ARG1", + "T_ARG2", + "T_ID1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_CLAIMQRY", - "desc": "클레임 목록조회: 영업일의 미종결 클레임을 커서로 순회, 총건수와 함께", + "desc": "클레임 목록조회: 영업일의 미종결 클레임(ac_unmatch CLAIM)을 집계해", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", - "T_ARG1" + "T_ID1", + "T_ARG1", + "T_GROSS", + "T_ARG2", + "T_ARG3", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -488,7 +743,10 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", - "T_ID1" + "T_ARG6", + "T_ID1", + "T_ARG1", + "T_AMOUNT" ] }, { @@ -497,37 +755,81 @@ const SVC_SCREENS = [ "svc": "ACQ_CORRAPPR", "desc": "매입 정정 승인: 접수된 조정(ac_fee_adj) 행의 사유 앞에 OK 승인 표식을", "in": [ - "T_ADJ" + "T_PURCHASE_ID", + "T_BIZDATE" ], "out": [ "T_RC", - "T_ADJ" + "T_ARG6", + "T_COUNT", + "T_ARG1", + "T_DELTA", + "T_ARG2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_CORRECT", - "desc": "* ACQ_CORRECT.pgc - ac 모듈 서비스 ACQ_CORRECT (one service = one file).", + "desc": "매입 정정: 금액/수수료를 함께 정정한다. 정정 전후 값을 조정 원장에", "in": [ "T_PURCHASE_ID", "T_AMOUNT", - "T_MERCHANT" + "T_REASON", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_ADJ", + "T_AMOUNT", "T_FEE", - "T_NET" + "T_NET", + "T_DELTA", + "T_ARG1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_CORRHIST", - "desc": "정정 이력조회: 매입 건에 붙은 수수료 조정 이력(최근 10건)을 역순 커서로", + "desc": "정정 이력조회: 매입 건에 붙은 수수료 조정 이력의 건수·순증감과", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_COUNT", + "T_DELTA", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ADJ", + "T_GROSS", + "T_ARG7", + "T_ARG8", + "T_ID2" + ] + }, + { + "mod": "ac", + "modnm": "매입", + "svc": "ACQ_CORRRSN", + "desc": "매입 정정 사유별 처리: 수수료 조정 원장에 사유 코드가 붙은 증감 행을", + "in": [ + "T_PURCHASE_ID", + "T_REASON", + "T_BIZDATE", + "T_DELTA" + ], + "out": [ + "T_RC", + "T_ARG6", + "T_ADJ", + "T_DELTA", + "T_AMT1", "T_COUNT", "T_ARG1" ] @@ -535,30 +837,24 @@ const SVC_SCREENS = [ { "mod": "ac", "modnm": "매입", - "svc": "ACQ_CORRRSN", - "desc": "매입 정정 사유별 처리: 수수료 조정 원장(ac_fee_adj)에 사유 코드가 붙은", + "svc": "ACQ_CORRSTAT", + "desc": "매입 정정 사유별 통계: 조정 원장을 사유 접두어별로 그룹 집계해 증액분/", "in": [ - "T_PURCHASE_ID", - "T_DELTA", - "T_REASON", "T_BIZDATE" ], "out": [ "T_RC", - "T_ADJ" - ] - }, - { - "mod": "ac", - "modnm": "매입", - "svc": "ACQ_CORRSTAT", - "desc": "매입 정정 사유별 통계: 조정 원장을 사유 접두어별로 그룹 집계해 사유코드·", - "in": [ - "T_BIZDATE" - ], - "out": [ - "T_GROSS", - "T_ARG1" + "T_ARG6", + "T_COUNT", + "T_DELTA", + "T_AMT1", + "T_AMT2", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -571,53 +867,85 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_GROSS", - "T_ARG1" + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ARG5", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_CTRYRANK", - "desc": "매입 국가별 순위: 통화코드를 권역(미주/일본/유럽/기타)으로 분류하는 C", + "desc": "매입 국가별 순위: 통화코드를 권역(미주/일본/유럽/기타)으로 분류하는", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_AMT1", + "T_AMT2", + "T_AMT3", + "T_AMT4", "T_ARG1", "T_ARG2", - "T_ARG3", - "T_ARG4", - "T_RC" + "T_COUNT", + "T_GROSS", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_CTRYSUM", - "desc": "국가별 매입 집계: 통화코드를 국가권역(미주/일본/유럽/기타)으로 매핑하는", + "desc": "국가별 매입 집계: 통화코드를 국가권역으로 매핑해 해외매입 원화금액을", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_ARG1", "T_ARG2", "T_ARG3", "T_ARG4", - "T_GROSS" + "T_ARG5", + "T_COUNT", + "T_GROSS", + "T_ID1", + "T_AMT1", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_DATECHG", - "desc": "* ACQ_DATECHG.pgc - ac 모듈 서비스 ACQ_DATECHG (one service = one file).", + "desc": "영업일 변경: 매입의 귀속 영업일을 요청일로 옮긴다. 정산이 끝난 건은", "in": [ "T_PURCHASE_ID", + "T_STR1", "T_BIZDATE" ], - "out": [] + "out": [ + "T_RC", + "T_ARG6", + "T_ARG1", + "T_ARG2" + ] }, { "mod": "ac", @@ -628,58 +956,93 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_ARG1", "T_ARG2", "T_ARG3", "T_ARG4", "T_ARG5", - "T_ARG6", "T_COUNT", - "T_GROSS" + "T_GROSS", + "T_NET", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_DDC", - "desc": "* ACQ_DDC.pgc - ac 모듈 서비스 ACQ_DDC (one service = one file).", + "desc": "동적통화환산(DDC): 카드 소지자 통화로 결제 금액을 환산해 제시한다.", "in": [ "T_PURCHASE_ID", - "T_FX_AMT", "T_CCY", + "T_FX_AMT", + "T_ARG1", "T_BIZDATE" ], "out": [ - "T_AMOUNT" + "T_RC", + "T_ARG6", + "T_ID1", + "T_AMOUNT", + "T_FX_AMT", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ARG1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_DOWSTAT", - "desc": "매입 요일별 통계: 지정 기간(T_STR1~T_STR2)의 매입을 요일(0~6)별로 그룹", + "desc": "매입 요일별 통계: 지정 기간(T_STR1~T_STR2)의 매입을 평일/주말로 분해", "in": [ "T_STR1", - "T_STR2" + "T_STR2", + "T_BIZDATE" ], "out": [ - "T_COUNT", + "T_RC", + "T_ARG6", "T_ARG1", - "T_RC" + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ARG5", + "T_COUNT", + "T_GROSS", + "T_AMT1", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_DUPCHK", - "desc": "* ACQ_DUPCHK.pgc - ac 모듈 서비스 ACQ_DUPCHK (one service = one file).", + "desc": "중복매입 단건 점검: 가맹점·영업일·금액이 모두 같은 매입이 몇 건 존재하는지", "in": [ "T_MERCHANT", - "T_BIZDATE", - "T_AMOUNT" + "T_AMOUNT", + "T_BIZDATE" ], "out": [ - "T_COUNT" + "T_RC", + "T_ARG6", + "T_COUNT", + "T_ARG1", + "T_ARG2", + "T_ID1", + "T_ID2", + "T_GROSS", + "T_ARG7", + "T_ARG8" ] }, { @@ -691,82 +1054,122 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_ARG1", - "T_AMT1", - "T_RC" + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_GROSS", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_EDC", - "desc": "* ACQ_EDC.pgc - ac 모듈 서비스 ACQ_EDC (one service = one file).", + "desc": "EDC 단말 매입 접수: EDC 채널로 유입된 매입을 현행 MDR 로 수수료 산출 후", "in": [ "T_MERCHANT", "T_AMOUNT", "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_PURCHASE_ID", "T_FEE", - "T_NET" + "T_NET", + "T_ARG1", + "T_AMT1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_EDI", - "desc": "* ACQ_EDI.pgc - ac 모듈 서비스 ACQ_EDI (one service = one file).", + "desc": "EDI 청구 매입 접수: EDI 문서로 청구된 매입을 접수하고 동시에 ac_edi", "in": [ "T_MERCHANT", + "T_AMOUNT", "T_DOCNO", - "T_BIZDATE", - "T_AMOUNT" + "T_BIZDATE" ], - "out": [] + "out": [ + "T_RC", + "T_ARG6", + "T_PURCHASE_ID", + "T_ID1", + "T_FEE", + "T_NET", + "T_ARG1" + ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_EDIQRY", - "desc": "EDI 조회: 지정 가맹점(T_MERCHANT)의 영업일 EDI 접수 문서 건수와 금액", + "desc": "EDI 조회: 지정 가맹점(T_MERCHANT)의 영업일 EDI 접수 문서 건수와", "in": [ "T_MERCHANT", "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_GROSS", - "T_RC" + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ID1", + "T_AMT1", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_EDIRESEND", - "desc": "EDI 재전송: 문서번호의 최신 EDI 원장을 찾아 재전송 사본(-R 접미 문서번호)", + "desc": "EDI 재전송: 문서번호의 최신 EDI 원장을 찾아 재전송 사본(-R 접미 문서번호)을", "in": [ - "T_DOCNO" + "T_PURCHASE_ID", + "T_DOCNO", + "T_BIZDATE" ], "out": [ "T_RC", + "T_ARG6", "T_ID1", - "T_AMOUNT" + "T_AMOUNT", + "T_ARG1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_EDIRSNDCH", - "desc": "EDI 재전송 채널별: 영업일 미수신 EDI 문서를 가맹점별로 커서 조회해", + "desc": "EDI 재전송 채널별: 영업일 미수신 EDI 문서를 가맹점별로 집계해 재전송", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_GROSS", - "T_RC" + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -778,26 +1181,43 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_ARG1", "T_ARG2", "T_ARG3", - "T_GROSS" + "T_ARG4", + "T_ARG5", + "T_COUNT", + "T_GROSS", + "T_ID1", + "T_AMT1", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_ERRCLASS", - "desc": "매입 오류유형별 분류: 미매입 원장(ac_unmatch)의 사유 접두어를 읽어", + "desc": "매입 오류유형별 분류: 미매입 원장의 사유 접두어를 읽어 중복/한도/환율/기타", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_ARG1", "T_ARG2", "T_ARG3", "T_ARG4", - "T_COUNT" + "T_ARG5", + "T_COUNT", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -806,11 +1226,21 @@ const SVC_SCREENS = [ "svc": "ACQ_ERRQRY", "desc": "매입 오류유형별 조회: 사유 접두어(T_REASON)로 시작하는 미매입 건을", "in": [ - "T_REASON" + "T_REASON", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", - "T_RC" + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_GROSS", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -819,25 +1249,37 @@ const SVC_SCREENS = [ "svc": "ACQ_ERRRETRY", "desc": "매입 오류 재처리: 보류(H) 상태로 묶여 있던 오류 매입을 접수(A)로 되돌려", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_BIZDATE" ], "out": [ - "T_RC" + "T_RC", + "T_ARG6", + "T_COUNT", + "T_ARG1", + "T_ARG2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_ERRSTAT", - "desc": "매입 오류유형별 통계: 미매입 원장을 사유 접두어 기준으로 한 번의 집계에서", + "desc": "매입 오류유형별 통계: 미매입 원장을 사유 접두어 기준으로 중복/한도/환율", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_COUNT", "T_ARG1", "T_ARG2", "T_ARG3", - "T_COUNT" + "T_ARG4", + "T_ARG5", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -848,11 +1290,17 @@ const SVC_SCREENS = [ "in": [ "T_PURCHASE_ID", "T_AMOUNT", + "T_REASON", "T_BIZDATE" ], "out": [ "T_RC", - "T_SETTLE_ID" + "T_ARG6", + "T_ID1", + "T_AMOUNT", + "T_AMT1", + "T_ARG1", + "T_GROSS" ] }, { @@ -862,11 +1310,18 @@ const SVC_SCREENS = [ "desc": "매입 에스크로 해제: 예치(ESCROW)와 해제(ESCROW_REL) 원장 잔액을 계산해", "in": [ "T_PURCHASE_ID", + "T_REASON", "T_BIZDATE" ], "out": [ - "T_GROSS", - "T_RC" + "T_RC", + "T_ARG6", + "T_ID1", + "T_AMOUNT", + "T_AMT1", + "T_AMT2", + "T_ARG1", + "T_ARG2" ] }, { @@ -875,46 +1330,62 @@ const SVC_SCREENS = [ "svc": "ACQ_EVENTBUY", "desc": "이벤트 매입: 프로모션 행사 매입을 EVENT 채널로 접수한다. 활성 가맹점만", "in": [ + "T_MERCHANT", "T_AMOUNT", "T_ARG1", - "T_MERCHANT", "T_BIZDATE" ], "out": [ "T_RC", + "T_ARG6", "T_PURCHASE_ID", - "T_FEE" + "T_FEE", + "T_NET", + "T_DELTA", + "T_ARG2", + "T_ARG1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_EXCHANGE", - "desc": "매입 교환 처리: 반품 후 동일가 재구매를 한 번에 처리한다. 원매입에 대해", + "desc": "매입 교환 처리: 반품 후 동일가 재구매를 한 번에 처리한다. 원매입에", "in": [ "T_PURCHASE_ID", - "T_AMOUNT", "T_REASON", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT" ], "out": [ "T_RC", - "T_CANCEL_ID" + "T_ARG6", + "T_CANCEL_ID", + "T_DELTA", + "T_AMOUNT", + "T_ARG1", + "T_ID1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_FEEADJ", - "desc": "* ACQ_FEEADJ.pgc - ac 모듈 서비스 ACQ_FEEADJ (one service = one file).", + "desc": "수수료 조정: 요청 증감액(T_DELTA)만큼 수수료 조정 이력을 남기고", "in": [ "T_PURCHASE_ID", - "T_DELTA", + "T_REASON", "T_BIZDATE", - "T_REASON" + "T_DELTA" ], "out": [ - "T_ADJ" + "T_RC", + "T_ARG6", + "T_ADJ", + "T_DELTA", + "T_FEE", + "T_NET", + "T_ARG1" ] }, { @@ -923,30 +1394,43 @@ const SVC_SCREENS = [ "svc": "ACQ_FEEHIST", "desc": "수수료 조정 이력 집계: 매입 건의 조정 건수, 순증감 합계, 증액분/감액분을", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_DELTA", "T_AMT1", "T_AMT2", - "T_RC" + "T_FEE", + "T_NET", + "T_ARG1", + "T_ARG2", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_FEERECALC", - "desc": "수수료 재산정: 가맹점의 현행 MDR 로 수수료를 다시 계산해 기존 수수료와의", + "desc": "수수료 재산정: 가맹점의 현행 MDR 로 수수료를 다시 계산해 기존 수수료와", "in": [ "T_PURCHASE_ID", "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_ADJ", "T_FEE", + "T_NET", "T_DELTA", - "T_RC" + "T_ARG1", + "T_ARG2" ] }, { @@ -957,28 +1441,42 @@ const SVC_SCREENS = [ "in": [ "T_PURCHASE_ID", "T_ARG1", + "T_REASON", "T_BIZDATE" ], "out": [ "T_RC", - "T_AMT1", - "T_ADJ" + "T_ARG6", + "T_ADJ", + "T_DELTA", + "T_FEE", + "T_NET", + "T_ARG2", + "T_ARG3" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_FOREIGN", - "desc": "* ACQ_FOREIGN.pgc - ac 모듈 서비스 ACQ_FOREIGN (one service = one file).", + "desc": "해외 매입 접수: 해외 카드 매입을 FOREIGN 채널로 접수한다. 원통화 금액과", "in": [ "T_MERCHANT", - "T_AMOUNT", - "T_BIZDATE", - "T_CCY" + "T_CCY", + "T_FX_AMT", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_PURCHASE_ID", - "T_FEE" + "T_AMOUNT", + "T_FX_AMT", + "T_ARG2", + "T_FEE", + "T_NET", + "T_ID1", + "T_ARG1" ] }, { @@ -988,47 +1486,69 @@ const SVC_SCREENS = [ "desc": "통화 환산 등록: 해외매입의 원통화 금액에 적용환율(bps)을 곱해 원화를", "in": [ "T_PURCHASE_ID", - "T_FX_AMT", - "T_ARG1", "T_CCY", - "T_BIZDATE" + "T_FX_AMT", + "T_BIZDATE", + "T_ARG1" ], "out": [ "T_RC", + "T_ARG6", "T_ID1", - "T_AMOUNT" + "T_AMOUNT", + "T_FX_AMT", + "T_ARG2", + "T_ARG1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_FXRATE", - "desc": "통화 적용환율 조회: 해외매입 원장(ac_fx)에서 특정 통화의 최근 영업일", + "desc": "통화 적용환율 조회: 해외매입 원장에서 특정 통화의 최근 영업일 평균/최저/", "in": [ "T_CCY", "T_BIZDATE" ], "out": [ "T_RC", + "T_ARG6", + "T_COUNT", "T_ARG1", "T_ARG2", "T_ARG3", - "T_COUNT" + "T_ARG4", + "T_ARG5", + "T_GROSS", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_GRADESTAT", - "desc": "매입 가맹점등급별 통계: 가맹점 MDR(bps)을 프리미엄/일반/기본 등급으로 나눠", + "desc": "매입 가맹점등급별 통계: 가맹점 MDR(bps)을 프리미엄/일반/기본 등급으로", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_AMT1", + "T_AMT2", + "T_AMT3", "T_ARG1", "T_ARG2", "T_ARG3", - "T_GROSS" + "T_COUNT", + "T_GROSS", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -1038,28 +1558,40 @@ const SVC_SCREENS = [ "desc": "매입 보증 등록: 대형 매입에 대한 이행보증금을 원장에 GUARANTEE 항목으로", "in": [ "T_PURCHASE_ID", - "T_AMOUNT", "T_ARG1", "T_BIZDATE" ], "out": [ "T_RC", - "T_AMT1", - "T_SETTLE_ID" + "T_ARG6", + "T_ID1", + "T_AMOUNT", + "T_ARG2", + "T_GROSS", + "T_ARG1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_GUARQRY", - "desc": "매입 보증 조회: 한 매입에 적립된 보증금(GUARANTEE) 총액과 건수를 원장에서", + "desc": "매입 보증 조회: 한 매입에 적립된 보증금(GUARANTEE) 총액과 건수를", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_BIZDATE" ], "out": [ - "T_AMT1", + "T_RC", + "T_ARG6", "T_COUNT", - "T_RC" + "T_GROSS", + "T_AMOUNT", + "T_ARG1", + "T_ARG2", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -1068,98 +1600,160 @@ const SVC_SCREENS = [ "svc": "ACQ_HALFSTAT", "desc": "매입 반기별 통계: 지정 연도(T_ARG1) 매입을 상반기(1~6월)/하반기(7~12월)로", "in": [ - "T_ARG1" + "T_ARG1", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_AMT1", + "T_AMT2", + "T_ARG1", "T_ARG2", "T_ARG3", + "T_ARG4", + "T_AMT3", "T_COUNT", - "T_RC" + "T_GROSS", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_HOLD", - "desc": "매입 보류: 접수/대사(A,M) 상태만 보류로 전이, 반영 건수를 T_RC 로.", + "desc": "매입 보류: 접수/대사(A,M) 상태만 보류로 전이한다. 보류 사유를 미매입 원장에", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_REASON", + "T_BIZDATE" ], "out": [ - "T_RC" + "T_RC", + "T_AMOUNT", + "T_COUNT", + "T_GROSS", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ID1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_HOLDQRY", - "desc": "보류 매입 조회: 가맹점의 보류(H) 건을 최근 갱신순 커서로 훑어 건수·총액을", + "desc": "보류 매입 조회: 가맹점의 보류(H) 건을 집계해 건수·총액과 최고 보류액,", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_GROSS", - "T_RC" + "T_AMOUNT", + "T_PURCHASE_ID", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_AMT1", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_HOURSTAT", - "desc": "매입 시간대별 통계: 접수시각을 새벽/오전/오후/야간 네 구간으로 나눠 구간별", + "desc": "매입 시간대별 통계: 접수시각을 새벽/오전/오후/야간 네 구간으로 나눠", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_ARG1", "T_ARG2", "T_ARG3", "T_ARG4", - "T_RC" + "T_ARG5", + "T_COUNT", + "T_GROSS", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_INSTALL", - "desc": "* ACQ_INSTALL.pgc - ac 모듈 서비스 ACQ_INSTALL (one service = one file).", + "desc": "할부 등록: 요청 개월수로 매입금액을 균등 분할해 ac_install 에 회차별", "in": [ "T_PURCHASE_ID", "T_INSTALL_N", - "T_AMOUNT", "T_BIZDATE" ], "out": [ - "T_COUNT" + "T_RC", + "T_ARG6", + "T_COUNT", + "T_INSTALL_N", + "T_AMT1", + "T_ARG1", + "T_ARG2", + "T_GROSS" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_INSTCXL", - "desc": "할부 취소: 한 매입의 할부 스케줄 전 회차를 원장에서 삭제해 할부를 해지한다.", + "desc": "할부 취소: 한 매입의 할부 스케줄 전 회차를 원장에서 삭제해 할부를", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_REASON", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", - "T_RC" + "T_INSTALL_N", + "T_GROSS", + "T_ARG1", + "T_ID1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_INSTPLAN", - "desc": "할부 계획 조회: 한 매입의 약정 개월수와 회차 합계, 회차당 금액을 단건", + "desc": "할부 계획 조회: 한 매입의 약정 개월수와 회차 합계, 회차당 금액을 조회해", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_COUNT", "T_INSTALL_N", "T_GROSS", + "T_AMOUNT", + "T_ARG1", + "T_ARG2", + "T_ARG3", "T_AMT1", - "T_RC" + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -1174,63 +1768,107 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG6", + "T_COUNT", "T_GROSS", - "T_COUNT" + "T_ARG2", + "T_ARG3", + "T_ID1", + "T_ARG4" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_INSTQRY", - "desc": "할부 회차조회: 한 매입의 할부 스케줄(ac_install)을 회차 순으로 커서 조회해", + "desc": "할부 회차조회: 한 매입의 할부 스케줄을 집계해 총 회차수와 합계,", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_BIZDATE" ], "out": [ - "T_INSTALL_N", + "T_RC", + "T_ARG6", + "T_COUNT", "T_GROSS", - "T_RC" + "T_AMT1", + "T_AMT2", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ARG5", + "T_AMOUNT", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_ISSSTAT", - "desc": "매입 발급사별 통계: 영업일 발급사 그룹 집계에서 1위 발급사(건수·총액)와", + "desc": "매입 발급사별 통계: 영업일 발급사 그룹에서 1위 발급사(건수·총액)와", "in": [ "T_BIZDATE" ], "out": [ "T_RC", + "T_ARG6", + "T_COUNT", + "T_ARG1", + "T_AMT1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ARG5", + "T_GROSS", + "T_ARG7", + "T_ARG8", + "T_ID2" + ] + }, + { + "mod": "ac", + "modnm": "매입", + "svc": "ACQ_ISSUERCLS", + "desc": "발급사 분류 지정: 매입의 발급사(issuer) 코드를 지정 값으로 갱신하고", + "in": [ + "T_PURCHASE_ID", + "T_ISSUER", + "T_BIZDATE" + ], + "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_GROSS", "T_ARG1", "T_ARG2" ] }, - { - "mod": "ac", - "modnm": "매입", - "svc": "ACQ_ISSUERCLS", - "desc": "* ACQ_ISSUERCLS.pgc - ac 모듈 서비스 ACQ_ISSUERCLS (one service = one file).", - "in": [ - "T_PURCHASE_ID", - "T_ISSUER" - ], - "out": [] - }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_LIMITCHK", - "desc": "* ACQ_LIMITCHK.pgc - ac 모듈 서비스 ACQ_LIMITCHK (one service = one file).", + "desc": "매입 한도 점검: 가맹점 일한도와 당일 누적 매입을 비교해 초과 여부를", "in": [ "T_MERCHANT", "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_GROSS", - "T_RC" + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_COUNT", + "T_AMT1", + "T_ARG4", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -1239,12 +1877,18 @@ const SVC_SCREENS = [ "svc": "ACQ_LIMITEXC", "desc": "매입 한도 예외처리: 가맹점 일한도와 당일 누적 매입을 비교해 한도 초과 시", "in": [ - "T_MERCHANT", + "T_PURCHASE_ID", "T_BIZDATE" ], "out": [ "T_RC", - "T_DELTA" + "T_ARG6", + "T_GROSS", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ID1" ] }, { @@ -1253,11 +1897,18 @@ const SVC_SCREENS = [ "svc": "ACQ_LIMITOVR", "desc": "매입 한도 초과 승인: 한도 초과로 보류(H)된 매입을 관리자가 예외 승인해", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_REASON", + "T_BIZDATE" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG6", + "T_GROSS", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ID1" ] }, { @@ -1271,40 +1922,66 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG6", + "T_GROSS", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_COUNT", "T_AMT1", "T_AMT2", - "T_AMT3", - "T_ARG1" + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_MERCHSUM", - "desc": "* ACQ_MERCHSUM.pgc - ac 모듈 서비스 ACQ_MERCHSUM (one service = one file).", + "desc": "가맹점 일별 집계: 영업일 매입을 재집계해 merchant_summary 에 upsert 하고", "in": [ "T_MERCHANT", "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", - "T_GROSS" + "T_GROSS", + "T_FEE", + "T_NET", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ARG5", + "T_ARG6" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_MONSTAT", - "desc": "매입 월간 통계: 기준일이 속한 달 전체의 건수/총액/평균 거래액과 영업일수", + "desc": "매입 월간 통계: 기준일이 속한 달 전체의 건수/총액/평균 거래액과", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_GROSS", - "T_AMT1", "T_ARG1", - "T_RC" + "T_ARG2", + "T_AMT1", + "T_FEE", + "T_NET", + "T_ARG3", + "T_ARG4", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -1313,13 +1990,18 @@ const SVC_SCREENS = [ "svc": "ACQ_NETCALC", "desc": "정산금 재계산: 매입금액에서 수수료와 원천세 누계를 차감한 실정산금을", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_BIZDATE" ], "out": [ "T_RC", + "T_ARG6", "T_NET", + "T_DELTA", "T_TAX", - "T_DELTA" + "T_FEE", + "T_AMOUNT", + "T_ARG1" ] }, { @@ -1333,16 +2015,41 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", - "T_AMT1", + "T_ARG6", + "T_ADJ", "T_INSTALL_N", - "T_ADJ" + "T_ARG2", + "T_DELTA", + "T_FEE", + "T_ARG1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_PARTCXL", - "desc": "부분취소: 취소액이 잔여 매입금액 미만인 경우에만 허용. 수수료를 비례", + "desc": "부분취소: 취소액이 잔여 매입금액 미만인 경우에만 허용한다. 수수료를", + "in": [ + "T_PURCHASE_ID", + "T_AMOUNT", + "T_REASON", + "T_BIZDATE" + ], + "out": [ + "T_RC", + "T_ARG6", + "T_CANCEL_ID", + "T_AMOUNT", + "T_FEE", + "T_ARG1", + "T_ARG2" + ] + }, + { + "mod": "ac", + "modnm": "매입", + "svc": "ACQ_PARTIAL", + "desc": "부분처리 등록: 매입의 일부 금액만 확정 처리하고 나머지는 미확정으로", "in": [ "T_PURCHASE_ID", "T_AMOUNT", @@ -1350,23 +2057,12 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG6", "T_CANCEL_ID", - "T_AMT1", - "T_FEE" - ] - }, - { - "mod": "ac", - "modnm": "매입", - "svc": "ACQ_PARTIAL", - "desc": "* ACQ_PARTIAL.pgc - ac 모듈 서비스 ACQ_PARTIAL (one service = one file).", - "in": [ - "T_PURCHASE_ID", "T_AMOUNT", - "T_BIZDATE" - ], - "out": [ - "T_CANCEL_ID" + "T_ARG1", + "T_ARG2", + "T_ARG3" ] }, { @@ -1377,12 +2073,18 @@ const SVC_SCREENS = [ "in": [ "T_PURCHASE_ID", "T_AMOUNT", + "T_REASON", "T_BIZDATE" ], "out": [ "T_RC", + "T_ARG6", "T_CANCEL_ID", - "T_AMT2" + "T_AMOUNT", + "T_AMT1", + "T_ARG1", + "T_ARG2", + "T_ID1" ] }, { @@ -1391,102 +2093,153 @@ const SVC_SCREENS = [ "svc": "ACQ_PROMO", "desc": "프로모션 매입 등록: 행사 코드가 유효하면 수수료 0 의 프로모션 매입을", "in": [ - "T_AMOUNT", "T_MERCHANT", - "T_STR1", + "T_AMOUNT", "T_BIZDATE" ], "out": [ "T_RC", + "T_ARG6", "T_PURCHASE_ID", - "T_NET" + "T_FEE", + "T_NET", + "T_DELTA", + "T_ARG1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_PROMOQRY", - "desc": "프로모션 조회: 영업일 프로모션(category=PROMO) 매입의 건수와 매출, 감면", + "desc": "프로모션 조회: 영업일 프로모션(category=PROMO) 매입의 건수와 매출,", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_GROSS", "T_FEE", - "T_RC" + "T_NET", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_PROMOSTAT", - "desc": "프로모션 통계: 영업일 업종/분류별 매출을 그룹 집계해 분류코드·매출 쌍을", + "desc": "프로모션 통계: 영업일 업종/분류별 매출을 그룹 집계해 최다 분류와", "in": [ "T_BIZDATE" ], "out": [ - "T_GROSS", + "T_RC", + "T_ARG6", + "T_COUNT", + "T_AMT1", "T_AMT2", - "T_RC" + "T_ARG1", + "T_ARG2", + "T_GROSS", + "T_AMT3", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_QTRSTAT", - "desc": "매입 분기별 통계: 지정 연도(T_ARG1)의 매입 매출을 1~4분기로 나눠 분기별", + "desc": "매입 분기별 통계: 지정 연도(T_ARG1)의 매입 매출을 1~4분기로 나눠", "in": [ - "T_ARG1" + "T_ARG1", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_AMT1", + "T_AMT2", + "T_AMT3", + "T_AMT4", "T_ARG2", "T_ARG3", "T_ARG4", - "T_ARG5", - "T_GROSS" + "T_COUNT", + "T_GROSS", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_REBUY", - "desc": "재매입: 취소(C)된 원매입의 가맹점/금액/채널을 승계해 신규 매입을 생성하고", + "desc": "재매입: 취소(C)된 원매입의 가맹점/금액을 승계해 신규 매입을 생성하고", "in": [ "T_PURCHASE_ID", "T_BIZDATE" ], "out": [ "T_RC", + "T_ARG6", + "T_PURCHASE_ID", "T_ID1", + "T_AMOUNT", "T_FEE", - "T_NET" + "T_NET", + "T_COUNT", + "T_ARG1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_REBUYAPPR", - "desc": "재매입 승인: 취소(C)된 원매입에 한해 승인 원장(approval)에 재승인 행을", + "desc": "재매입 승인: 취소(C)된 원매입에 한해 승인 원장에 재승인 행을 남기고", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_BIZDATE" ], "out": [ "T_RC", - "T_ID1" + "T_ARG6", + "T_AMOUNT", + "T_ARG1", + "T_ARG2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_REBUYHIST", - "desc": "재매입 이력: 한 매입의 승인 원장을 승인시각 순으로 커서 조회해 승인 ID를", + "desc": "재매입 이력: 한 매입의 승인 원장을 집계해 총 승인 횟수와 최근 승인 ID,", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", - "T_RC" + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ID1", + "T_AMOUNT", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -1500,48 +2253,67 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_ID1", - "T_RC" + "T_COUNT", + "T_ARG1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_RECLASS", - "desc": "업종/분류 재지정: category 갱신 후 같은 가맹점에서 동일 분류 건수를 집계 회신.", + "desc": "업종/분류 재지정: 매입의 category 를 요청 분류로 갱신하고 같은 가맹점의", "in": [ "T_PURCHASE_ID", - "T_CATEGORY" + "T_CATEGORY", + "T_BIZDATE" ], "out": [ - "T_COUNT" + "T_RC", + "T_ARG6", + "T_COUNT", + "T_ARG1", + "T_ARG2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_REFUND", - "desc": "* ACQ_REFUND.pgc - ac 모듈 서비스 ACQ_REFUND (one service = one file).", + "desc": "매입 환불: 정산 완료 매입에 대한 환불을 접수한다. REFUND 취소행을", "in": [ "T_PURCHASE_ID", "T_AMOUNT", - "T_BIZDATE", - "T_REASON" + "T_REASON", + "T_BIZDATE" ], "out": [ - "T_CANCEL_ID" + "T_RC", + "T_ARG6", + "T_CANCEL_ID", + "T_AMOUNT", + "T_ARG1", + "T_ARG2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_RELEASE", - "desc": "보류 해제: 직전 상태를 조회한 뒤 H->A 전이, 이전 상태를 함께 회신.", + "desc": "보류 해제: 보류(H) 상태의 매입을 접수(A)로 되돌린다. 직전 상태를 함께", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_REASON", + "T_BIZDATE" ], "out": [ - "T_RC" + "T_RC", + "T_ARG6", + "T_ARG1", + "T_ID1", + "T_AMOUNT" ] }, { @@ -1550,23 +2322,34 @@ const SVC_SCREENS = [ "svc": "ACQ_REMATCH", "desc": "승인미매칭 재처리: 미매입 원장에 남은 건에 유효 승인이 도착했는지 다시", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_BIZDATE" ], "out": [ "T_RC", + "T_ARG6", + "T_COUNT", "T_ARG1", - "T_ARG2" + "T_ARG2", + "T_ARG3" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_REPROC", - "desc": "* ACQ_REPROC.pgc - ac 모듈 서비스 ACQ_REPROC (one service = one file).", + "desc": "매입 재처리: 취소/무효로 종료된 매입을 접수(A) 상태로 되살려 재처리", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_REASON", + "T_BIZDATE" ], - "out": [] + "out": [ + "T_RC", + "T_ARG6", + "T_COUNT", + "T_ARG1" + ] }, { "mod": "ac", @@ -1574,14 +2357,18 @@ const SVC_SCREENS = [ "svc": "ACQ_RESERVE", "desc": "매입 예약: 활성(ACTIVE) 가맹점에 대해 미래 영업일 예약 매입(status P)을", "in": [ - "T_AMOUNT", "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ "T_RC", + "T_ARG6", "T_PURCHASE_ID", - "T_FEE" + "T_FEE", + "T_NET", + "T_COUNT", + "T_ARG1" ] }, { @@ -1590,25 +2377,42 @@ const SVC_SCREENS = [ "svc": "ACQ_RESERVECXL", "desc": "매입 예약취소: 아직 접수 전환되지 않은 예약(P) 건만 물리 삭제한다.", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_REASON", + "T_BIZDATE" ], "out": [ "T_RC", - "T_AMOUNT" + "T_ARG6", + "T_AMOUNT", + "T_ARG1", + "T_COUNT", + "T_GROSS", + "T_ARG2", + "T_ID1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_RESERVEQRY", - "desc": "매입 예약 조회: 가맹점의 대기 중 예약(P)을 예약일 순 커서로 훑어 건수와", + "desc": "매입 예약 조회: 가맹점의 대기 중 예약(P)을 집계해 건수와 총액,", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_GROSS", - "T_RC" + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_AMT1", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -1623,8 +2427,10 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG6", "T_CANCEL_ID", - "T_AMOUNT" + "T_AMOUNT", + "T_ARG1" ] }, { @@ -1633,27 +2439,49 @@ const SVC_SCREENS = [ "svc": "ACQ_RETURNQRY", "desc": "매입 반품 조회: 가맹점의 영업일 반품/교환(RETURN·EXCHANGE) 취소 이력을", "in": [ + "T_MERCHANT", "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_GROSS", - "T_RC" + "T_CANCEL_ID", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_AMT1", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_RETURNSTAT", - "desc": "매입 반품 통계: 영업일 취소 원장을 반품/교환/환불 유형별로 한 번에 분해", + "desc": "매입 반품 통계: 영업일 취소 원장을 반품/교환/환불 유형별로 분해 집계해", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_ARG1", "T_ARG2", "T_ARG3", - "T_GROSS" + "T_AMT1", + "T_AMT2", + "T_AMT3", + "T_ARG4", + "T_ARG5", + "T_COUNT", + "T_GROSS", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -1662,13 +2490,22 @@ const SVC_SCREENS = [ "svc": "ACQ_SETTLEDUE", "desc": "정산예정 조회: 대사완료(M) 상태이면서 아직 정산행이 만들어지지 않은", "in": [ - "T_MERCHANT", "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_NET", - "T_RC" + "T_GROSS", + "T_ARG1", + "T_AMT1", + "T_ARG2", + "T_ARG3", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -1681,40 +2518,61 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_NET", - "T_RC" + "T_FEE", + "T_GROSS", + "T_AMT1", + "T_ARG1", + "T_ARG2", + "T_AMT2", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_SETTLELINK", - "desc": "정산 연계: 가맹점 미지정 시 매입에서 조회, settlement 링크 행(status='LINKED') 생성.", + "desc": "정산 연계: 대사완료 매입에 대해 settlement 링크 행(status='LINKED')을", "in": [ "T_PURCHASE_ID", - "T_NET", - "T_MERCHANT", "T_BIZDATE" ], "out": [ - "T_SETTLE_ID" + "T_RC", + "T_ARG6", + "T_SETTLE_ID", + "T_NET", + "T_ARG1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_SETTLEPRE", - "desc": "매입-정산 예정대사: 가맹점의 영업일 대사완료(M) 매입 순정산 예정액과 이미", + "desc": "매입-정산 예정대사: 가맹점의 영업일 대사완료(M) 매입 순정산 예정액과", "in": [ "T_MERCHANT", "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_COUNT", "T_NET", "T_AMT1", - "T_DELTA", - "T_RC" + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_GROSS", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -1724,41 +2582,67 @@ const SVC_SCREENS = [ "desc": "매입 분할납부 등록: 매입금액을 요청 회차수로 균등 분할(잔액은 마지막", "in": [ "T_PURCHASE_ID", - "T_INSTALL_N", + "T_ARG1", "T_BIZDATE" ], "out": [ "T_RC", + "T_ARG6", "T_COUNT", "T_AMT1", - "T_AMT2" + "T_GROSS", + "T_ARG2", + "T_ARG3" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_SPLITQRY", - "desc": "분할납부 회차 조회: 매입 건의 회차 스케줄을 커서로 순회하며 회차수·합계와", + "desc": "분할납부 회차 조회: 매입 건의 회차 스케줄을 집계해 회차수·합계와 최소/최대", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_GROSS", "T_AMT1", "T_AMT2", - "T_RC" + "T_AMOUNT", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_STATUS", - "desc": "* ACQ_STATUS.pgc - ac 모듈 서비스 ACQ_STATUS (one service = one file).", + "desc": "매입 상태 조회: 매입 ID 로 현재 상태를 읽어 돌려주고, 상태에 따른 진행 단계와", "in": [ "T_PURCHASE_ID" ], - "out": [] + "out": [ + "T_RC", + "T_AMOUNT", + "T_FEE", + "T_NET", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT1" + ] }, { "mod": "ac", @@ -1766,42 +2650,57 @@ const SVC_SCREENS = [ "svc": "ACQ_TAXCXL", "desc": "세금계산서 취소: 원본 계산서를 상쇄하는 음수(공급가/부가세 부호 반전)", "in": [ - "T_ID1" + "T_PURCHASE_ID", + "T_BIZDATE" ], "out": [ "T_RC", - "T_ID2", - "T_TAX" + "T_ARG6", + "T_ID1", + "T_GROSS", + "T_TAX", + "T_COUNT", + "T_ARG1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_TAXINV", - "desc": "* ACQ_TAXINV.pgc - ac 모듈 서비스 ACQ_TAXINV (one service = one file).", + "desc": "세금계산서 발행: 매입 공급가에 대한 부가세를 산출해 ac_tax_invoice 에", "in": [ "T_PURCHASE_ID", - "T_AMOUNT", - "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT" ], "out": [ - "T_TAX" + "T_RC", + "T_ARG6", + "T_ID1", + "T_TAX", + "T_GROSS", + "T_ARG1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_TAXMODIFY", - "desc": "세금계산서 수정발행: 기 발행된 계산서(ac_tax_invoice)의 공급가·부가세를", + "desc": "세금계산서 수정발행: 기 발행된 계산서의 공급가·부가세를 정정 금액으로", "in": [ - "T_ID1", - "T_GROSS" + "T_PURCHASE_ID", + "T_AMOUNT", + "T_BIZDATE" ], "out": [ "T_RC", + "T_ARG6", + "T_ID1", "T_GROSS", - "T_TAX" + "T_TAX", + "T_DELTA", + "T_ARG1", + "T_AMT1" ] }, { @@ -1810,13 +2709,22 @@ const SVC_SCREENS = [ "svc": "ACQ_TAXQRY", "desc": "세금계산서 조회: 한 매입에 발행된 계산서의 공급가·부가세 누계와 발행", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_COUNT", "T_GROSS", "T_TAX", - "T_COUNT", - "T_RC" + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { @@ -1825,13 +2733,17 @@ const SVC_SCREENS = [ "svc": "ACQ_TAXREISSUE", "desc": "세금계산서 재발행: 원본 계산서의 공급가/부가세를 승계한 신규 계산서를", "in": [ - "T_ID1", + "T_PURCHASE_ID", "T_BIZDATE" ], "out": [ "T_RC", + "T_ARG6", + "T_ID1", "T_ID2", - "T_TAX" + "T_GROSS", + "T_TAX", + "T_ARG1" ] }, { @@ -1840,39 +2752,60 @@ const SVC_SCREENS = [ "svc": "ACQ_TAXREVERSE", "desc": "세금계산서 역발행: 매출 취소분에 대한 (-)공급가 계산서를 새로 발행한다.", "in": [ - "T_ID1", + "T_PURCHASE_ID", "T_BIZDATE" ], "out": [ "T_RC", - "T_ID2" + "T_ARG6", + "T_ID1", + "T_ID2", + "T_GROSS", + "T_TAX", + "T_ARG1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_TOPMERCH", - "desc": "상위 가맹점 조회: 영업일 매출 상위 N개 가맹점을 커서로 뽑아 가맹점 ID와", + "desc": "상위 가맹점 조회: 영업일 매출 1위 가맹점과 매출액, 상위 3사 합계를", "in": [ - "T_ARG1", "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_AMT1", + "T_ARG1", + "T_AMT2", + "T_ARG2", + "T_ARG3", + "T_ARG4", "T_COUNT", "T_GROSS", - "T_RC" + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_UNMATCH", - "desc": "* ACQ_UNMATCH.pgc - ac 모듈 서비스 ACQ_UNMATCH (one service = one file).", + "desc": "미매입 등록: 대사에서 짝을 찾지 못한 매입을 미매입 원장에 사유와 함께", "in": [ + "T_PURCHASE_ID", + "T_REASON", "T_BIZDATE" ], "out": [ - "T_COUNT" + "T_RC", + "T_ARG6", + "T_ID1", + "T_COUNT", + "T_ARG1", + "T_AMOUNT" ] }, { @@ -1881,27 +2814,42 @@ const SVC_SCREENS = [ "svc": "ACQ_VELOCITY", "desc": "매입 유입속도 점검: 가맹점의 최근 10분간 접수 건수를 재어 허용치 초과", "in": [ - "T_ARG2", - "T_MERCHANT" + "T_MERCHANT", + "T_ARG1", + "T_BIZDATE" ], "out": [ - "T_ARG1", + "T_RC", + "T_ARG6", + "T_COUNT", + "T_ARG2", "T_ARG3", - "T_RC" + "T_ARG4", + "T_AMT1", + "T_AMT2", + "T_GROSS", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_VOID", - "desc": "* ACQ_VOID.pgc - ac 모듈 서비스 ACQ_VOID (one service = one file).", + "desc": "매입 무효(void): 당일 접수분에 한해 매입을 무효(V)로 전이한다.", "in": [ "T_PURCHASE_ID", - "T_AMOUNT", + "T_REASON", "T_BIZDATE" ], "out": [ - "T_CANCEL_ID" + "T_RC", + "T_ARG6", + "T_AMOUNT", + "T_DELTA", + "T_ARG1", + "T_ID1" ] }, { @@ -1913,36 +2861,59 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_GROSS", "T_FEE", - "T_AMT1" + "T_NET", + "T_AMT1", + "T_AMT2", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG7", + "T_ARG8", + "T_ID2" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_WHT", - "desc": "* ACQ_WHT.pgc - ac 모듈 서비스 ACQ_WHT (one service = one file).", + "desc": "원천징수: 가맹점 수수료에 대한 원천세를 운영 파라미터(WHT_BPS) 기준으로", "in": [ "T_PURCHASE_ID", "T_BIZDATE" ], "out": [ - "T_TAX" + "T_RC", + "T_ARG6", + "T_TAX", + "T_ID1", + "T_ARG2", + "T_FEE", + "T_NET", + "T_ARG1" ] }, { "mod": "ac", "modnm": "매입", "svc": "ACQ_XFERLINK", - "desc": "* ACQ_XFERLINK.pgc - ac 모듈 서비스 ACQ_XFERLINK (one service = one file).", + "desc": "이체 연계: 정산 완료 매입의 지급 이체를 원장에 XFER 분개로 연결한다.", "in": [ "T_PURCHASE_ID", - "T_AMOUNT", "T_BIZDATE" ], - "out": [] + "out": [ + "T_RC", + "T_ARG6", + "T_ID1", + "T_NET", + "T_AMOUNT", + "T_ARG1" + ] }, { "mod": "au", @@ -1950,14 +2921,17 @@ const SVC_SCREENS = [ "svc": "APPROVAL_RATE", "desc": "가맹점 승인율 조회: 영업일 접수 대비 승인 성공(A/M/S) 비율을 bps 로", "in": [ + "T_KEY1", + "T_AMOUNT", "T_MERCHANT", "T_BIZDATE" ], "out": [ + "T_RC", + "T_FEE", "T_ARG1", "T_COUNT", - "T_ARG2", - "T_RC" + "T_ARG2" ] }, { @@ -1968,12 +2942,13 @@ const SVC_SCREENS = [ "in": [ "T_KEY1", "T_MERCHANT", - "T_AMOUNT", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT" ], "out": [ - "T_ID1", - "T_RC" + "T_RC", + "T_FEE", + "T_ID1" ] }, { @@ -1982,12 +2957,18 @@ const SVC_SCREENS = [ "svc": "AUTH_AMT_ADJUST", "desc": "승인금액 조정(incremental/decremental auth): 호텔·주유소처럼 최종 금액이", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_ID1", - "T_DELTA" + "T_DELTA", + "T_ID2" ], "out": [ - "T_AMOUNT", - "T_RC" + "T_RC", + "T_FEE", + "T_AMOUNT" ] }, { @@ -1996,9 +2977,17 @@ const SVC_SCREENS = [ "svc": "AUTH_APPROVE", "desc": "승인 확정(pending->approved): dbio 래퍼로 상태만 A 로 확정.", "in": [ - "T_ID1" + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", + "T_ID1", + "T_ID2" ], - "out": [] + "out": [ + "T_RC", + "T_FEE" + ] }, { "mod": "au", @@ -2006,13 +2995,17 @@ const SVC_SCREENS = [ "svc": "AUTH_CANCEL", "desc": "승인 전체 취소 + 한도 원복.", "in": [ - "T_ID1", - "T_AMOUNT", + "T_MERCHANT", "T_KEY1", "T_BIZDATE", - "T_REASON" + "T_REASON", + "T_ID1", + "T_AMOUNT", + "T_ID2" ], "out": [ + "T_RC", + "T_FEE", "T_CANCEL_ID" ] }, @@ -2022,10 +3015,16 @@ const SVC_SCREENS = [ "svc": "AUTH_CAPTURE", "desc": "승인 매입확정(capture): 상태 A->M(매입대상).", "in": [ - "T_ID1" + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", + "T_ID1", + "T_ID2" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -2034,10 +3033,18 @@ const SVC_SCREENS = [ "svc": "AUTH_DECLINE", "desc": "승인 거절 처리: 거절 사유와 함께 상태 D 로 갱신.", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", + "T_REASON", "T_ID1", - "T_REASON" + "T_ID2" ], - "out": [] + "out": [ + "T_RC", + "T_FEE" + ] }, { "mod": "au", @@ -2045,14 +3052,18 @@ const SVC_SCREENS = [ "svc": "AUTH_DELAY_MON", "desc": "승인 지연 모니터: 승인(A) 상태로 기준 시간(분)을 넘겨 매입확정이 안 된", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE", "T_ARG1" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", "T_ARG2", - "T_ARG3", - "T_RC" + "T_ARG3" ] }, { @@ -2061,14 +3072,16 @@ const SVC_SCREENS = [ "svc": "AUTH_DUP_CHK", "desc": "중복승인 검출: 단말 재시도 등으로 동일 카드+가맹점+금액 조합이 직전 5분내", "in": [ + "T_BIZDATE", "T_KEY1", "T_MERCHANT", "T_AMOUNT" ], "out": [ + "T_RC", + "T_FEE", "T_ARG1", - "T_ID1", - "T_RC" + "T_ID1" ] }, { @@ -2077,12 +3090,17 @@ const SVC_SCREENS = [ "svc": "AUTH_EXPIRE_CHK", "desc": "미매입 승인 만기 점검: 기준일수를 넘겨도 매입확정되지 않은 승인(A) 건의", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_ARG1" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", "T_GROSS", - "T_RC", "T_ID1" ] }, @@ -2092,9 +3110,14 @@ const SVC_SCREENS = [ "svc": "AUTH_HISTORY", "desc": "카드 승인 이력 건수 조회.", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT" ] }, @@ -2104,10 +3127,16 @@ const SVC_SCREENS = [ "svc": "AUTH_HOLD", "desc": "승인 보류: 승인/예비(A,P) 상태만 보류(H)로 전이, 반영 건수를 T_RC 로.", "in": [ - "T_ID1" + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", + "T_ID1", + "T_ID2" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -2122,8 +3151,9 @@ const SVC_SCREENS = [ "T_AMOUNT" ], "out": [ - "T_AMT1", "T_RC", + "T_FEE", + "T_AMT1", "T_ID2" ] }, @@ -2133,12 +3163,16 @@ const SVC_SCREENS = [ "svc": "AUTH_PARTCXL", "desc": "부분 취소: 승인 금액 감액 + 한도 부분 원복.", "in": [ + "T_MERCHANT", + "T_KEY1", + "T_BIZDATE", "T_ID1", "T_AMOUNT", - "T_KEY1", - "T_BIZDATE" + "T_ID2" ], "out": [ + "T_RC", + "T_FEE", "T_CANCEL_ID", "T_AMOUNT" ] @@ -2155,9 +3189,10 @@ const SVC_SCREENS = [ "T_AMOUNT" ], "out": [ + "T_RC", + "T_FEE", "T_ID1", - "T_AMT1", - "T_RC" + "T_AMT1" ] }, { @@ -2166,10 +3201,16 @@ const SVC_SCREENS = [ "svc": "AUTH_RELEASE", "desc": "보류 해제: 직전 상태를 조회한 뒤 H->A 전이, 이전 상태를 함께 회신.", "in": [ - "T_ID1" + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", + "T_ID1", + "T_ID2" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -2178,10 +3219,15 @@ const SVC_SCREENS = [ "svc": "AUTH_REPROC_REQ", "desc": "승인 재처리 요청: 거절(D)/보류(H) 상태의 승인을 재처리 대상으로 다시", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_ID2" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -2190,12 +3236,16 @@ const SVC_SCREENS = [ "svc": "AUTH_REPROC_STAT", "desc": "재처리 현황: 영업일 승인 원장에서 보류(H) 대기 건수와, 갱신시각이 생성시각", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ + "T_RC", + "T_FEE", "T_ARG1", - "T_ARG2", - "T_RC" + "T_ARG2" ] }, { @@ -2204,11 +3254,17 @@ const SVC_SCREENS = [ "svc": "AUTH_RESEND", "desc": "승인 재전송: 망 단절 등으로 응답을 못 받은 단말이 원승인의 승인코드를", "in": [ - "T_ID1" + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", + "T_ID1", + "T_ID2" ], "out": [ - "T_AMOUNT", - "T_RC" + "T_RC", + "T_FEE", + "T_AMOUNT" ] }, { @@ -2217,12 +3273,16 @@ const SVC_SCREENS = [ "svc": "AUTH_RETRY", "desc": "거절 승인 재시도 (상태 D->A 재승인).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_KEY1", "T_ID1", "T_AMOUNT", - "T_KEY1" + "T_ID2" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -2231,11 +3291,17 @@ const SVC_SCREENS = [ "svc": "AUTH_REVERSE", "desc": "승인 역전(reversal) + 한도 원복.", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_KEY1", "T_ID1", "T_AMOUNT", - "T_KEY1" + "T_ID2" ], - "out": [] + "out": [ + "T_RC", + "T_FEE" + ] }, { "mod": "au", @@ -2243,9 +3309,14 @@ const SVC_SCREENS = [ "svc": "AUTH_ROUTE", "desc": "승인 라우팅(발급사 BIN 기준 라우팅 코드 산출).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_FEE", "T_ARG1" ] }, @@ -2255,9 +3326,16 @@ const SVC_SCREENS = [ "svc": "AUTH_STATUS", "desc": "승인 상태 조회 (dbio).", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_ID1" ], - "out": [] + "out": [ + "T_RC", + "T_FEE" + ] }, { "mod": "au", @@ -2269,6 +3347,16 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG1", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_FEE", + "T_TAX", "T_COUNT", "T_GROSS" ] @@ -2279,12 +3367,16 @@ const SVC_SCREENS = [ "svc": "AUTH_VOID", "desc": "당일 승인 무효화(void) + 한도 원복.", "in": [ + "T_MERCHANT", + "T_KEY1", + "T_BIZDATE", "T_ID1", "T_AMOUNT", - "T_KEY1", - "T_BIZDATE" + "T_ID2" ], "out": [ + "T_RC", + "T_FEE", "T_CANCEL_ID" ] }, @@ -2294,11 +3386,15 @@ const SVC_SCREENS = [ "svc": "BIN_ROUTE_INQ", "desc": "BIN별 라우팅 조회: 카드번호 선두 6자리(BIN)의 첫 자리로 매입 게이트웨이를", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_FEE", + "T_COUNT" ] }, { @@ -2307,12 +3403,16 @@ const SVC_SCREENS = [ "svc": "BLACKLIST_ADD", "desc": "블랙리스트 등재: 카드를 블랙리스트에 올리고(기등재 건은 사유만 갱신),", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1", "T_REASON" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_FEE", + "T_COUNT" ] }, { @@ -2321,10 +3421,14 @@ const SVC_SCREENS = [ "svc": "BLACKLIST_CHK", "desc": "블랙리스트 카드 조회.", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -2332,8 +3436,15 @@ const SVC_SCREENS = [ "modnm": "승인/한도", "svc": "BLACKLIST_COUNT", "desc": "블랙리스트 통계: 전체 등재 수, 도난(REPORTED_STOLEN/STOLEN) 등재 수,", - "in": [], + "in": [ + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT" + ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", "T_ARG1", "T_ARG2" @@ -2345,11 +3456,15 @@ const SVC_SCREENS = [ "svc": "BLACKLIST_DEL", "desc": "블랙리스트 해제: 등재 행을 삭제하고, 옵션(T_ARG1=1)에 따라 함께 정지됐던", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1", "T_ARG1" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -2358,11 +3473,15 @@ const SVC_SCREENS = [ "svc": "BLACKLIST_INQ", "desc": "블랙리스트 조회: 특정 카드의 등재 여부/사유/등재일시(문자열 포맷)를", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_FEE", + "T_COUNT" ] }, { @@ -2371,9 +3490,14 @@ const SVC_SCREENS = [ "svc": "BLACKLIST_PROMO", "desc": "블랙 승격 후보 조회: 영업일 부정 적발 점수 합이 1500 이상이면서 아직", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT" ] }, @@ -2383,10 +3507,15 @@ const SVC_SCREENS = [ "svc": "BRAND_AUTH_STAT", "desc": "브랜드별 승인 통계: 영업일 승인 원장을 카드 선두자리(브랜드)로 그룹핑해", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", + "T_FEE", "T_ARG1" ] }, @@ -2396,11 +3525,15 @@ const SVC_SCREENS = [ "svc": "BRAND_POLICY", "desc": "브랜드별 승인 정책: 카드 선두자리로 브랜드(VISA/MC/AMEX/LOCAL)를 판별하고", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_FEE", + "T_COUNT" ] }, { @@ -2409,13 +3542,18 @@ const SVC_SCREENS = [ "svc": "CANCEL_AMT_INQ", "desc": "취소가능액 조회: 원승인 금액에서 이미 취소된 누계를 빼 취소가능 잔액을", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_ID2" ], "out": [ + "T_RC", + "T_FEE", "T_AMT1", "T_AMT2", - "T_AMT3", - "T_RC" + "T_AMT3" ] }, { @@ -2424,11 +3562,16 @@ const SVC_SCREENS = [ "svc": "CANCEL_FULL", "desc": "전액 취소: 승인(A)/매입확정(M) 상태의 원승인을 전액 취소 처리한다. 취소", "in": [ - "T_ID2", - "T_REASON" + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", + "T_REASON", + "T_ID2" ], "out": [ "T_RC", + "T_FEE", "T_ID1", "T_AMT1" ] @@ -2439,10 +3582,16 @@ const SVC_SCREENS = [ "svc": "CANCEL_HISTORY", "desc": "승인건 취소 이력: 특정 원승인에 걸린 취소들을 시간순 커서로 순회하며", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_ID2" ], "out": [ + "T_RC", "T_COUNT", + "T_FEE", "T_AMT1" ] }, @@ -2452,11 +3601,17 @@ const SVC_SCREENS = [ "svc": "CANCEL_REPROC", "desc": "승인취소 재처리: au_cancel 에 접수됐지만 원승인 원장에 반영되지 못한 건을", "in": [ - "T_CANCEL_ID" + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", + "T_CANCEL_ID", + "T_ID2" ], "out": [ - "T_ID1", - "T_RC" + "T_RC", + "T_FEE", + "T_ID1" ] }, { @@ -2465,9 +3620,14 @@ const SVC_SCREENS = [ "svc": "CANCEL_STATUS", "desc": "취소 처리현황 조회: 영업일 기준 취소 접수를 유형(FULL/PARTIAL/VOID)별로", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", "T_ARG1", "T_ARG2", @@ -2481,9 +3641,14 @@ const SVC_SCREENS = [ "svc": "CANCEL_SUMMARY", "desc": "일별 취소 집계: 영업일 취소 원장을 전체/전액(FULL)/부분(PARTIAL)로 나눠", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", "T_AMT1", "T_ARG1", @@ -2496,12 +3661,16 @@ const SVC_SCREENS = [ "svc": "CARD_ACTIVATE", "desc": "카드 신규 활성화: 원장이 없으면 요청 한도(기본 100만원)로 생성하고, 이미", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1", "T_AMT1" ], "out": [ - "T_AMT1", - "T_RC" + "T_RC", + "T_FEE", + "T_AMT1" ] }, { @@ -2510,10 +3679,14 @@ const SVC_SCREENS = [ "svc": "CARD_DAILY_RPT", "desc": "카드 일일 승인 리포트: 카드+영업일 기준 상태별(승인/거절/취소류/매입) 건수,", "in": [ + "T_MERCHANT", + "T_AMOUNT", "T_KEY1", "T_BIZDATE" ], "out": [ + "T_RC", + "T_FEE", "T_ARG1", "T_ARG2", "T_ARG3", @@ -2529,12 +3702,16 @@ const SVC_SCREENS = [ "svc": "CARD_EXPIRE_CHK", "desc": "카드 휴면(만료 근사) 점검: 마지막 승인 이후 경과일을 EPOCH 로 환산해", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", - "T_ARG1", - "T_RC" + "T_ARG1" ] }, { @@ -2543,11 +3720,15 @@ const SVC_SCREENS = [ "svc": "CARD_LOST_REG", "desc": "분실 등록: 카드 원장 상태를 LOST 로 바꾸고 블랙리스트에 LOST 사유로", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1", "T_REASON" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -2556,10 +3737,14 @@ const SVC_SCREENS = [ "svc": "CARD_REACTIVATE", "desc": "카드 재활성화: 정지/동결/분실 상태의 원장을 ACTIVE 로 되돌리고, 분실·토큰", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -2568,11 +3753,15 @@ const SVC_SCREENS = [ "svc": "CARD_REISSUE", "desc": "카드 재발급 연계: 구 카드의 한도원장을 신 카드번호로 이관 생성하고", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1", "T_KEY2" ], "out": [ "T_RC", + "T_FEE", "T_AMT1" ] }, @@ -2582,10 +3771,14 @@ const SVC_SCREENS = [ "svc": "CARD_RESUME", "desc": "카드 정지 해제: 블랙리스트에 남아 있으면 해제를 거부하고,", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -2594,10 +3787,15 @@ const SVC_SCREENS = [ "svc": "CARD_STATE_HIST", "desc": "카드 승인상태 이력 분포: 카드의 전체 승인 건을 상태(status)별로 집계하는", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", "T_COUNT", + "T_FEE", "T_ARG1" ] }, @@ -2607,14 +3805,18 @@ const SVC_SCREENS = [ "svc": "CARD_STATE_INQ", "desc": "카드 상태 종합조회: 한도원장(상태/기본·임시한도/사용액)에", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_FEE", "T_AMT1", "T_AMT2", "T_AMT3", - "T_COUNT", - "T_RC" + "T_COUNT" ] }, { @@ -2623,13 +3825,16 @@ const SVC_SCREENS = [ "svc": "CARD_STOLEN_REG", "desc": "도난 등록: 원장 상태를 STOLEN 으로 전환하고 블랙리스트를 STOLEN 사유로", "in": [ + "T_MERCHANT", + "T_AMOUNT", "T_KEY1", "T_REASON", "T_BIZDATE" ], "out": [ - "T_ID1", - "T_RC" + "T_RC", + "T_FEE", + "T_ID1" ] }, { @@ -2638,12 +3843,16 @@ const SVC_SCREENS = [ "svc": "CARD_SUSPEND", "desc": "카드 정지: ACTIVE 상태의 카드만 SUSPEND 로 전환하고, 참고용으로", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1", "T_REASON" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_FEE", + "T_COUNT" ] }, { @@ -2652,10 +3861,14 @@ const SVC_SCREENS = [ "svc": "CARD_VALIDATE", "desc": "카드번호 Luhn 검증 (common lib).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -2664,12 +3877,17 @@ const SVC_SCREENS = [ "svc": "CHANNEL_POLICY", "desc": "채널별 승인 정책: 매입 원장에서 당일 해당 채널(POS/EDC/EDI/FOREIGN)의", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_CHANNEL" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", - "T_ARG1", - "T_RC" + "T_ARG1" ] }, { @@ -2678,10 +3896,15 @@ const SVC_SCREENS = [ "svc": "CHANNEL_STAT", "desc": "채널별 매입 통계: 영업일 매입을 채널별로 그룹핑하는 커서를 순회하며", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", + "T_FEE", "T_GROSS" ] }, @@ -2691,10 +3914,16 @@ const SVC_SCREENS = [ "svc": "DCC_QUOTE", "desc": "해외승인 DCC 환산 견적(통화 -> 원화).", "in": [ - "T_FX_AMT", - "T_CCY" + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", + "T_CCY", + "T_FX_AMT" ], "out": [ + "T_RC", + "T_FEE", "T_AMOUNT", "T_ARG1" ] @@ -2705,10 +3934,15 @@ const SVC_SCREENS = [ "svc": "DECLINE_ANALYZE", "desc": "승인 거절사유 분석: 영업일 거절(D) 건을 사유별로 그룹핑해 상위 3개 사유를", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", + "T_FEE", "T_ARG1" ] }, @@ -2718,10 +3952,16 @@ const SVC_SCREENS = [ "svc": "FRAUD_CASE_CLOSE", "desc": "부정 케이스 종결: 미종결 부정로그의 사유 뒤에 '/CLOSED' 를 붙여 종결 표시", "in": [ - "T_ID1" + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", + "T_ID1", + "T_ID2" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -2730,9 +3970,14 @@ const SVC_SCREENS = [ "svc": "FRAUD_CASE_INQ", "desc": "이상거래 케이스 조회: 카드의 부정거래 적발 이력을 집계(건수/최고/평균점수)", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", "T_ARG1", "T_ARG2" @@ -2744,10 +3989,15 @@ const SVC_SCREENS = [ "svc": "FRAUD_CASE_LIST", "desc": "부정 케이스 상위목록: 영업일 부정로그를 점수 내림차순 상위 5건 커서로", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ - "T_COUNT" + "T_RC", + "T_COUNT", + "T_FEE" ] }, { @@ -2756,15 +4006,17 @@ const SVC_SCREENS = [ "svc": "FRAUD_DETECT", "desc": "부정거래 스코어링 + 위험시 fraud_log 기록.", "in": [ - "T_ID1", - "T_AMOUNT", + "T_MERCHANT", "T_KEY1", - "T_BIZDATE" + "T_BIZDATE", + "T_ID1", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_FEE", "T_ARG1", - "T_ID2", - "T_RC" + "T_ID2" ] }, { @@ -2773,13 +4025,15 @@ const SVC_SCREENS = [ "svc": "FRAUD_RULE_AMT", "desc": "이상거래 고액 룰: 거래금액이 카드의 30일 평균 사용액의 3배를 넘고", "in": [ + "T_MERCHANT", "T_KEY1", "T_BIZDATE", "T_AMOUNT" ], "out": [ - "T_AMT1", "T_RC", + "T_FEE", + "T_AMT1", "T_ARG1", "T_ID1" ] @@ -2790,13 +4044,16 @@ const SVC_SCREENS = [ "svc": "FRAUD_RULE_DUP", "desc": "이상거래 동일금액 반복 룰: 당일 카드 승인 가운데 같은 금액이 3회 이상", "in": [ + "T_MERCHANT", + "T_AMOUNT", "T_KEY1", "T_BIZDATE" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", "T_AMT1", - "T_RC", "T_ID1" ] }, @@ -2806,12 +4063,15 @@ const SVC_SCREENS = [ "svc": "FRAUD_RULE_FREQ", "desc": "이상거래 다빈도 룰: 직전 10분간 승인 시도(거절 포함)가 5건 이상이면", "in": [ + "T_MERCHANT", + "T_AMOUNT", "T_KEY1", "T_BIZDATE" ], "out": [ - "T_COUNT", "T_RC", + "T_FEE", + "T_COUNT", "T_ID1" ] }, @@ -2821,13 +4081,16 @@ const SVC_SCREENS = [ "svc": "FRAUD_RULE_INTL", "desc": "고액(해외성) 이상거래 룰: 최근 1일 100만원 초과 승인이 3건 이상이면", "in": [ + "T_MERCHANT", + "T_AMOUNT", "T_KEY1", "T_BIZDATE" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", "T_GROSS", - "T_RC", "T_ID1" ] }, @@ -2837,12 +4100,15 @@ const SVC_SCREENS = [ "svc": "FRAUD_RULE_MCC", "desc": "관찰가맹 이상거래 룰: 카드의 당일 승인 중 상태가 WATCH 인 가맹점 건수를", "in": [ + "T_MERCHANT", + "T_AMOUNT", "T_KEY1", "T_BIZDATE" ], "out": [ - "T_COUNT", "T_RC", + "T_FEE", + "T_COUNT", "T_ID1" ] }, @@ -2852,12 +4118,15 @@ const SVC_SCREENS = [ "svc": "FRAUD_RULE_NIGHT", "desc": "심야 이상거래 룰: 최근 3일 심야시간대(23~06시) 승인 시도가 6건 이상이면", "in": [ + "T_MERCHANT", + "T_AMOUNT", "T_KEY1", "T_BIZDATE" ], "out": [ - "T_COUNT", "T_RC", + "T_FEE", + "T_COUNT", "T_ID1" ] }, @@ -2867,9 +4136,14 @@ const SVC_SCREENS = [ "svc": "FRAUD_SCORE_SUM", "desc": "카드 위험도 종합: 카드의 부정 적발 건수/점수합/최고점수를 집계해 최고점수", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", "T_ARG1", "T_ARG2" @@ -2883,11 +4157,13 @@ const SVC_SCREENS = [ "in": [ "T_KEY1", "T_MERCHANT", + "T_BIZDATE", "T_AMOUNT", - "T_INSTALL_N", - "T_BIZDATE" + "T_INSTALL_N" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT" ] }, @@ -2897,13 +4173,17 @@ const SVC_SCREENS = [ "svc": "KEY_ROTATE_LOG", "desc": "암호키 회전 로그: 요청 키버전(T_ARG1)의 차기 버전을 산출하고 키식별자", "in": [ - "T_ARG1", - "T_KEY1" + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", + "T_KEY1", + "T_ARG1" ], "out": [ + "T_RC", + "T_FEE", "T_ARG2", - "T_ARG3", - "T_RC" + "T_ARG3" ] }, { @@ -2912,11 +4192,15 @@ const SVC_SCREENS = [ "svc": "LIMIT_ADJUST", "desc": "한도 증감 조정: 요청 증감액(T_DELTA)을 일한도에 가감하되 음수 한도가 되지", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1", "T_DELTA" ], "out": [ "T_RC", + "T_FEE", "T_AMT1" ] }, @@ -2926,13 +4210,16 @@ const SVC_SCREENS = [ "svc": "LIMIT_CHK", "desc": "카드 한도 점검 (사용가능 여부만 리턴).", "in": [ + "T_MERCHANT", + "T_BIZDATE", "T_KEY1", "T_AMOUNT" ], "out": [ + "T_RC", + "T_FEE", "T_AMT1", - "T_AMT2", - "T_RC" + "T_AMT2" ] }, { @@ -2941,9 +4228,14 @@ const SVC_SCREENS = [ "svc": "LIMIT_CUMUL_INQ", "desc": "전(全)기간 누적 사용 조회: 카드의 전체 승인 이력에서 누적 건수/누적 합계/", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", "T_AMT1", "T_AMT2" @@ -2955,9 +4247,14 @@ const SVC_SCREENS = [ "svc": "LIMIT_DAILY_INQ", "desc": "일 한도 상세 조회: 원장에서 일한도/사용액/임시한도/상태를 한 번에 읽어", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_FEE", "T_AMT1", "T_AMT2", "T_AMT3", @@ -2970,10 +4267,15 @@ const SVC_SCREENS = [ "svc": "LIMIT_DEC", "desc": "한도 차감(사용금액 증가).", "in": [ - "T_AMOUNT", - "T_KEY1" + "T_MERCHANT", + "T_BIZDATE", + "T_KEY1", + "T_AMOUNT" ], - "out": [] + "out": [ + "T_RC", + "T_FEE" + ] }, { "mod": "au", @@ -2981,10 +4283,16 @@ const SVC_SCREENS = [ "svc": "LIMIT_EXHAUST_LST", "desc": "한도 소진 카드 목록: 요청 기준율(bps, 기본 9000=90%) 이상으로 한도를 소진한", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_ARG1" ], "out": [ + "T_RC", "T_COUNT", + "T_FEE", "T_ARG1" ] }, @@ -2994,10 +4302,14 @@ const SVC_SCREENS = [ "svc": "LIMIT_FREEZE", "desc": "한도 동결: ACTIVE 상태의 카드 원장만 FROZEN 으로 전환한다. 이미 다른", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -3006,11 +4318,16 @@ const SVC_SCREENS = [ "svc": "LIMIT_HIST", "desc": "한도 사용 이력조회: 최근 N일 일자별 승인합계를 커서로 훑어", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1", "T_ARG1" ], "out": [ + "T_RC", "T_COUNT", + "T_FEE", "T_GROSS", "T_AMT1" ] @@ -3021,12 +4338,22 @@ const SVC_SCREENS = [ "svc": "LIMIT_INQ", "desc": "한도 조회 (가용한도 계산).", "in": [ - "T_KEY1" + "T_KEY1", + "T_BIZDATE" ], "out": [ + "T_RC", "T_AMT1", "T_AMT2", - "T_AMT3" + "T_AMT3", + "T_ARG1", + "T_GROSS", + "T_COUNT", + "T_ARG2", + "T_ARG3", + "T_ARG4", + "T_ARG5", + "T_ARG6" ] }, { @@ -3035,13 +4362,16 @@ const SVC_SCREENS = [ "svc": "LIMIT_MONTH_CHK", "desc": "월 한도 점검: 이번 달 승인(A/M/S) 누적액에 요청금액을 더해 월 한도", "in": [ + "T_MERCHANT", + "T_BIZDATE", "T_KEY1", "T_AMOUNT" ], "out": [ + "T_RC", + "T_FEE", "T_AMT1", - "T_AMT2", - "T_RC" + "T_AMT2" ] }, { @@ -3050,9 +4380,14 @@ const SVC_SCREENS = [ "svc": "LIMIT_MONTH_INQ", "desc": "월 누적 사용 조회: 이번 달 승인건 기준으로 건수/합계/평균단가를 한 번의", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", "T_AMT1", "T_AMT2" @@ -3064,12 +4399,16 @@ const SVC_SCREENS = [ "svc": "LIMIT_OVER_ALERT", "desc": "한도 소진 임박 알림: 사용액/일한도를 bps 로 환산해 90% 이상이면 경보", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_FEE", "T_ARG1", - "T_AMT1", - "T_RC" + "T_AMT1" ] }, { @@ -3078,12 +4417,15 @@ const SVC_SCREENS = [ "svc": "LIMIT_PERTX_CHK", "desc": "건별(1회) 한도 점검: 일한도의 20% 를 건별 상한으로 삼아 요청 단건 금액이", "in": [ + "T_MERCHANT", + "T_BIZDATE", "T_KEY1", "T_AMOUNT" ], "out": [ - "T_AMT1", - "T_RC" + "T_RC", + "T_FEE", + "T_AMT1" ] }, { @@ -3092,12 +4434,16 @@ const SVC_SCREENS = [ "svc": "LIMIT_RAISE_REQ", "desc": "한도 영구상향 심사: 최근 30일 승인실적 합계와 거절 건수를 심사해", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1", "T_AMT1" ], "out": [ - "T_GROSS", "T_RC", + "T_FEE", + "T_GROSS", "T_AMT2" ] }, @@ -3107,12 +4453,16 @@ const SVC_SCREENS = [ "svc": "LIMIT_REMAIN", "desc": "잔여한도 조회: (기본한도 + 임시한도 - 사용액) 을 계산해 내려준다.", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_FEE", "T_AMT1", - "T_AMT2", - "T_RC" + "T_AMT2" ] }, { @@ -3121,11 +4471,15 @@ const SVC_SCREENS = [ "svc": "LIMIT_RESTORE", "desc": "임시한도 자동복원: LIMIT_TEMP 로 상향됐던 temp_limit 를 걷어내고", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ - "T_AMT1", - "T_RC" + "T_RC", + "T_FEE", + "T_AMT1" ] }, { @@ -3134,9 +4488,15 @@ const SVC_SCREENS = [ "svc": "LIMIT_RST", "desc": "한도 사용금액 초기화(일마감 원복).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], - "out": [] + "out": [ + "T_RC", + "T_FEE" + ] }, { "mod": "au", @@ -3144,10 +4504,16 @@ const SVC_SCREENS = [ "svc": "LIMIT_SET", "desc": "카드 한도 설정/등록 (dbio upsert).", "in": [ - "T_AMT1", - "T_KEY1" + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", + "T_KEY1", + "T_AMT1" ], - "out": [] + "out": [ + "T_RC", + "T_FEE" + ] }, { "mod": "au", @@ -3155,10 +4521,16 @@ const SVC_SCREENS = [ "svc": "LIMIT_TEMP", "desc": "임시 한도 상향 (temp_limit 저장 + 당일 상향).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1", "T_AMT1" ], - "out": [] + "out": [ + "T_RC", + "T_FEE" + ] }, { "mod": "au", @@ -3166,9 +4538,14 @@ const SVC_SCREENS = [ "svc": "LIMIT_TIER_INQ", "desc": "한도 등급 조회: 일한도 구간으로 BASIC/SILVER/GOLD/PLATINUM 등급을 매겨", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_FEE", "T_AMT1" ] }, @@ -3178,10 +4555,14 @@ const SVC_SCREENS = [ "svc": "LIMIT_UNFREEZE", "desc": "한도 동결 해제: FROZEN 상태의 카드 원장만 ACTIVE 로 되돌린다. 대상이", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -3190,9 +4571,14 @@ const SVC_SCREENS = [ "svc": "LIMIT_USAGE", "desc": "한도 사용률 조회: 사용액/기본한도를 bps(만분율)로 환산하고", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_FEE", "T_ARG1", "T_AMT1", "T_AMT2" @@ -3204,9 +4590,15 @@ const SVC_SCREENS = [ "svc": "MCC_AUTH_STAT", "desc": "업종별 매입 금액 통계: 영업일 매입을 업종(category)별 합계로 그룹핑하는", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ + "T_RC", + "T_COUNT", + "T_FEE", "T_GROSS" ] }, @@ -3216,12 +4608,17 @@ const SVC_SCREENS = [ "svc": "MCC_POLICY", "desc": "업종(MCC)별 승인 정책: 매입 원장의 당일 업종 실적(건수/합계)을 읽어", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_CATEGORY" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", - "T_GROSS", - "T_RC" + "T_GROSS" ] }, { @@ -3230,14 +4627,17 @@ const SVC_SCREENS = [ "svc": "MERCH_AUTH_RANK", "desc": "가맹점 승인 순위: 영업일의 가맹점별 승인금액을 rank() 윈도우로 서열화한", "in": [ + "T_KEY1", + "T_AMOUNT", "T_MERCHANT", "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", + "T_FEE", "T_ARG1", - "T_GROSS", - "T_RC" + "T_GROSS" ] }, { @@ -3246,13 +4646,16 @@ const SVC_SCREENS = [ "svc": "NIGHT_POLICY", "desc": "심야 승인정책 점검: 최근 7일간 심야시간대(23시~06시) 승인 이력을 집계해", "in": [ + "T_MERCHANT", + "T_BIZDATE", "T_KEY1", "T_AMOUNT" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", - "T_GROSS", - "T_RC" + "T_GROSS" ] }, { @@ -3261,13 +4664,16 @@ const SVC_SCREENS = [ "svc": "NOAUTH_CAPTURE", "desc": "무승인 매입 검출: 매입원장(purchase)에는 있는데 같은 가맹점/영업일/금액의", "in": [ + "T_KEY1", + "T_AMOUNT", "T_MERCHANT", "T_BIZDATE" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", - "T_GROSS", - "T_RC" + "T_GROSS" ] }, { @@ -3284,6 +4690,7 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_FEE", "T_ID1" ] }, @@ -3299,6 +4706,8 @@ const SVC_SCREENS = [ "T_AMOUNT" ], "out": [ + "T_RC", + "T_FEE", "T_ID2" ] }, @@ -3308,10 +4717,15 @@ const SVC_SCREENS = [ "svc": "PREAUTH_CANCEL", "desc": "선승인 취소: 미확정(P) 선승인을 X 로 전환해 홀드 금액을 해제한다.", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_ID2" ], "out": [ "T_RC", + "T_FEE", "T_AMT1" ] }, @@ -3321,10 +4735,15 @@ const SVC_SCREENS = [ "svc": "PREAUTH_COMPLETE", "desc": "예비승인 확정: preauth 소진 + 본승인 생성.", "in": [ - "T_ID2", - "T_BIZDATE" + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", + "T_BIZDATE", + "T_ID2" ], "out": [ + "T_RC", + "T_FEE", "T_ID1" ] }, @@ -3334,11 +4753,16 @@ const SVC_SCREENS = [ "svc": "PREAUTH_EXTEND", "desc": "선승인 연장: 아직 확정되지 않은(P) 선승인의 기준일(biz_date)을 지정 일수만큼", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_ID2", "T_ARG1" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -3347,14 +4771,19 @@ const SVC_SCREENS = [ "svc": "PREAUTH_INQ", "desc": "선승인 조회: 카드의 선승인 현황을 상태(P/C/X)별 FILTER 집계로 나눠 주고", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_COUNT", + "T_FEE", "T_ARG1", "T_ARG2", "T_ARG3", - "T_AMT1", - "T_RC" + "T_AMT1" ] }, { @@ -3363,11 +4792,15 @@ const SVC_SCREENS = [ "svc": "PREAUTH_PARTUSE", "desc": "사전승인 부분 사용: 예비승인(P) 잔액에서 사용액을 차감(부분 소진)한다.", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_BIZDATE", "T_ID2", "T_AMOUNT" ], "out": [ "T_RC", + "T_FEE", "T_AMT1" ] }, @@ -3377,9 +4810,14 @@ const SVC_SCREENS = [ "svc": "RISK_SCORE_INQ", "desc": "카드 리스크 점수 산출: 부정거래 적발 이력(평균점수/건수), 30일 거절률,", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_FEE", "T_ARG1", "T_ARG2", "T_ARG3" @@ -3393,11 +4831,12 @@ const SVC_SCREENS = [ "in": [ "T_KEY1", "T_MERCHANT", - "T_AMOUNT", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT" ], "out": [ "T_RC", + "T_FEE", "T_ID1" ] }, @@ -3407,14 +4846,19 @@ const SVC_SCREENS = [ "svc": "STANDIN_INQ", "desc": "대행승인 현황 조회: 영업일의 STANDIN 승인 건을 미정산(A)/정산완료(S)로", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ + "T_RC", + "T_COUNT", + "T_FEE", "T_ARG1", "T_ARG2", "T_AMT1", - "T_AMT2", - "T_RC" + "T_AMT2" ] }, { @@ -3423,13 +4867,16 @@ const SVC_SCREENS = [ "svc": "STANDIN_LIMIT", "desc": "대행 한도 점검: 발급사 미응답 시 대행 승인은 일한도의 절반을 상한으로 두어,", "in": [ + "T_MERCHANT", + "T_BIZDATE", "T_KEY1", "T_AMOUNT" ], "out": [ + "T_RC", + "T_FEE", "T_AMT1", - "T_AMT2", - "T_RC" + "T_AMT2" ] }, { @@ -3438,13 +4885,17 @@ const SVC_SCREENS = [ "svc": "STANDIN_RECON", "desc": "대행 정산 대사: 영업일 대행 승인 중 매입확정(captured_at NOT NULL) 건과", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", + "T_FEE", "T_ARG1", - "T_ARG2", - "T_RC" + "T_ARG2" ] }, { @@ -3453,12 +4904,16 @@ const SVC_SCREENS = [ "svc": "STANDIN_SETTLE", "desc": "대행승인(stand-in) 정산: 이슈어 복구 후, 대행으로 승인(A)해 둔 STANDIN", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", - "T_GROSS", - "T_RC" + "T_GROSS" ] }, { @@ -3467,10 +4922,15 @@ const SVC_SCREENS = [ "svc": "STANDIN_STAT", "desc": "대행(stand-in) 승인 통계: 영업일 auth_type='STANDIN' 승인을 대상으로", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", + "T_FEE", "T_GROSS", "T_ARG1" ] @@ -3481,10 +4941,15 @@ const SVC_SCREENS = [ "svc": "STAT_AMT_BAND", "desc": "금액대별 승인 통계: 영업일 승인을 1만/5만/10만/50만/그이상 구간(CASE)으로", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", + "T_FEE", "T_ARG1" ] }, @@ -3494,9 +4959,14 @@ const SVC_SCREENS = [ "svc": "STAT_APPROVE_TR", "desc": "카드 승인 추세: 최근 7일과 그 직전 7일의 승인 건수를 각각 필터 집계로 뽑아", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_FEE", "T_ARG1", "T_ARG2", "T_DELTA" @@ -3508,16 +4978,21 @@ const SVC_SCREENS = [ "svc": "STAT_BYTYPE", "desc": "승인유형(채널)별 통계: 영업일 승인 접수를 auth_type", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ + "T_RC", + "T_COUNT", + "T_FEE", "T_ARG1", "T_ARG2", "T_ARG3", "T_ARG4", "T_ARG5", - "T_ARG6", - "T_COUNT" + "T_ARG6" ] }, { @@ -3526,9 +5001,14 @@ const SVC_SCREENS = [ "svc": "STAT_DAILY_SUM", "desc": "일별 승인 종합: 영업일 승인 원장의 접수/승인/거절/취소 건수와 승인 합계를", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", "T_ARG1", "T_ARG2", @@ -3542,10 +5022,15 @@ const SVC_SCREENS = [ "svc": "STAT_DECLINE", "desc": "거절사유별 통계: 영업일 거절(D) 건을 사유별로 그룹핑하는 커서를 순회하며", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", + "T_FEE", "T_ARG1" ] }, @@ -3555,11 +5040,16 @@ const SVC_SCREENS = [ "svc": "STAT_HOURLY", "desc": "시간대별 승인 통계: 영업일의 승인 접수를 시(HH24) 단위로 그룹핑한 커서를", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ - "T_ARG1", + "T_RC", "T_COUNT", + "T_FEE", + "T_ARG1", "T_ARG2" ] }, @@ -3569,11 +5059,16 @@ const SVC_SCREENS = [ "svc": "STAT_ISSUER", "desc": "발급사(카드망)별 승인 통계: 카드번호 선두 자리로 발급사망을 판별", "in": [ + "T_KEY1", + "T_MERCHANT", + "T_AMOUNT", "T_BIZDATE" ], "out": [ - "T_ARG1", - "T_COUNT" + "T_RC", + "T_COUNT", + "T_FEE", + "T_ARG1" ] }, { @@ -3582,10 +5077,14 @@ const SVC_SCREENS = [ "svc": "TOKEN_INQ", "desc": "토큰 상태 조회: 원장 상태와 블랙리스트 등재 여부를 합쳐 토큰의 종합 상태", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -3594,10 +5093,14 @@ const SVC_SCREENS = [ "svc": "TOKEN_ISSUE", "desc": "결제 토큰 발급: PAN 의 Luhn 검증 후 앞6·뒤4 자리만 남기고 가운데를 마스킹한", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -3606,10 +5109,14 @@ const SVC_SCREENS = [ "svc": "TOKEN_REVOKE", "desc": "토큰 폐기: 카드(토큰 원본 PAN)를 TOKEN_REVOKED 사유로 블랙리스트에 등재해", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -3618,10 +5125,14 @@ const SVC_SCREENS = [ "svc": "TOKEN_VERIFY", "desc": "토큰 검증: 결제 토큰(PAN)을 4단계로 검증해 비트마스크 RC 로 응답한다.", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -3630,12 +5141,15 @@ const SVC_SCREENS = [ "svc": "VELOCITY_AMT", "desc": "금액 velocity 룰: 직전 1시간 승인 합계에 요청금액을 더해 200만원을 넘으면", "in": [ + "T_MERCHANT", + "T_BIZDATE", "T_KEY1", "T_AMOUNT" ], "out": [ - "T_GROSS", - "T_RC" + "T_RC", + "T_FEE", + "T_GROSS" ] }, { @@ -3644,9 +5158,14 @@ const SVC_SCREENS = [ "svc": "VELOCITY_CHAN", "desc": "승인유형 velocity 룰: 직전 1시간 카드 승인을 승인유형(auth_type)별로", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_FEE", "T_ARG1", "T_COUNT" ] @@ -3657,13 +5176,16 @@ const SVC_SCREENS = [ "svc": "VELOCITY_CHK", "desc": "속도(velocity) 점검: 당일 카드 승인건수/합계.", "in": [ + "T_MERCHANT", + "T_AMOUNT", "T_KEY1", "T_BIZDATE" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", - "T_GROSS", - "T_RC" + "T_GROSS" ] }, { @@ -3672,12 +5194,16 @@ const SVC_SCREENS = [ "svc": "VELOCITY_DAY", "desc": "일 velocity 룰: 당일 카드 승인 건수와 합계를 세어 20건 초과 또는 합계", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", - "T_GROSS", - "T_RC" + "T_GROSS" ] }, { @@ -3686,12 +5212,16 @@ const SVC_SCREENS = [ "svc": "VELOCITY_HOUR", "desc": "시간 단위 velocity 점검: 직전 60분 슬라이딩 윈도우의 카드 승인", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", - "T_GROSS", - "T_RC" + "T_GROSS" ] }, { @@ -3700,11 +5230,15 @@ const SVC_SCREENS = [ "svc": "VELOCITY_MCC", "desc": "가맹점 분산 velocity 룰: 직전 1시간 동안 서로 다른 가맹점 수(카드가 돌린", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_FEE", + "T_COUNT" ] }, { @@ -3713,12 +5247,16 @@ const SVC_SCREENS = [ "svc": "VELOCITY_MERCH", "desc": "가맹점 velocity 점검: 직전 10분간 가맹점 승인 요청 폭주를 감시한다.", "in": [ + "T_KEY1", + "T_BIZDATE", + "T_AMOUNT", "T_MERCHANT" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", - "T_ARG1", - "T_RC" + "T_ARG1" ] }, { @@ -3727,12 +5265,15 @@ const SVC_SCREENS = [ "svc": "VELOCITY_RESET", "desc": "velocity 적발 리셋: 특정 카드의 당일 velocity 계열 부정 적발(reason 접두", "in": [ + "T_MERCHANT", + "T_AMOUNT", "T_KEY1", "T_BIZDATE" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_FEE", + "T_COUNT" ] }, { @@ -3741,13 +5282,17 @@ const SVC_SCREENS = [ "svc": "VELOCITY_WEEK", "desc": "주간 velocity 점검: 최근 7일 카드 승인 건수/금액에 더해 방문 가맹점의", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_FEE", "T_COUNT", "T_GROSS", - "T_ARG1", - "T_RC" + "T_ARG1" ] }, { @@ -3756,10 +5301,14 @@ const SVC_SCREENS = [ "svc": "WHITELIST_ADD", "desc": "화이트리스트 등록: 카드의 블랙리스트 등재를 삭제하고 원장 상태가 ACTIVE 가", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -3768,11 +5317,15 @@ const SVC_SCREENS = [ "svc": "WHITELIST_CHK", "desc": "화이트 여부 판정: 블랙리스트 미등재이면서 원장이 존재하는 카드를 화이트", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1" ], "out": [ - "T_ARG1", - "T_RC" + "T_RC", + "T_FEE", + "T_ARG1" ] }, { @@ -3781,11 +5334,15 @@ const SVC_SCREENS = [ "svc": "WHITELIST_DEL", "desc": "화이트리스트 해제: 카드를 다시 블랙리스트에 (사유와 함께) 등재/갱신해", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_AMOUNT", "T_KEY1", "T_REASON" ], "out": [ - "T_RC" + "T_RC", + "T_FEE" ] }, { @@ -3794,10 +5351,17 @@ const SVC_SCREENS = [ "svc": "CL_AGGREGATE", "desc": "마감 총계 재집계 후 close_log 반영.", "in": [ - "T_ID1", - "T_BIZDATE" + "T_BIZDATE", + "T_STR1", + "T_KEY1", + "T_ID1" ], "out": [ + "T_RC", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", "T_GROSS" ] @@ -3811,8 +5375,12 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_RC" + "T_GROSS" ] }, { @@ -3824,11 +5392,16 @@ const SVC_SCREENS = [ "T_ID1" ], "out": [ + "T_RC", "T_AMT1", "T_AMT2", "T_AMT3", "T_DELTA", - "T_RC" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -3841,8 +5414,12 @@ const SVC_SCREENS = [ "T_STR1" ], "out": [ - "T_ID1", - "T_RC" + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -3851,11 +5428,16 @@ const SVC_SCREENS = [ "svc": "CL_APPRFINAL", "desc": "최종 마감 승인: 2차 승인(APPR:L2) 마감을 최종 승인(status=A)으로 확정.", "in": [ - "T_ID1" + "T_ID1", + "T_STR1" ], "out": [ - "T_ID1", - "T_ARG1" + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -3864,13 +5446,17 @@ const SVC_SCREENS = [ "svc": "CL_APPRHIST", "desc": "마감 승인 이력: 기간 프리픽스(YYYY-MM 등)의 마감을 승인(A/L)/완료(C)/취소(X)", "in": [ + "T_KEY1", "T_STR1" ], "out": [ - "T_ARG1", - "T_ARG2", + "T_RC", + "T_COUNT", "T_ARG3", - "T_COUNT" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_GROSS" ] }, { @@ -3879,11 +5465,16 @@ const SVC_SCREENS = [ "svc": "CL_APPRL1", "desc": "1차 마감 승인: 개시(O)/완료(C) 마감을 1차 승인(memo=APPR:L1) 단계로 전이.", "in": [ - "T_ID1" + "T_ID1", + "T_STR1" ], "out": [ - "T_ID1", - "T_ARG1" + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -3892,11 +5483,16 @@ const SVC_SCREENS = [ "svc": "CL_APPRL2", "desc": "2차 마감 승인: 1차 승인(APPR:L1) 마감을 2차 승인(APPR:L2)으로 전이.", "in": [ - "T_ID1" + "T_ID1", + "T_STR1" ], "out": [ - "T_ID1", - "T_ARG1" + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -3905,9 +5501,17 @@ const SVC_SCREENS = [ "svc": "CL_APPROVE", "desc": "마감 승인: 상태 A.", "in": [ - "T_ID1" + "T_ID1", + "T_STR1" ], - "out": [] + "out": [ + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" + ] }, { "mod": "cl", @@ -3916,10 +5520,15 @@ const SVC_SCREENS = [ "desc": "마감 승인 반려: 승인(A) 상태 마감을 완료(C)로 되돌리며 반려 사유를 기록.", "in": [ "T_ID1", - "T_REASON" + "T_STR1" ], "out": [ - "T_RC" + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -3928,11 +5537,16 @@ const SVC_SCREENS = [ "svc": "CL_APPRREQ", "desc": "마감 승인 요청: C 상태이며 집계건수>0 인 마감에 승인요청(APPR_REQ) 표시를", "in": [ - "T_ID1" + "T_ID1", + "T_STR1" ], "out": [ - "T_ARG1", - "T_RC" + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -3942,10 +5556,13 @@ const SVC_SCREENS = [ "desc": "마감 승인 현황: 승인 단계별(L1/L2/최종A) 마감 건수를 집계한다.", "in": [], "out": [ - "T_ARG1", - "T_ARG2", + "T_RC", + "T_COUNT", "T_ARG3", - "T_COUNT" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_GROSS" ] }, { @@ -3954,10 +5571,16 @@ const SVC_SCREENS = [ "svc": "CL_APPRWD", "desc": "마감 승인 회수: 승인(A) 마감을 완료(C) 상태로 되돌리고 승인 이력 초기화.", "in": [ - "T_ID1" + "T_ID1", + "T_STR1" ], "out": [ - "T_ID1" + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -3966,10 +5589,19 @@ const SVC_SCREENS = [ "svc": "CL_ARCHIVE", "desc": "마감 아카이브: 승인/완료 마감을 보관 상태로(memo 표시).", "in": [ + "T_BIZDATE", + "T_STR1", + "T_KEY1", "T_ID1" ], "out": [ - "T_COUNT" + "T_RC", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -3978,11 +5610,21 @@ const SVC_SCREENS = [ "svc": "CL_AUDITTRAIL", "desc": "마감 이력 감사: 마감건의 생성-수정 시각 간격(초)을 산출하고, 잠금(L)", "in": [ + "T_BIZDATE", + "T_STR1", + "T_KEY1", "T_ID1" ], "out": [ + "T_RC", "T_ARG1", - "T_RC" + "T_ARG2", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -3991,12 +5633,21 @@ const SVC_SCREENS = [ "svc": "CL_BALVERIFY", "desc": "잔액 검증: 스냅샷 net 합 vs close_log total 비교.", "in": [ - "T_ID1" + "T_ID1", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG1", + "T_ARG2", + "T_AMT1", "T_AMT2", - "T_AMT3", - "T_DELTA" + "T_DELTA", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4005,10 +5656,19 @@ const SVC_SCREENS = [ "svc": "CL_CALENDAR", "desc": "마감 캘린더: 월(YYYY-MM)의 1~31일 각 일자 마감 상태를 한 글자씩 표현한", "in": [ - "T_STR1" + "T_BIZDATE", + "T_STR1", + "T_KEY1", + "T_ID1" ], "out": [ - "T_COUNT" + "T_RC", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4017,9 +5677,18 @@ const SVC_SCREENS = [ "svc": "CL_CANCEL", "desc": "마감 취소: 상태 X.", "in": [ - "T_ID1" + "T_ID1", + "T_REASON", + "T_BIZDATE" ], - "out": [] + "out": [ + "T_RC", + "T_COUNT", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_GROSS" + ] }, { "mod": "cl", @@ -4030,12 +5699,12 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_ARG1", - "T_ARG2", - "T_ARG3", - "T_ARG4", + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_RC" + "T_GROSS" ] }, { @@ -4047,8 +5716,12 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_RC" + "T_GROSS" ] }, { @@ -4060,9 +5733,12 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_GROSS", - "T_RC" + "T_GROSS" ] }, { @@ -4074,8 +5750,12 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_RC" + "T_GROSS" ] }, { @@ -4087,8 +5767,12 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_RC" + "T_GROSS" ] }, { @@ -4098,10 +5782,17 @@ const SVC_SCREENS = [ "desc": "채널별 마감: 채널(POS/EDC/EDI/FOREIGN) 단위로 당일 매입을 집계하고", "in": [ "T_BIZDATE", + "T_STR1", "T_CHANNEL" ], "out": [ + "T_RC", "T_ID1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", "T_GROSS" ] @@ -4112,14 +5803,21 @@ const SVC_SCREENS = [ "svc": "CL_CHNVERIFY", "desc": "채널별 마감 검증: 영업일+채널의 매입건수와 해당 매입에 연결된 ledger", "in": [ - "T_BIZDATE", - "T_CHANNEL" + "T_ID1", + "T_BIZDATE" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", + "T_AMT1", + "T_AMT2", "T_DELTA", - "T_RC" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4128,13 +5826,19 @@ const SVC_SCREENS = [ "svc": "CL_CLOSERATE", "desc": "마감율 산출: 월 프리픽스(YYYY-MM)에 대해 매입이 발생한 영업일수 대비", "in": [ + "T_BIZDATE", "T_STR1" ], "out": [ - "T_ARG1", - "T_ARG2", - "T_ARG3", - "T_RC" + "T_RC", + "T_ID1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4143,13 +5847,21 @@ const SVC_SCREENS = [ "svc": "CL_CNTVERIFY", "desc": "건수 정합검증: 마감이력 txn_count 와 해당 영업일 매입 실건수를 대사한다.", "in": [ - "T_ID1" + "T_ID1", + "T_BIZDATE" ], "out": [ + "T_RC", "T_ARG1", - "T_COUNT", + "T_ARG2", + "T_AMT1", + "T_AMT2", "T_DELTA", - "T_RC" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4161,10 +5873,16 @@ const SVC_SCREENS = [ "T_ID1" ], "out": [ - "T_ARG1", - "T_ARG2", - "T_ARG3", - "T_RC" + "T_RC", + "T_AMT1", + "T_AMT2", + "T_AMT3", + "T_DELTA", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4173,9 +5891,17 @@ const SVC_SCREENS = [ "svc": "CL_CONFIRM", "desc": "마감 확정: 상태 C.", "in": [ - "T_ID1" + "T_ID1", + "T_STR1" ], - "out": [] + "out": [ + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" + ] }, { "mod": "cl", @@ -4183,11 +5909,20 @@ const SVC_SCREENS = [ "svc": "CL_CORPCLOSE", "desc": "법인별 마감: 당일 매입을 발급사(issuer) 법인 단위로 그룹 커서 순회하며", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1", + "T_ISSUER" ], "out": [ + "T_RC", + "T_ID1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_ID1" + "T_GROSS" ] }, { @@ -4196,14 +5931,21 @@ const SVC_SCREENS = [ "svc": "CL_CORPVERIFY", "desc": "법인별 마감 검증: 마감이력 헤더 건수/금액과 스냅샷 집계 정합 점검.", "in": [ - "T_ID1" + "T_ID1", + "T_BIZDATE" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", "T_AMT1", "T_AMT2", - "T_RC" + "T_DELTA", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4212,11 +5954,17 @@ const SVC_SCREENS = [ "svc": "CL_CXLDUP", "desc": "중복마감 취소: 동일 기간 중복 마감이력 중 최신을 남기고 취소(status=X) 처리.", "in": [ - "T_ID1" + "T_ID1", + "T_REASON", + "T_BIZDATE" ], "out": [ "T_RC", - "T_COUNT" + "T_COUNT", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_GROSS" ] }, { @@ -4225,11 +5973,17 @@ const SVC_SCREENS = [ "svc": "CL_CXLERROR", "desc": "오류정정 취소: 오류정정 사유로 마감을 취소(status=X)한다.", "in": [ - "T_ID1" + "T_ID1", + "T_REASON", + "T_BIZDATE" ], "out": [ - "T_ID1", - "T_RC" + "T_RC", + "T_COUNT", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_GROSS" ] }, { @@ -4238,11 +5992,17 @@ const SVC_SCREENS = [ "svc": "CL_CXLLATE", "desc": "지연자료 취소: 지연자료 반영 사유로 마감을 취소(status=X)한다.", "in": [ - "T_ID1" + "T_ID1", + "T_REASON", + "T_BIZDATE" ], "out": [ - "T_ID1", - "T_RC" + "T_RC", + "T_COUNT", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_GROSS" ] }, { @@ -4250,11 +6010,15 @@ const SVC_SCREENS = [ "modnm": "마감", "svc": "CL_CXLREASON", "desc": "마감 취소 사유별 집계: 기간 프리픽스의 취소(X) 마감을 memo(사유)별로 그룹", - "in": [ - "T_STR1" - ], + "in": [], "out": [ - "T_COUNT" + "T_RC", + "T_COUNT", + "T_ARG3", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_GROSS" ] }, { @@ -4264,11 +6028,16 @@ const SVC_SCREENS = [ "desc": "마감 취소 요청: 취소 사유(T_REASON)를 memo(CXLREQ:..)에 기록해 요청 표기(상태 유지).", "in": [ "T_ID1", - "T_REASON" + "T_REASON", + "T_BIZDATE" ], "out": [ - "T_ID1", - "T_RC" + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4278,10 +6047,13 @@ const SVC_SCREENS = [ "desc": "마감 취소 현황: 취소 사유별(오류정정/지연자료/중복마감) 취소 건수를 집계한다.", "in": [], "out": [ - "T_ARG1", - "T_ARG2", + "T_RC", + "T_COUNT", "T_ARG3", - "T_COUNT" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_GROSS" ] }, { @@ -4293,7 +6065,11 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG6", + "T_ARG7", + "T_ARG8" ] }, { @@ -4302,15 +6078,21 @@ const SVC_SCREENS = [ "svc": "CL_DAYVERIFY", "desc": "일 마감 검증: 당일 마감이력 집계와 매입 실집계 대사.", "in": [ + "T_ID1", "T_BIZDATE" ], "out": [ - "T_COUNT", + "T_RC", "T_ARG1", + "T_ARG2", "T_AMT1", "T_AMT2", "T_DELTA", - "T_RC" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4318,10 +6100,20 @@ const SVC_SCREENS = [ "modnm": "마감", "svc": "CL_DELAYLIST", "desc": "지연 마감 목록: 생성 후 24시간이 지나도록 개시(O) 상태로 남은 마감을", - "in": [], + "in": [ + "T_BIZDATE", + "T_STR1", + "T_KEY1", + "T_ID1" + ], "out": [ + "T_RC", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_RC" + "T_GROSS" ] }, { @@ -4330,11 +6122,20 @@ const SVC_SCREENS = [ "svc": "CL_DELAYREG", "desc": "마감 지연 사유 등록: 개시(O) 상태의 마감건에만 지연 사유를 memo 로", "in": [ + "T_BIZDATE", + "T_STR1", + "T_KEY1", "T_ID1", "T_REASON" ], "out": [ - "T_RC" + "T_RC", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4343,11 +6144,16 @@ const SVC_SCREENS = [ "svc": "CL_DIFFCHK", "desc": "차액 점검: close_log total vs 스냅샷 gross 합.", "in": [ + "T_BIZDATE", "T_ID1" ], "out": [ - "T_DELTA", - "T_RC" + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4357,12 +6163,19 @@ const SVC_SCREENS = [ "desc": "부문별 마감: 업종(category) 부문 단위로 당일 매입 건수/정산액(net)을 집계하고", "in": [ "T_BIZDATE", + "T_STR1", "T_CATEGORY" ], "out": [ + "T_RC", "T_ID1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_NET" + "T_GROSS" ] }, { @@ -4371,11 +6184,19 @@ const SVC_SCREENS = [ "svc": "CL_DUELIST", "desc": "마감 예정 목록: 기준일 이전 14일 내 영업일 중 DAILY 마감이 없는 미마감 대상일을 나열.", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1", + "T_KEY1", + "T_ID1" ], "out": [ + "T_RC", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_RC" + "T_GROSS" ] }, { @@ -4383,11 +6204,16 @@ const SVC_SCREENS = [ "modnm": "마감", "svc": "CL_DUPCHK", "desc": "중복 마감 탐지: 취소(X) 제외 동일 (close_type, period_key) 에 마감이 2건", - "in": [], + "in": [ + "T_BIZDATE" + ], "out": [ + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_ARG1", - "T_RC" + "T_GROSS" ] }, { @@ -4395,13 +6221,15 @@ const SVC_SCREENS = [ "modnm": "마감", "svc": "CL_ESCALATE", "desc": "승인 지연 에스컬레이션: 기준일수(T_ARG1, 기본 3일) 초과 승인 대기(C)", - "in": [ - "T_ARG1" - ], + "in": [], "out": [ + "T_RC", "T_COUNT", - "T_ARG2", - "T_RC" + "T_ARG3", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_GROSS" ] }, { @@ -4413,10 +6241,16 @@ const SVC_SCREENS = [ "T_ID1" ], "out": [ + "T_RC", "T_AMT1", "T_AMT2", + "T_AMT3", "T_DELTA", - "T_RC" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4425,14 +6259,21 @@ const SVC_SCREENS = [ "svc": "CL_FEEVERIFY", "desc": "수수료 검증: 마감 스냅샷의 fee 합계와 해당 영업일 매입 fee 합계에", "in": [ - "T_ID1" + "T_ID1", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG1", + "T_ARG2", "T_AMT1", "T_AMT2", - "T_AMT3", "T_DELTA", - "T_RC" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4441,12 +6282,19 @@ const SVC_SCREENS = [ "svc": "CL_FINALIZE", "desc": "본마감: 해당 영업일의 가마감(PROV, status O)을 찾아 매입을 재집계한 뒤", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1" ], "out": [ + "T_RC", "T_ID1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_DELTA" + "T_GROSS" ] }, { @@ -4455,12 +6303,19 @@ const SVC_SCREENS = [ "svc": "CL_FORCECLOSE", "desc": "강제 마감: 체크리스트를 우회해 해당 영업일의 미확정 개시(O) 마감을 제거한", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1" ], "out": [ + "T_RC", "T_ID1", - "T_ARG1", - "T_COUNT" + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4469,14 +6324,19 @@ const SVC_SCREENS = [ "svc": "CL_FXCLOSE", "desc": "해외(FOREIGN) 채널 마감: 해외 매입 집계 + ac_fx 원화환산 합계 후 FX 마감.", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1" ], "out": [ + "T_RC", "T_ID1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_GROSS", - "T_AMT1", - "T_RC" + "T_GROSS" ] }, { @@ -4485,11 +6345,19 @@ const SVC_SCREENS = [ "svc": "CL_GAPDAYS", "desc": "마감 누락일 탐지: 월 프리픽스 내 매입은 존재하나 일마감이 없는 일자를", "in": [ - "T_STR1" + "T_BIZDATE", + "T_STR1", + "T_KEY1", + "T_ID1" ], "out": [ + "T_RC", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_RC" + "T_GROSS" ] }, { @@ -4498,11 +6366,17 @@ const SVC_SCREENS = [ "svc": "CL_HALFCLOSE", "desc": "반기 결산 마감: 연도(T_STR1=YYYY)와 반기(T_ARG1=1|2)를 받아 상/하반기", "in": [ - "T_STR1", - "T_ARG1" + "T_BIZDATE", + "T_STR1" ], "out": [ + "T_RC", "T_ID1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", "T_GROSS" ] @@ -4516,8 +6390,12 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_ARG1", - "T_RC" + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4526,11 +6404,20 @@ const SVC_SCREENS = [ "svc": "CL_HOLIDAY", "desc": "휴일 마감 정책: 영업일의 요일을 판정해 주말이면 마감 SKIP(RC=1) 정책을,", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1", + "T_KEY1", + "T_ID1" ], "out": [ "T_RC", - "T_COUNT" + "T_ARG1", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4539,11 +6426,17 @@ const SVC_SCREENS = [ "svc": "CL_HYCLOSE", "desc": "반기 마감: 상/하반기 6개월 매입 집계 후 HALFYR 마감이력 생성.", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1" ], "out": [ + "T_RC", "T_ID1", - "T_ARG1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", "T_GROSS" ] @@ -4554,14 +6447,21 @@ const SVC_SCREENS = [ "svc": "CL_HYVERIFY", "desc": "반기 마감 검증: 반기 마감이력과 반기 매입 실집계 대사.", "in": [ + "T_ID1", "T_BIZDATE" ], "out": [ + "T_RC", "T_ARG1", + "T_ARG2", "T_AMT1", "T_AMT2", "T_DELTA", - "T_RC" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4569,11 +6469,14 @@ const SVC_SCREENS = [ "modnm": "마감", "svc": "CL_INQUIRY", "desc": "마감 조회: close_type/기간/상태/총건수 반환.", - "in": [ - "T_ID1" - ], + "in": [], "out": [ + "T_RC", "T_COUNT", + "T_ARG3", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_GROSS" ] }, @@ -4584,13 +6487,19 @@ const SVC_SCREENS = [ "desc": "발급사별 마감: 발급사(issuer) 단위 매입 집계 후 ISS 마감.", "in": [ "T_BIZDATE", + "T_STR1", "T_ISSUER" ], "out": [ + "T_RC", "T_ID1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_GROSS", - "T_FEE" + "T_GROSS" ] }, { @@ -4603,7 +6512,14 @@ const SVC_SCREENS = [ "T_STR1", "T_BIZDATE" ], - "out": [] + "out": [ + "T_RC", + "T_COUNT", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_GROSS" + ] }, { "mod": "cl", @@ -4612,8 +6528,13 @@ const SVC_SCREENS = [ "desc": "잠금 감사: 잠금(locked=true) 기간 중 승인(A) 또는 잠금(L) 마감이 하나도", "in": [], "out": [ + "T_RC", "T_COUNT", - "T_RC" + "T_ARG3", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_GROSS" ] }, { @@ -4622,12 +6543,17 @@ const SVC_SCREENS = [ "svc": "CL_LOCKSWEEP", "desc": "기간 잠금 일괄: 프리픽스 구간의 기존 잠금 레코드를 locked=true 로 재잠금하고", "in": [ - "T_STR1" + "T_ID1", + "T_STR1", + "T_BIZDATE" ], "out": [ - "T_ARG1", - "T_ARG2", - "T_COUNT" + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4636,10 +6562,17 @@ const SVC_SCREENS = [ "svc": "CL_MAINCLOSE", "desc": "본마감: 가마감(O) 이력을 당일 전체 매입으로 재집계해 확정(C)으로 승격.", "in": [ - "T_ID1" + "T_BIZDATE", + "T_STR1" ], "out": [ + "T_RC", "T_ID1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", "T_GROSS" ] @@ -4650,12 +6583,19 @@ const SVC_SCREENS = [ "svc": "CL_MEMOSEARCH", "desc": "마감 사유 검색: memo 에 키워드(T_STR1)가 포함된 마감을 검색해 총 건수와", "in": [ - "T_STR1" + "T_BIZDATE", + "T_STR1", + "T_KEY1", + "T_ID1" ], "out": [ - "T_COUNT", "T_RC", - "T_ID1" + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4665,14 +6605,19 @@ const SVC_SCREENS = [ "desc": "가맹점별 마감: 단일 가맹점의 당일 매입(gross/fee/net/건수)을 집계해 해당", "in": [ "T_BIZDATE", + "T_STR1", "T_MERCHANT" ], "out": [ + "T_RC", "T_ID1", - "T_ID2", - "T_GROSS", + "T_FEE", "T_NET", - "T_COUNT" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4684,9 +6629,16 @@ const SVC_SCREENS = [ "T_ID1" ], "out": [ + "T_RC", + "T_AMT1", + "T_AMT2", + "T_AMT3", + "T_DELTA", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_ARG1", - "T_RC" + "T_GROSS" ] }, { @@ -4695,11 +6647,19 @@ const SVC_SCREENS = [ "svc": "CL_MIDCLOSE", "desc": "중간 마감: 당일 중간 스냅 마감이력(MID) 생성.", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1" ], "out": [ + "T_RC", "T_ID1", - "T_COUNT" + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4712,7 +6672,15 @@ const SVC_SCREENS = [ "T_STR1" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4721,15 +6689,21 @@ const SVC_SCREENS = [ "svc": "CL_MONVERIFY", "desc": "월 마감 검증: 월 마감이력과 월 매입 실집계 대사.", "in": [ + "T_ID1", "T_BIZDATE" ], "out": [ + "T_RC", "T_ARG1", - "T_COUNT", + "T_ARG2", "T_AMT1", "T_AMT2", "T_DELTA", - "T_RC" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4741,10 +6715,16 @@ const SVC_SCREENS = [ "T_ID1" ], "out": [ + "T_RC", "T_AMT1", "T_AMT2", + "T_AMT3", "T_DELTA", - "T_RC" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4753,12 +6733,21 @@ const SVC_SCREENS = [ "svc": "CL_NETVERIFY", "desc": "정산액 검증: 마감 스냅샷에서 gross - fee <> net 인 내부 모순 행을 찾아", "in": [ - "T_ID1" + "T_ID1", + "T_BIZDATE" ], "out": [ - "T_COUNT", "T_RC", - "T_DELTA" + "T_ARG1", + "T_ARG2", + "T_AMT1", + "T_AMT2", + "T_DELTA", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4767,10 +6756,19 @@ const SVC_SCREENS = [ "svc": "CL_NEXTBIZ", "desc": "익영업일 산출: 기준일 포함 이후 주말을 건너뛴 첫 영업일과 건너뛴 일수를 반환.", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1", + "T_KEY1", + "T_ID1" ], "out": [ - "T_ARG1" + "T_RC", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4780,10 +6778,13 @@ const SVC_SCREENS = [ "desc": "승인 대기 목록: 승인요청(memo APPR_REQ) 상태로 대기 중인 완료(C) 마감의", "in": [], "out": [ - "T_COUNT", "T_RC", - "T_ID1", - "T_ARG1" + "T_COUNT", + "T_ARG3", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_GROSS" ] }, { @@ -4793,11 +6794,18 @@ const SVC_SCREENS = [ "desc": "기간 마감 개시(open): close_log 를 O 상태로 등록.", "in": [ "T_BIZDATE", - "T_STR1", - "T_STR2" + "T_STR1" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4806,14 +6814,19 @@ const SVC_SCREENS = [ "svc": "CL_POSCLOSE", "desc": "POS 채널 마감: POS 채널 당일 매입 집계 후 POS 마감이력.", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1" ], "out": [ + "T_RC", "T_ID1", - "T_COUNT", - "T_GROSS", "T_FEE", - "T_NET" + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4822,10 +6835,20 @@ const SVC_SCREENS = [ "svc": "CL_POSTTRIG", "desc": "마감 후속작업 트리거: 마감(C/A) 확인 후 해당 영업일의 정산건을 ledger 에", "in": [ + "T_BIZDATE", + "T_STR1", + "T_KEY1", "T_ID1" ], "out": [ - "T_COUNT" + "T_RC", + "T_ARG1", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4838,7 +6861,11 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", - "T_COUNT" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4847,11 +6874,17 @@ const SVC_SCREENS = [ "svc": "CL_PROVCLOSE", "desc": "가마감: 당일 매입 집계 후 status O + memo 'PROV' 가마감을 생성한다.", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1" ], "out": [ "T_RC", "T_ID1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", "T_GROSS" ] @@ -4862,13 +6895,20 @@ const SVC_SCREENS = [ "svc": "CL_PROVCMP", "desc": "가마감/본마감 비교: 동일 영업일의 가마감(TENT) 잠정 집계와 확정(DAILY C) 차이 산출.", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1", + "T_KEY1", + "T_ID1" ], "out": [ - "T_AMT1", - "T_AMT2", - "T_DELTA", - "T_RC" + "T_RC", + "T_ARG1", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4880,7 +6920,12 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_COUNT" + "T_RC", + "T_COUNT", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_GROSS" ] }, { @@ -4889,13 +6934,21 @@ const SVC_SCREENS = [ "svc": "CL_QTRVERIFY", "desc": "분기 마감 검증: 분기 마감이력과 분기 매입 실집계 대사.", "in": [ + "T_ID1", "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG1", + "T_ARG2", "T_AMT1", "T_AMT2", "T_DELTA", - "T_RC" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4908,7 +6961,15 @@ const SVC_SCREENS = [ "T_STR1" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4921,7 +6982,13 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_COUNT" + "T_RC", + "T_ID1", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4930,10 +6997,16 @@ const SVC_SCREENS = [ "svc": "CL_RECLOSECHK", "desc": "재마감 가능여부: 승인(A)/잠금(L) 상태면 재마감 불가로 판정한다.", "in": [ + "T_BIZDATE", "T_ID1" ], "out": [ - "T_RC" + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4942,11 +7015,16 @@ const SVC_SCREENS = [ "svc": "CL_RECLOSEREG", "desc": "재마감 등록: 기존 마감을 재마감(R) 표기하고 동일 기간에 신규 마감이력 개시.", "in": [ - "T_ID1" + "T_ID1", + "T_BIZDATE" ], "out": [ - "T_ID1", + "T_RC", "T_ID2", + "T_ID1", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", "T_GROSS" ] @@ -4961,9 +7039,17 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_COUNT", + "T_RC", + "T_ARG1", + "T_ARG2", + "T_AMT1", + "T_AMT2", "T_DELTA", - "T_RC" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4973,10 +7059,15 @@ const SVC_SCREENS = [ "desc": "마감 재개시: 완료(C) 상태이고 기간이 잠금되지 않은 마감을 개시(O)로", "in": [ "T_ID1", - "T_REASON" + "T_STR1" ], "out": [ - "T_RC" + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -4988,8 +7079,14 @@ const SVC_SCREENS = [ "T_ID1" ], "out": [ - "T_COUNT", - "T_GROSS" + "T_RC", + "T_GROSS", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT" ] }, { @@ -4998,9 +7095,18 @@ const SVC_SCREENS = [ "svc": "CL_RESTORE", "desc": "마감 복원: 취소(X) 마감을 개시(O) 상태로 되돌림.", "in": [ - "T_ID1" + "T_ID1", + "T_REASON", + "T_BIZDATE" ], - "out": [] + "out": [ + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" + ] }, { "mod": "cl", @@ -5008,10 +7114,17 @@ const SVC_SCREENS = [ "svc": "CL_ROLLBACK", "desc": "마감 롤백: 스냅샷 삭제 + 상태 O 로 복귀.", "in": [ - "T_ID1" + "T_ID1", + "T_REASON", + "T_BIZDATE" ], "out": [ - "T_COUNT" + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5021,7 +7134,12 @@ const SVC_SCREENS = [ "desc": "승인 마감 리포트: 승인(A) 상태 마감이력을 최신순으로 나열한다.", "in": [], "out": [ + "T_RC", "T_COUNT", + "T_ARG3", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_GROSS" ] }, @@ -5034,8 +7152,13 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", - "T_ARG4" + "T_ARG3", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_GROSS" ] }, { @@ -5047,7 +7170,12 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", + "T_ARG3", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_GROSS" ] }, @@ -5058,7 +7186,13 @@ const SVC_SCREENS = [ "desc": "취소 마감 리포트: 취소(X) 상태 마감이력을 취소 사유(memo)와 함께 나열한다.", "in": [], "out": [ - "T_COUNT" + "T_RC", + "T_COUNT", + "T_ARG3", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_GROSS" ] }, { @@ -5070,8 +7204,13 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", - "T_ARG2" + "T_ARG3", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_GROSS" ] }, { @@ -5083,7 +7222,12 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", + "T_ARG3", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_GROSS" ] }, @@ -5093,11 +7237,16 @@ const SVC_SCREENS = [ "svc": "CL_RPTISSUER", "desc": "발급사별 마감 리포트: 영업일 매입을 발급사 그룹으로 집계하되 임계금액", "in": [ - "T_BIZDATE", - "T_AMT1" + "T_BIZDATE" ], "out": [ - "T_COUNT" + "T_RC", + "T_COUNT", + "T_ARG3", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_GROSS" ] }, { @@ -5106,10 +7255,16 @@ const SVC_SCREENS = [ "svc": "CL_RPTPERIOD", "desc": "기간별 마감 리포트: close_type(T_KEY1) 의 최근 마감이력을 기간별로 나열한다.", "in": [ - "T_KEY1" + "T_KEY1", + "T_STR1" ], "out": [ + "T_RC", "T_COUNT", + "T_ARG3", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_GROSS" ] }, @@ -5120,7 +7275,13 @@ const SVC_SCREENS = [ "desc": "상태별 마감 리포트: 마감이력을 상태(O/C/A/L/X/R)별 건수/총액으로 집계한다.", "in": [], "out": [ - "T_COUNT" + "T_RC", + "T_COUNT", + "T_ARG3", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_GROSS" ] }, { @@ -5129,11 +7290,16 @@ const SVC_SCREENS = [ "svc": "CL_RPTTOPMER", "desc": "상위 가맹점 마감 리포트: 마감 스냅샷에서 net 상위 N(T_COUNT 입력, 기본 5)", "in": [ - "T_ID1", - "T_COUNT" + "T_ID1" ], "out": [ - "T_COUNT" + "T_RC", + "T_COUNT", + "T_ARG3", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_GROSS" ] }, { @@ -5142,19 +7308,41 @@ const SVC_SCREENS = [ "svc": "CL_SCHEDNEXT", "desc": "다음 마감 일정: 기준일 다음날부터 주말(토/일)을 건너뛴 첫 영업일을 마감 예정일로 산출.", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1", + "T_KEY1", + "T_ID1" ], - "out": [] + "out": [ + "T_RC", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" + ] }, { "mod": "cl", "modnm": "마감", "svc": "CL_SCHEDULE", "desc": "마감 일정 관리: 최종 일마감(biz_date 최대) 기준으로 차기 마감 예정일과", - "in": [], + "in": [ + "T_BIZDATE", + "T_STR1", + "T_KEY1", + "T_ID1" + ], "out": [ "T_RC", - "T_ARG1" + "T_ARG1", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5164,10 +7352,17 @@ const SVC_SCREENS = [ "desc": "부문별 마감: 업종(category) 부문 단위 매입 집계 후 SEG 마감.", "in": [ "T_BIZDATE", + "T_STR1", "T_CATEGORY" ], "out": [ + "T_RC", "T_ID1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", "T_GROSS" ] @@ -5179,13 +7374,20 @@ const SVC_SCREENS = [ "desc": "부문별 마감 검증: 부문 마감이력 총액과 해당 부문 매입 집계 대사.", "in": [ "T_ID1", - "T_CATEGORY" + "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG1", + "T_ARG2", "T_AMT1", "T_AMT2", "T_DELTA", - "T_RC" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5194,11 +7396,17 @@ const SVC_SCREENS = [ "svc": "CL_SEMIMONTH", "desc": "반월 마감: 전반(1~15)/후반(16~말) 구간 집계 후 SEMIMON 마감.", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1" ], "out": [ + "T_RC", "T_ID1", - "T_ARG1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", "T_GROSS" ] @@ -5209,10 +7417,20 @@ const SVC_SCREENS = [ "svc": "CL_SEQ", "desc": "마감 순번 발번(dbio upsert).", "in": [ - "T_STR2" + "T_BIZDATE", + "T_STR1", + "T_KEY1", + "T_ID1" ], "out": [ - "T_ID2" + "T_RC", + "T_ID1", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5220,11 +7438,22 @@ const SVC_SCREENS = [ "modnm": "마감", "svc": "CL_SEQAUDIT", "desc": "마감 순번 감사: cl_seq_reg 의 유형별 last_seq 와 실제 cl_close_log 의", - "in": [], + "in": [ + "T_BIZDATE", + "T_STR1", + "T_KEY1", + "T_ID1" + ], "out": [ - "T_COUNT", + "T_RC", "T_ARG1", - "T_RC" + "T_ARG2", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5236,8 +7465,12 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_RC" + "T_GROSS" ] }, { @@ -5246,13 +7479,20 @@ const SVC_SCREENS = [ "svc": "CL_SLAMON", "desc": "마감 SLA 모니터: 기간 프리픽스의 일마감 중 기준시각(T_STR2=HHMI, 기본 1000)", "in": [ + "T_BIZDATE", "T_STR1", - "T_STR2" + "T_KEY1", + "T_ID1" ], "out": [ - "T_COUNT", + "T_RC", "T_ARG1", - "T_RC" + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5261,11 +7501,20 @@ const SVC_SCREENS = [ "svc": "CL_SLASET", "desc": "마감 SLA 등록: close_type(T_KEY1)별 마감 목표시간(T_ARG1, 시간)을 seq_reg 에 upsert.", "in": [ + "T_BIZDATE", + "T_STR1", "T_KEY1", - "T_ARG1" + "T_ID1" ], "out": [ - "T_ARG1" + "T_RC", + "T_ID1", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5278,10 +7527,15 @@ const SVC_SCREENS = [ "T_ID2" ], "out": [ + "T_RC", "T_AMT1", "T_AMT2", - "T_ARG1", - "T_RC" + "T_DELTA", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5294,9 +7548,15 @@ const SVC_SCREENS = [ "T_ID2" ], "out": [ - "T_COUNT", + "T_RC", + "T_AMT1", + "T_AMT2", "T_DELTA", - "T_RC" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5310,6 +7570,11 @@ const SVC_SCREENS = [ "T_STR1" ], "out": [ + "T_RC", + "T_GROSS", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT" ] }, @@ -5322,11 +7587,16 @@ const SVC_SCREENS = [ "T_ID1" ], "out": [ + "T_RC", "T_AMT1", "T_AMT2", + "T_AMT3", "T_DELTA", - "T_ARG1", - "T_RC" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5334,11 +7604,14 @@ const SVC_SCREENS = [ "modnm": "마감", "svc": "CL_STATS", "desc": "마감 통계: close_type 별 마감 건수/총액.", - "in": [ - "T_STR2" - ], + "in": [], "out": [ + "T_RC", "T_COUNT", + "T_ARG3", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_GROSS" ] }, @@ -5348,9 +7621,20 @@ const SVC_SCREENS = [ "svc": "CL_STATUS", "desc": "마감 상태 조회(dbio getter).", "in": [ + "T_BIZDATE", + "T_STR1", + "T_KEY1", "T_ID1" ], - "out": [] + "out": [ + "T_RC", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" + ] }, { "mod": "cl", @@ -5358,16 +7642,21 @@ const SVC_SCREENS = [ "svc": "CL_STCHGAUDIT", "desc": "마감 상태변경 감사: 특정 기간(period_key) 마감이력의 상태 분포를 상태별로 집계해", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1", + "T_KEY1", + "T_ID1" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", - "T_ARG3", - "T_ARG4", "T_ARG5", "T_ARG6", - "T_COUNT" + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5376,15 +7665,19 @@ const SVC_SCREENS = [ "svc": "CL_STCROSS", "desc": "상태 크로스검증: 당일 매입 상태 분포(A/M/S/C)를 집계해 미마감(A+M) 잔량으로", "in": [ - "T_BIZDATE" + "T_ID1" ], "out": [ - "T_ARG1", - "T_ARG2", - "T_ARG3", - "T_ARG4", + "T_RC", + "T_AMT1", + "T_AMT2", + "T_AMT3", + "T_DELTA", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_RC" + "T_GROSS" ] }, { @@ -5396,9 +7689,14 @@ const SVC_SCREENS = [ "T_ID1" ], "out": [ + "T_RC", "T_GROSS", "T_FEE", - "T_NET" + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT" ] }, { @@ -5407,11 +7705,17 @@ const SVC_SCREENS = [ "svc": "CL_TENDAY", "desc": "순(10일) 마감: 상/중/하순 구간 매입 집계 후 TENDAY 마감이력 생성.", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1" ], "out": [ + "T_RC", "T_ID1", - "T_ARG1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", "T_GROSS" ] @@ -5422,10 +7726,17 @@ const SVC_SCREENS = [ "svc": "CL_TENTCLOSE", "desc": "가마감(잠정): 당일 접수/대사(A,M) 매입 잠정 집계로 가마감(status=O) 이력 생성.", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1" ], "out": [ + "T_RC", "T_ID1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", "T_GROSS" ] @@ -5440,10 +7751,14 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", - "T_COUNT", "T_AMT1", "T_AMT2", - "T_AMT3" + "T_AMT3", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5452,11 +7767,20 @@ const SVC_SCREENS = [ "svc": "CL_TRIGRPT", "desc": "마감 후 리포트 트리거: 마감완료(C)이나 리포트 미생성(memo!=RPT:*)인 마감을 대상으로", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1", + "T_KEY1", + "T_ID1" ], "out": [ + "T_RC", + "T_ARG1", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_RC" + "T_GROSS" ] }, { @@ -5465,12 +7789,20 @@ const SVC_SCREENS = [ "svc": "CL_TRIGSETTLE", "desc": "마감 후 정산 트리거: 마감 완료 당일 정산완료(S) 대상 건수/정산액을 산출하고", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1", + "T_KEY1", + "T_ID1" ], "out": [ + "T_RC", + "T_ARG1", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_NET", - "T_RC" + "T_GROSS" ] }, { @@ -5479,14 +7811,20 @@ const SVC_SCREENS = [ "svc": "CL_TRIGSTAT", "desc": "후속작업 상태: 영업일 정산 건수 대비 ledger XFER 전기 건수를 비교해", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1", + "T_KEY1", + "T_ID1" ], "out": [ + "T_RC", "T_ARG1", - "T_ARG2", - "T_ARG3", - "T_DELTA", - "T_RC" + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5494,9 +7832,20 @@ const SVC_SCREENS = [ "modnm": "마감", "svc": "CL_UNCLOSED", "desc": "미마감일 점검: close_log 없는 매입 영업일 건수.", - "in": [], + "in": [ + "T_BIZDATE", + "T_STR1" + ], "out": [ - "T_COUNT" + "T_RC", + "T_ID1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5505,11 +7854,17 @@ const SVC_SCREENS = [ "svc": "CL_UNLOCK", "desc": "기간 잠금 해제.", "in": [ + "T_ID1", "T_STR1", "T_BIZDATE" ], "out": [ - "T_COUNT" + "T_RC", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5522,8 +7877,17 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG1", + "T_ARG2", + "T_AMT1", + "T_AMT2", + "T_DELTA", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", - "T_RC" + "T_GROSS" ] }, { @@ -5532,10 +7896,17 @@ const SVC_SCREENS = [ "svc": "CL_WEEKLY", "desc": "주 마감: 주간(월~일) 매입을 집계해 WEEKLY 마감이력 생성.", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1" ], "out": [ + "T_RC", "T_ID1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", "T_COUNT", "T_GROSS" ] @@ -5546,13 +7917,21 @@ const SVC_SCREENS = [ "svc": "CL_WEEKVERIFY", "desc": "주 마감 검증: 주간 마감이력과 주간 매입 실집계 대사.", "in": [ + "T_ID1", "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG1", + "T_ARG2", "T_AMT1", "T_AMT2", "T_DELTA", - "T_RC" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5561,10 +7940,19 @@ const SVC_SCREENS = [ "svc": "CL_YEAREND", "desc": "연말 마감: 연도(YYYY) 프리픽스로 집계.", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_STR1" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_FEE", + "T_NET", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5573,13 +7961,19 @@ const SVC_SCREENS = [ "svc": "CL_YEARPREP", "desc": "연말 결산 사전점검: 연도 내 완료된 월마감(MONTHLY, C/A/L)의 distinct", "in": [ - "T_STR1" + "T_BIZDATE", + "T_STR1", + "T_KEY1", + "T_ID1" ], "out": [ - "T_ARG1", - "T_ARG2", - "T_ARG3", - "T_RC" + "T_RC", + "T_ARG5", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5588,15 +7982,21 @@ const SVC_SCREENS = [ "svc": "CL_YEARVERIFY", "desc": "연 마감 검증: 연간 마감이력과 연간 매입 실집계 대사.", "in": [ + "T_ID1", "T_BIZDATE" ], "out": [ + "T_RC", "T_ARG1", - "T_COUNT", + "T_ARG2", "T_AMT1", "T_AMT2", "T_DELTA", - "T_RC" + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_COUNT", + "T_GROSS" ] }, { @@ -5606,10 +8006,16 @@ const SVC_SCREENS = [ "desc": "* CM_ADDDAYS.pgc - cm 모듈 서비스 CM_ADDDAYS (one service = one file).", "in": [ "T_STR1", - "T_ARG1" + "T_ARG1", + "T_BIZDATE", + "T_AMOUNT" ], "out": [ - "T_STR3" + "T_STR3", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5618,13 +8024,20 @@ const SVC_SCREENS = [ "svc": "CM_ADLER32", "desc": "Adler-32 체크섬(순수 계산): 입력(T_STR1)에 대해 두 누산기 a(초기 1)와", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_ARG1", "T_ARG2", "T_ARG3", - "T_STR4" + "T_STR4", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5633,11 +8046,17 @@ const SVC_SCREENS = [ "svc": "CM_AMT_HANGUL", "desc": "금액 한글변환(순수 계산): 정수 금액을 4자리(만 단위) 그룹으로 쪼개", "in": [ - "T_AMOUNT" + "T_AMOUNT", + "T_BIZDATE", + "T_ARG1", + "T_STR1" ], "out": [ "T_RC", - "T_STR4" + "T_STR4", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5647,10 +8066,16 @@ const SVC_SCREENS = [ "desc": "금액 반올림 (순수 계산): 단위(T_ARG1)로 반올림. */", "in": [ "T_AMOUNT", - "T_ARG1" + "T_ARG1", + "T_BIZDATE", + "T_STR1" ], "out": [ - "T_AMT1" + "T_AMT1", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5660,12 +8085,18 @@ const SVC_SCREENS = [ "desc": "금액 균등 분할 (순수 계산): n등분 + 잔액. */", "in": [ "T_AMOUNT", - "T_ARG1" + "T_ARG1", + "T_BIZDATE", + "T_STR1" ], "out": [ "T_AMT1", "T_AMT2", - "T_COUNT" + "T_COUNT", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5675,12 +8106,17 @@ const SVC_SCREENS = [ "desc": "감사로그 레벨별 조회(조회 세분): 레벨(I/W/E)을 검증한 뒤 기준일 해당 레벨", "in": [ "T_STR1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", "T_COUNT", - "T_MSG" + "T_MSG", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5690,12 +8126,19 @@ const SVC_SCREENS = [ "desc": "감사로그 서비스별 조회(조회 세분): 서비스명 기준으로 기준일 로그 건수와", "in": [ "T_SVCNAME", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_COUNT", "T_ID1", - "T_ID2" + "T_ID2", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5705,13 +8148,18 @@ const SVC_SCREENS = [ "desc": "감사로그 오류 비율: dbio(cmdb_log_err_ratio)로 기준일 오류(E) 로그 비율(bps)", "in": [ "T_BIZDATE", - "T_ARG1" + "T_ARG1", + "T_AMOUNT", + "T_STR1" ], "out": [ "T_ARG2", "T_ARG3", "T_STATUS", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5721,13 +8169,20 @@ const SVC_SCREENS = [ "desc": "감사로그 ID 구간 조회(조회 세분): log_id 구간(id1~id2, 최대 폭 10000)의", "in": [ "T_ID1", - "T_ID2" + "T_ID2", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_RC", "T_COUNT", "T_ARG1", - "T_ARG2" + "T_ARG2", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5737,12 +8192,18 @@ const SVC_SCREENS = [ "desc": "감사로그 최근 N건: 기준일(T_BIZDATE)에서 최근 로그 N개(T_ARG1, 기본 5,", "in": [ "T_BIZDATE", - "T_ARG1" + "T_ARG1", + "T_AMOUNT", + "T_STR1" ], "out": [ "T_COUNT", "T_ID1", - "T_STATUS" + "T_STATUS", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5751,13 +8212,19 @@ const SVC_SCREENS = [ "svc": "CM_AUDIT_TOP", "desc": "감사로그 최다 서비스: 기준일(T_BIZDATE) 공통로그를 서비스명으로 그룹핑해", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_RC", "T_SVCNAME", "T_COUNT", - "T_ARG1" + "T_ARG1", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5767,12 +8234,18 @@ const SVC_SCREENS = [ "desc": "배치 선행 완료 점검: 잡(T_KEY1)의 선행 잡(DEP:)을 찾아, 기준일", "in": [ "T_KEY1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_STR4", "T_RC", - "T_COUNT" + "T_COUNT", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5783,10 +8256,15 @@ const SVC_SCREENS = [ "in": [ "T_KEY1", "T_STR1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5796,14 +8274,21 @@ const SVC_SCREENS = [ "desc": "배치 실행 이력 집계: 기준일 해당 잡(svc_name)의 로그를 레벨별로 집계해", "in": [ "T_KEY1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_COUNT", "T_ARG1", "T_ARG2", "T_ARG3", - "T_ID1" + "T_ID1", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5812,13 +8297,20 @@ const SVC_SCREENS = [ "svc": "CM_BATLAST", "desc": "배치 최근 실행 조회: 잡(T_KEY1)의 완료 마커(svc=BATRUN, \" ...\")를", "in": [ - "T_KEY1" + "T_KEY1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_RC", "T_STR4", "T_ID1", - "T_MSG" + "T_MSG", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5829,12 +8321,18 @@ const SVC_SCREENS = [ "in": [ "T_KEY1", "T_BIZDATE", - "T_REASON" + "T_REASON", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_RC", "T_ID1", - "T_COUNT" + "T_COUNT", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5844,11 +8342,17 @@ const SVC_SCREENS = [ "desc": "배치 스케줄 삭제: 잡(T_KEY1)의 스케줄 파라미터(SCHD:)와 딸린 의존성", "in": [ "T_KEY1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_RC", - "T_COUNT" + "T_COUNT", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5857,12 +8361,19 @@ const SVC_SCREENS = [ "svc": "CM_BATSCHD_GET", "desc": "배치 스케줄 조회: SCHD: 파라미터를 읽어 주기와 실행시각을 분리해", "in": [ - "T_KEY1" + "T_KEY1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_RC", "T_STR4", - "T_ARG1" + "T_ARG1", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5870,13 +8381,22 @@ const SVC_SCREENS = [ "modnm": "공통", "svc": "CM_BATSCHD_LIST", "desc": "배치 스케줄 목록 집계: 등록된 스케줄(SCHD:%) 중 주기별(DAILY/WEEKLY/MONTHLY)", - "in": [], + "in": [ + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" + ], "out": [ "T_COUNT", "T_ARG1", "T_ARG2", "T_ARG3", - "T_ARG4" + "T_ARG4", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5888,20 +8408,33 @@ const SVC_SCREENS = [ "T_KEY1", "T_STR1", "T_ARG1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { "mod": "cm", "modnm": "공통", "svc": "CM_BIZDAY", - "desc": "* CM_BIZDAY.pgc - cm 모듈 서비스 CM_BIZDAY (one service = one file).", - "in": [], + "desc": "원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */", + "in": [ + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" + ], "out": [ - "T_BIZDATE" + "T_BIZDATE", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5911,12 +8444,17 @@ const SVC_SCREENS = [ "desc": "N영업일 가산: 기준일(T_BIZDATE)에서 N(T_ARG1)영업일 뒤 날짜를 구한다.", "in": [ "T_BIZDATE", - "T_ARG1" + "T_ARG1", + "T_AMOUNT", + "T_STR1" ], "out": [ "T_RC", "T_STR4", - "T_ARG2" + "T_ARG2", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5926,13 +8464,19 @@ const SVC_SCREENS = [ "desc": "영업일수 계산: from~to 사이를 하루씩 진행하며 주말은 순수 요일 공식", "in": [ "T_STR1", - "T_STR2" + "T_STR2", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", "T_COUNT", "T_ARG1", - "T_ARG2" + "T_ARG2", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5941,49 +8485,77 @@ const SVC_SCREENS = [ "svc": "CM_CACHE_INV", "desc": "캐시 무효화 (순수 처리: 버전 증가 표시). */", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", - "T_MSG" + "T_MSG", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { "mod": "cm", "modnm": "공통", "svc": "CM_CACHE_LOAD", - "desc": "* CM_CACHE_LOAD.pgc - cm 모듈 서비스 CM_CACHE_LOAD (one service = one file).", + "desc": "원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ - "T_COUNT" + "T_COUNT", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { "mod": "cm", "modnm": "공통", "svc": "CM_CHECKDIGIT", - "desc": "* CM_CHECKDIGIT.pgc - cm 모듈 서비스 CM_CHECKDIGIT (one service = one file).", + "desc": "원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */", "in": [ - "T_STR2" + "T_STR2", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_ARG1", - "T_STR4" + "T_STR4", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { "mod": "cm", "modnm": "공통", "svc": "CM_CODE", - "desc": "* CM_CODE.pgc - cm 모듈 서비스 CM_CODE (one service = one file).", + "desc": "원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */", "in": [ "T_STR1", - "T_STR2" + "T_STR2", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STATUS" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -5995,10 +8567,17 @@ const SVC_SCREENS = [ "T_KEY1", "T_KEY2", "T_REASON", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1", + "T_STATUS" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6007,13 +8586,21 @@ const SVC_SCREENS = [ "svc": "CM_CODEGRP_CNT", "desc": "코드그룹 현황: 그룹의 전체 코드 수와 사용중(use_yn='Y') 코드 수를 한 번의", "in": [ - "T_KEY1" + "T_KEY1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1", + "T_STATUS" ], "out": [ "T_RC", "T_COUNT", "T_ARG1", - "T_ARG2" + "T_ARG2", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6023,12 +8610,21 @@ const SVC_SCREENS = [ "desc": "코드그룹 차집합 비교: 기준 그룹(T_KEY1)에는 있으나 대상 그룹(T_KEY2)에는", "in": [ "T_KEY1", - "T_KEY2" + "T_KEY2", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1", + "T_STATUS" ], "out": [ "T_COUNT", "T_ARG1", - "T_ARG2" + "T_ARG2", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6037,12 +8633,21 @@ const SVC_SCREENS = [ "svc": "CM_CODEGRP_HIST", "desc": "코드그룹 변경 이력 조회: CODEGRP 이력 로그에서 그룹 접두 패턴으로 건수와", "in": [ - "T_KEY1" + "T_KEY1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1", + "T_STATUS" ], "out": [ "T_COUNT", "T_ID1", - "T_MSG" + "T_MSG", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6055,10 +8660,15 @@ const SVC_SCREENS = [ "T_KEY2", "T_STR1", "T_ARG1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_STATUS" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6067,13 +8677,21 @@ const SVC_SCREENS = [ "svc": "CM_CODEGRP_SPAN", "desc": "코드그룹 정렬범위 조회: 그룹(T_KEY1) 내 sort_ord 의 최대-최소 폭을 dbio", "in": [ - "T_KEY1" + "T_KEY1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1", + "T_STATUS" ], "out": [ "T_RC", "T_ARG1", "T_COUNT", - "T_ARG2" + "T_ARG2", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6086,11 +8704,16 @@ const SVC_SCREENS = [ "T_KEY2", "T_STR1", "T_ARG1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_STATUS" ], "out": [ "T_RC", - "T_STR4" + "T_STR4", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6101,24 +8724,38 @@ const SVC_SCREENS = [ "in": [ "T_KEY1", "T_ARG1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_STR1", + "T_STATUS" ], "out": [ "T_ARG2", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { "mod": "cm", "modnm": "공통", "svc": "CM_CODENAME", - "desc": "* CM_CODENAME.pgc - cm 모듈 서비스 CM_CODENAME (one service = one file).", + "desc": "원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */", "in": [ "T_STR1", - "T_STR2" + "T_STR2", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STATUS" ], "out": [ - "T_STR3" + "T_STR3", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6127,12 +8764,21 @@ const SVC_SCREENS = [ "svc": "CM_CODE_EXPORT", "desc": "코드그룹 직렬화: 그룹(T_KEY1)의 사용중 코드를 정렬순서대로 커서 순회하며", "in": [ - "T_KEY1" + "T_KEY1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1", + "T_STATUS" ], "out": [ "T_STR4", "T_COUNT", - "T_ARG1" + "T_ARG1", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6144,10 +8790,16 @@ const SVC_SCREENS = [ "T_KEY1", "T_KEY2", "T_STR1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STATUS" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6156,12 +8808,20 @@ const SVC_SCREENS = [ "svc": "CM_CODE_REORDER", "desc": "코드 정렬순서 재부여: 대상 그룹(T_KEY1)의 사용중 코드를 코드값 오름차순으로", "in": [ - "T_KEY1" + "T_KEY1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1", + "T_STATUS" ], "out": [ "T_COUNT", "T_ARG1", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6170,12 +8830,19 @@ const SVC_SCREENS = [ "svc": "CM_CODE_SEARCH", "desc": "코드명 검색: 입력어(T_STR1)를 코드명 부분일치 패턴(%검색어%)으로 만들어", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STATUS" ], "out": [ "T_RC", "T_COUNT", - "T_STR4" + "T_STR4", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6185,11 +8852,19 @@ const SVC_SCREENS = [ "desc": "코드 사용여부 토글: 대상 코드(그룹 T_KEY1 / 코드 T_KEY2)의 use_yn 를 현재", "in": [ "T_KEY1", - "T_KEY2" + "T_KEY2", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1", + "T_STATUS" ], "out": [ "T_RC", - "T_STATUS" + "T_STATUS", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6198,11 +8873,18 @@ const SVC_SCREENS = [ "svc": "CM_CRC", "desc": "* CM_CRC.pgc - cm 모듈 서비스 CM_CRC (one service = one file).", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_STR4", - "T_ARG1" + "T_ARG1", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6212,12 +8894,18 @@ const SVC_SCREENS = [ "desc": "기간 계산 유틸(순수 계산): 두 날짜를 역법 공식으로 절대 일련일수로 바꿔", "in": [ "T_STR1", - "T_STR2" + "T_STR2", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", "T_ARG1", - "T_ARG2" + "T_ARG2", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6226,14 +8914,20 @@ const SVC_SCREENS = [ "svc": "CM_DAYOFYEAR", "desc": "연중 경과일/잔여일(순수 계산): 입력일(T_STR1)이 그 해의 몇 번째 날인지", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", "T_ARG1", "T_ARG2", "T_ARG3", - "T_ARG4" + "T_ARG4", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6244,24 +8938,37 @@ const SVC_SCREENS = [ "in": [ "T_STR1", "T_KEY1", - "T_ARG1" + "T_ARG1", + "T_BIZDATE", + "T_AMOUNT" ], "out": [ "T_RC", - "T_STR4" + "T_STR4", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { "mod": "cm", "modnm": "공통", "svc": "CM_ERRMSG", - "desc": "* CM_ERRMSG.pgc - cm 모듈 서비스 CM_ERRMSG (one service = one file).", + "desc": "원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */", "in": [ "T_STR1", - "T_STR2" + "T_STR2", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STATUS" ], "out": [ - "T_MSG" + "T_MSG", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6270,25 +8977,39 @@ const SVC_SCREENS = [ "svc": "CM_FILE_CKSUM", "desc": "파일 체크섬(순수 계산): 파일 내용 문자열에 대해 Adler-32 체크섬(mod 65521", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_STR4", "T_ARG1", "T_ARG2", - "T_COUNT" + "T_COUNT", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { "mod": "cm", "modnm": "공통", "svc": "CM_FMT_AMT", - "desc": "* CM_FMT_AMT.pgc - cm 모듈 서비스 CM_FMT_AMT (one service = one file).", + "desc": "원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */", "in": [ - "T_AMOUNT" + "T_AMOUNT", + "T_BIZDATE", + "T_ARG1", + "T_STR1" ], "out": [ - "T_STR4" + "T_STR4", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6297,23 +9018,37 @@ const SVC_SCREENS = [ "svc": "CM_FMT_DATE", "desc": "날짜 포맷 변환 (순수 계산): YYYYMMDD -> YYYY-MM-DD. */", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ - "T_STR3" + "T_STR3", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { "mod": "cm", "modnm": "공통", "svc": "CM_FX", - "desc": "* CM_FX.pgc - cm 모듈 서비스 CM_FX (one service = one file).", + "desc": "원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */", "in": [ "T_CCY", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ - "T_ARG1" + "T_ARG1", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6323,12 +9058,19 @@ const SVC_SCREENS = [ "desc": "환율 최신성 조회: 통화의 마지막 고시일과 오늘 사이 경과일수를 DB 날짜", "in": [ "T_CCY", - "T_ARG2" + "T_ARG2", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_RC", "T_ARG1", - "T_STR4" + "T_STR4", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6339,11 +9081,18 @@ const SVC_SCREENS = [ "in": [ "T_CCY", "T_BIZDATE", - "T_FX_AMT" + "T_FX_AMT", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_AMOUNT", - "T_ARG1" + "T_ARG1", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6354,13 +9103,18 @@ const SVC_SCREENS = [ "in": [ "T_CCY", "T_STR1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_ARG1", "T_RC", "T_AMT1", - "T_AMT2" + "T_AMT2", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6371,14 +9125,20 @@ const SVC_SCREENS = [ "in": [ "T_CCY", "T_STR1", - "T_STR2" + "T_STR2", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", "T_COUNT", "T_AMT1", "T_AMT2", - "T_STR4" + "T_STR4", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6388,12 +9148,18 @@ const SVC_SCREENS = [ "desc": "역환율 산출: 통화(T_CCY)의 고시환율(rate_bps = KRW*100/unit)을 조회해", "in": [ "T_CCY", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_RC", "T_ARG1", - "T_ARG2" + "T_ARG2", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6403,13 +9169,20 @@ const SVC_SCREENS = [ "desc": "매매기준율 스프레드(순수 계산): 고시환율(T_ARG1, bps)에 스프레드율", "in": [ "T_ARG1", - "T_ARG2" + "T_ARG2", + "T_BIZDATE", + "T_AMOUNT", + "T_STR1", + "T_CCY" ], "out": [ "T_RC", "T_ARG3", "T_ARG4", - "T_DELTA" + "T_DELTA", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6419,13 +9192,19 @@ const SVC_SCREENS = [ "desc": "기준일 평균환율: dbio(cmdb_fx_avg)로 기준일 전체 통화의 평균환율을 구하고,", "in": [ "T_BIZDATE", - "T_CCY" + "T_CCY", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_ARG1", "T_ARG2", "T_ARG3", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6436,10 +9215,15 @@ const SVC_SCREENS = [ "in": [ "T_CCY", "T_BIZDATE", - "T_ARG1" + "T_ARG1", + "T_AMOUNT", + "T_STR1" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6448,13 +9232,21 @@ const SVC_SCREENS = [ "svc": "CM_FX_TICKS", "desc": "환율 고시 현황: 기준일(T_BIZDATE)에 고시된 통화 수와 CCY 코드그룹의 등록", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1", + "T_CCY" ], "out": [ "T_COUNT", "T_ARG1", "T_ARG2", - "T_ARG3" + "T_ARG3", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6465,7 +9257,9 @@ const SVC_SCREENS = [ "in": [ "T_CCY", "T_STR1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", @@ -6473,7 +9267,10 @@ const SVC_SCREENS = [ "T_ARG2", "T_DELTA", "T_ARG3", - "T_STATUS" + "T_STATUS", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6485,12 +9282,16 @@ const SVC_SCREENS = [ "T_CCY", "T_STR1", "T_ARG1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT" ], "out": [ "T_RC", "T_ARG2", - "T_ARG3" + "T_ARG3", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6501,12 +9302,17 @@ const SVC_SCREENS = [ "in": [ "T_CCY", "T_BIZDATE", - "T_ARG1" + "T_ARG1", + "T_AMOUNT", + "T_STR1" ], "out": [ "T_ARG2", "T_ARG3", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6515,12 +9321,19 @@ const SVC_SCREENS = [ "svc": "CM_HASH_DJB2", "desc": "djb2 해시(순수 계산): 입력 문자열(T_STR1)에 대해 hash = hash*33 + c 점화식", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_ARG1", "T_STR4", - "T_COUNT" + "T_COUNT", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6529,12 +9342,19 @@ const SVC_SCREENS = [ "svc": "CM_HASH_FNV", "desc": "FNV-1a 해시(순수 계산): 입력(T_STR1)을 오프셋 베이시스(2166136261)에서", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_ARG1", "T_STR4", - "T_ARG2" + "T_ARG2", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6543,12 +9363,19 @@ const SVC_SCREENS = [ "svc": "CM_HASH_SUM", "desc": "가중 체크섬(순수 계산): 입력(T_STR1) 각 문자의 코드값에 위치 가중(1부터", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_ARG1", "T_ARG2", - "T_STR4" + "T_STR4", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6557,14 +9384,20 @@ const SVC_SCREENS = [ "svc": "CM_HEALTH_ALL", "desc": "종합 헬스 스코어: 기준일 로그/환율/채번/파라미터 존재를 각 25점으로 가중해", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_ARG1", "T_STATUS", "T_ARG2", "T_ARG3", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6572,13 +9405,21 @@ const SVC_SCREENS = [ "modnm": "공통", "svc": "CM_HEALTH_DB", "desc": "시스템 헬스(DB 세분): DB 왕복(SELECT)으로 연결을 확인하고 DB 서버 일자와", - "in": [], + "in": [ + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" + ], "out": [ "T_STATUS", "T_STR4", "T_STR5", "T_ARG2", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6588,13 +9429,18 @@ const SVC_SCREENS = [ "desc": "시스템 헬스(환율 신선도): 기준일(T_BIZDATE)에 고시된 통화가 하나라도 있는지,", "in": [ "T_BIZDATE", - "T_ARG1" + "T_ARG1", + "T_AMOUNT", + "T_STR1" ], "out": [ "T_STATUS", "T_COUNT", "T_ARG2", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6603,13 +9449,20 @@ const SVC_SCREENS = [ "svc": "CM_HEALTH_LOG", "desc": "시스템 헬스(로그 세분): 금일 로그 총량과 오류(E) 비율(bps)을 계산해 등급을", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_STATUS", "T_COUNT", "T_ARG1", - "T_ARG2" + "T_ARG2", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6617,13 +9470,21 @@ const SVC_SCREENS = [ "modnm": "공통", "svc": "CM_HEALTH_PARAM", "desc": "필수 파라미터 점검: 운영에 반드시 필요한 키 목록(BIZ_DATE/VAT_RATE/WHT_BPS", - "in": [], + "in": [ + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" + ], "out": [ "T_ARG1", "T_ARG2", "T_STR4", "T_STATUS", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6631,25 +9492,39 @@ const SVC_SCREENS = [ "modnm": "공통", "svc": "CM_HEALTH_SEQ", "desc": "시스템 헬스(채번 세분): 카운터 개수/최대값과 음수로 굴러간 비정상 카운터", - "in": [], + "in": [ + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" + ], "out": [ "T_STATUS", "T_COUNT", "T_ARG1", "T_ARG2", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { "mod": "cm", "modnm": "공통", "svc": "CM_HOLIDAY_CHK", - "desc": "* CM_HOLIDAY_CHK.pgc - cm 모듈 서비스 CM_HOLIDAY_CHK (one service = one file).", + "desc": "원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6660,12 +9535,19 @@ const SVC_SCREENS = [ "in": [ "T_STR1", "T_STR2", - "T_STR3" + "T_STR3", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_COUNT", "T_STR4", - "T_STR5" + "T_STR5", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6674,11 +9556,17 @@ const SVC_SCREENS = [ "svc": "CM_HOLIDAY_DEL", "desc": "휴일 삭제(캘린더 관리): 삭제 전에 휴일명을 읽어 이력에 남기고 cm_holiday", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", - "T_STR4" + "T_STR4", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6688,13 +9576,18 @@ const SVC_SCREENS = [ "desc": "다음 휴일 조회: 기준일(T_BIZDATE) 이후 국가(T_STR1, 기본 KR)의 가장 가까운", "in": [ "T_BIZDATE", - "T_STR1" + "T_STR1", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", "T_STR4", "T_MSG", - "T_ARG1" + "T_ARG1", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6705,10 +9598,16 @@ const SVC_SCREENS = [ "in": [ "T_STR1", "T_STR2", - "T_STR3" + "T_STR3", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6718,13 +9617,18 @@ const SVC_SCREENS = [ "desc": "연도별 휴일 집계: 연도(T_ARG1)와 국가(T_STR1, 기본 KR)의 등록 휴일 수와", "in": [ "T_ARG1", - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT" ], "out": [ "T_RC", "T_COUNT", "T_ARG2", - "T_ARG3" + "T_ARG3", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6735,10 +9639,16 @@ const SVC_SCREENS = [ "in": [ "T_AMOUNT", "T_ARG1", - "T_ARG2" + "T_ARG2", + "T_BIZDATE", + "T_STR1" ], "out": [ - "T_AMT1" + "T_AMT1", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6747,39 +9657,59 @@ const SVC_SCREENS = [ "svc": "CM_ISO_WEEK", "desc": "ISO-8601 주차 산출(순수 계산): 입력일(T_STR1)의 통일된 일련일수를 구해", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", "T_ARG1", "T_ARG2", - "T_ARG3" + "T_ARG3", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { "mod": "cm", "modnm": "공통", "svc": "CM_LOG", - "desc": "* CM_LOG.pgc - cm 모듈 서비스 CM_LOG (one service = one file).", + "desc": "원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */", "in": [ "T_SVCNAME", "T_MSG", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ - "T_ID1" + "T_ID1", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { "mod": "cm", "modnm": "공통", "svc": "CM_LUHN", - "desc": "* CM_LUHN.pgc - cm 모듈 서비스 CM_LUHN (one service = one file).", + "desc": "원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */", "in": [ - "T_STR5" + "T_STR5", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6788,10 +9718,18 @@ const SVC_SCREENS = [ "svc": "CM_MASK", "desc": "카드번호 마스킹 (순수 계산): 앞6 뒤4 노출. */", "in": [ - "T_STR5" + "T_STR5", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ - "T_STR4" + "T_STR4", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6800,12 +9738,18 @@ const SVC_SCREENS = [ "svc": "CM_MASK_ACCT", "desc": "계좌번호 마스킹(순수 계산): 하이픈 구분자는 위치를 보존하면서, 숫자 기준", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", "T_STR4", - "T_ARG1" + "T_ARG1", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6814,12 +9758,18 @@ const SVC_SCREENS = [ "svc": "CM_MASK_CARD", "desc": "카드번호 마스킹(마스킹 세분, 순수 계산): 전체가 숫자(13~19자리)인지 먼저", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", "T_STR4", - "T_ARG1" + "T_ARG1", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6828,12 +9778,18 @@ const SVC_SCREENS = [ "svc": "CM_MASK_EMAIL", "desc": "이메일 마스킹(순수 계산): 로컬파트(@ 앞)의 첫 글자만 남기고 나머지를 '*' 로", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", "T_STR4", - "T_ARG1" + "T_ARG1", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6842,12 +9798,18 @@ const SVC_SCREENS = [ "svc": "CM_MASK_NAME", "desc": "성명 마스킹(마스킹 세분, 순수 계산): UTF-8 문자 단위로 첫 글자만 남기고", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", "T_STR4", - "T_ARG1" + "T_ARG1", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6856,12 +9818,18 @@ const SVC_SCREENS = [ "svc": "CM_MASK_PHONE", "desc": "전화번호 마스킹(마스킹 세분, 순수 계산): 하이픈 등 구분자는 위치를 그대로", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", "T_STR4", - "T_ARG1" + "T_ARG1", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6870,12 +9838,18 @@ const SVC_SCREENS = [ "svc": "CM_MASK_RRN", "desc": "주민등록번호 마스킹(순수 계산): \"YYMMDD-SXXXXXX\" 13자리 형식을 검증하고,", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", "T_STR4", - "T_ARG1" + "T_ARG1", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6885,13 +9859,18 @@ const SVC_SCREENS = [ "desc": "시장 개장 여부: 대상일(T_BIZDATE)이 국가(T_STR1, 기본 KR) 기준 개장일인지", "in": [ "T_BIZDATE", - "T_STR1" + "T_STR1", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_STATUS", "T_STR4", "T_ARG1", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6900,11 +9879,18 @@ const SVC_SCREENS = [ "svc": "CM_MONTHEND", "desc": "* CM_MONTHEND.pgc - cm 모듈 서비스 CM_MONTHEND (one service = one file).", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_STR3", - "T_ARG1" + "T_ARG1", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6914,12 +9900,19 @@ const SVC_SCREENS = [ "desc": "다국어 메시지 폐기: 언어(T_STR1)와 메시지 ID(T_KEY1)로 MSG_ 그룹의", "in": [ "T_STR1", - "T_KEY1" + "T_KEY1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STATUS" ], "out": [ "T_RC", "T_STR4", - "T_COUNT" + "T_COUNT", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6929,11 +9922,18 @@ const SVC_SCREENS = [ "desc": "다국어 메시지 폴백 조회: 요청 언어 그룹에서 먼저 찾고, 없으면 기본 언어", "in": [ "T_STR1", - "T_KEY1" + "T_KEY1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STATUS" ], "out": [ "T_MSG", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6945,11 +9945,19 @@ const SVC_SCREENS = [ "T_STR1", "T_STR2", "T_STR3", - "T_STR4" + "T_STR4", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STATUS" ], "out": [ "T_MSG", - "T_COUNT" + "T_COUNT", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6959,12 +9967,19 @@ const SVC_SCREENS = [ "desc": "다국어 메시지 조회: 언어코드(KO/EN/JA...)로 메시지 그룹 MSG_ 을 정해", "in": [ "T_STR1", - "T_KEY1" + "T_KEY1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STATUS" ], "out": [ "T_RC", "T_MSG", - "T_STR4" + "T_STR4", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6973,11 +9988,20 @@ const SVC_SCREENS = [ "svc": "CM_MSG_LANGS", "desc": "메시지 다국어 커버리지: 특정 메시지 ID(T_KEY1)가 어떤 언어 그룹(MSG_%)에", "in": [ - "T_KEY1" + "T_KEY1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1", + "T_STATUS" ], "out": [ "T_COUNT", - "T_STR4" + "T_STR4", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -6989,11 +10013,18 @@ const SVC_SCREENS = [ "T_STR1", "T_KEY1", "T_STR3", - "T_STR4" + "T_STR4", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STATUS" ], "out": [ "T_RC", - "T_MSG" + "T_MSG", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7004,10 +10035,17 @@ const SVC_SCREENS = [ "in": [ "T_STR1", "T_KEY1", - "T_MSG" + "T_MSG", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STATUS" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7017,23 +10055,37 @@ const SVC_SCREENS = [ "desc": "네팅 계산 (순수 계산): 차변-대변. */", "in": [ "T_AMT1", - "T_AMT2" + "T_AMT2", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_NET", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { "mod": "cm", "modnm": "공통", "svc": "CM_NEXTBIZ", - "desc": "* CM_NEXTBIZ.pgc - cm 모듈 서비스 CM_NEXTBIZ (one service = one file).", + "desc": "원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ - "T_STR3" + "T_STR3", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7043,12 +10095,19 @@ const SVC_SCREENS = [ "desc": "알림 발송 이력: 기준일 NOTI 로그에서 채널 접두 패턴([SMS>...] 등)으로 발송", "in": [ "T_CHANNEL", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_COUNT", "T_ID1", - "T_MSG" + "T_MSG", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7059,13 +10118,18 @@ const SVC_SCREENS = [ "in": [ "T_CHANNEL", "T_BIZDATE", - "T_ARG1" + "T_ARG1", + "T_AMOUNT", + "T_STR1" ], "out": [ "T_ARG2", "T_COUNT", "T_ARG3", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7075,12 +10139,18 @@ const SVC_SCREENS = [ "desc": "알림 재발송: 원 발송 로그 ID(T_ID1)의 내용을 조회해 동일 본문을 재큐잉", "in": [ "T_ID1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_RC", "T_ID2", - "T_MSG" + "T_MSG", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7092,11 +10162,16 @@ const SVC_SCREENS = [ "T_CHANNEL", "T_STR1", "T_MSG", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", - "T_ID1" + "T_ID1", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7105,13 +10180,20 @@ const SVC_SCREENS = [ "svc": "CM_NOTI_STAT", "desc": "알림 채널별 통계: 기준일(T_BIZDATE) NOTI 로그를 본문 접두([SMS>/[EMAIL>/", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_ARG1", "T_ARG2", "T_ARG3", - "T_COUNT" + "T_COUNT", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7125,24 +10207,38 @@ const SVC_SCREENS = [ "T_STR3", "T_CHANNEL", "T_STR1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", "T_ID1", - "T_MSG" + "T_MSG", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { "mod": "cm", "modnm": "공통", "svc": "CM_PARAM", - "desc": "* CM_PARAM.pgc - cm 모듈 서비스 CM_PARAM (one service = one file).", + "desc": "원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_KEY1", + "T_STATUS" ], "out": [ - "T_STR3" + "T_STR3", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7153,11 +10249,18 @@ const SVC_SCREENS = [ "in": [ "T_STR1", "T_STR3", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_KEY1", + "T_STATUS" ], "out": [ "T_RC", - "T_STR4" + "T_STR4", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7168,11 +10271,18 @@ const SVC_SCREENS = [ "in": [ "T_STR1", "T_STR3", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_KEY1", + "T_STATUS" ], "out": [ "T_RC", - "T_STR4" + "T_STR4", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7181,7 +10291,12 @@ const SVC_SCREENS = [ "svc": "CM_PARAM_DIFF", "desc": "파라미터 요청-현재 비교: 대기 요청 값(REQ:)과 현재 본 값을 읽어와", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_KEY1", + "T_STATUS" ], "out": [ "T_RC", @@ -7189,7 +10304,10 @@ const SVC_SCREENS = [ "T_STR5", "T_ARG1", "T_ARG2", - "T_DELTA" + "T_DELTA", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7198,12 +10316,21 @@ const SVC_SCREENS = [ "svc": "CM_PARAM_HIST", "desc": "파라미터 변경 이력 조회: 승인 워크플로가 남긴 공통로그(svc=PARAMWF)에서", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_KEY1", + "T_STATUS" ], "out": [ "T_COUNT", "T_ID1", - "T_MSG" + "T_MSG", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7212,11 +10339,20 @@ const SVC_SCREENS = [ "svc": "CM_PARAM_LIST", "desc": "파라미터 접두 목록: 접두어(T_STR1, 예 \"SCHD:\")로 시작하는 파라미터 키의", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_KEY1", + "T_STATUS" ], "out": [ "T_COUNT", - "T_ARG1" + "T_ARG1", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7227,12 +10363,19 @@ const SVC_SCREENS = [ "in": [ "T_STR1", "T_ARG1", - "T_ARG2" + "T_ARG2", + "T_BIZDATE", + "T_AMOUNT", + "T_KEY1", + "T_STATUS" ], "out": [ "T_RC", "T_ARG3", - "T_ARG4" + "T_ARG4", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7240,12 +10383,22 @@ const SVC_SCREENS = [ "modnm": "공통", "svc": "CM_PARAM_PENDING", "desc": "파라미터 승인 대기 현황: 대기 키(REQ:%) 건수, 가장 오래된 요청 키, 최장", - "in": [], + "in": [ + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1", + "T_KEY1", + "T_STATUS" + ], "out": [ "T_COUNT", "T_STR4", "T_ARG1", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7256,10 +10409,17 @@ const SVC_SCREENS = [ "in": [ "T_STR1", "T_REASON", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_KEY1", + "T_STATUS" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7271,33 +10431,58 @@ const SVC_SCREENS = [ "T_STR1", "T_STR2", "T_STR3", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_KEY1", + "T_STATUS" ], "out": [ "T_RC", - "T_STR4" + "T_STR4", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { "mod": "cm", "modnm": "공통", "svc": "CM_PARAM_SET", - "desc": "* CM_PARAM_SET.pgc - cm 모듈 서비스 CM_PARAM_SET (one service = one file).", + "desc": "원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */", "in": [ "T_STR1", - "T_STR3" + "T_STR3", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_KEY1", + "T_STATUS" ], - "out": [] + "out": [ + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" + ] }, { "mod": "cm", "modnm": "공통", "svc": "CM_PING", - "desc": "* CM_PING.pgc - cm 모듈 서비스 CM_PING (one service = one file).", - "in": [], + "desc": "원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */", + "in": [ + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" + ], "out": [ "T_MSG", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7306,11 +10491,18 @@ const SVC_SCREENS = [ "svc": "CM_PREVBIZ", "desc": "직전 영업일 조회: 기준일에서 하루씩 거슬러 내려가며 토/일(순수 요일 공식)과", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_STR3", - "T_ARG1" + "T_ARG1", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7321,10 +10513,16 @@ const SVC_SCREENS = [ "in": [ "T_AMOUNT", "T_ARG1", - "T_ARG2" + "T_ARG2", + "T_BIZDATE", + "T_STR1" ], "out": [ - "T_AMT1" + "T_AMT1", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7333,24 +10531,38 @@ const SVC_SCREENS = [ "svc": "CM_QTR_END", "desc": "분기말일 산출(순수 계산): 입력일(T_STR1)이 속한 분기(1~4)를 판정하고 그", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", "T_STR4", - "T_ARG1" + "T_ARG1", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { "mod": "cm", "modnm": "공통", "svc": "CM_SEQ", - "desc": "* CM_SEQ.pgc - cm 모듈 서비스 CM_SEQ (one service = one file).", + "desc": "원 서비스 핵심 로직 - 입출력 필드 계약 보존 (0=정상, -1=실패). */", "in": [ - "T_KEY1" + "T_KEY1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ - "T_ARG1" + "T_ARG1", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7360,13 +10572,19 @@ const SVC_SCREENS = [ "desc": "채번 범위 예약: 카운터(T_KEY1)의 seq_val 을 한 번의 UPDATE 로 N(T_ARG1)만큼", "in": [ "T_KEY1", - "T_ARG1" + "T_ARG1", + "T_BIZDATE", + "T_AMOUNT", + "T_STR1" ], "out": [ "T_RC", "T_ID1", "T_ID2", - "T_COUNT" + "T_COUNT", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7376,11 +10594,18 @@ const SVC_SCREENS = [ "desc": "일자별 채번(채번 세분): 이름+영업일을 합친 키(:)로 매일 1부터", "in": [ "T_KEY1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_ARG1", - "T_STR4" + "T_STR4", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7390,13 +10615,19 @@ const SVC_SCREENS = [ "desc": "연속 채번(블록): 카운터(T_KEY1)에서 N개(T_ARG1)의 일련번호를 연속 채번해", "in": [ "T_KEY1", - "T_ARG1" + "T_ARG1", + "T_BIZDATE", + "T_AMOUNT", + "T_STR1" ], "out": [ "T_RC", "T_ID1", "T_ID2", - "T_COUNT" + "T_COUNT", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7405,12 +10636,19 @@ const SVC_SCREENS = [ "svc": "CM_SEQ_PEEK", "desc": "채번 현재값 조회(채번 세분): 카운터를 증가시키지 않고 cm_seq 의 현재값과", "in": [ - "T_KEY1" + "T_KEY1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_RC", "T_ARG1", - "T_STR4" + "T_STR4", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7421,11 +10659,16 @@ const SVC_SCREENS = [ "in": [ "T_KEY1", "T_ARG1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_STR1" ], "out": [ "T_RC", - "T_ARG2" + "T_ARG2", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7435,12 +10678,18 @@ const SVC_SCREENS = [ "desc": "일자형 채번: 유형(T_KEY1)과 영업일(T_BIZDATE)을 결합한 카운터명", "in": [ "T_KEY1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_RC", "T_ARG1", - "T_STR4" + "T_STR4", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7449,12 +10698,19 @@ const SVC_SCREENS = [ "svc": "CM_SEQ_TYPED", "desc": "유형별 채번(채번 세분): 허용된 채번 유형(SLIP/TICKET/BATCH/NOTI)만 유형별", "in": [ - "T_KEY1" + "T_KEY1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" ], "out": [ "T_RC", "T_ARG1", - "T_STR4" + "T_STR4", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7463,12 +10719,19 @@ const SVC_SCREENS = [ "svc": "CM_SEQ_VERIFY", "desc": "채번 체크디짓 검증(순수 계산): 발번 문자열(T_STR1)의 마지막 자리를 체크디짓", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_KEY1" ], "out": [ "T_RC", "T_ARG1", - "T_ARG2" + "T_ARG2", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7476,11 +10739,19 @@ const SVC_SCREENS = [ "modnm": "공통", "svc": "CM_STATUS", "desc": "모듈 상태: 파라미터 건수 조회로 DB 연결 확인. */", - "in": [], + "in": [ + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1", + "T_STR1" + ], "out": [ "T_COUNT", "T_RC", - "T_STATUS" + "T_STATUS", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7492,13 +10763,18 @@ const SVC_SCREENS = [ "T_STR1", "T_STR2", "T_KEY1", - "T_ARG1" + "T_ARG1", + "T_BIZDATE", + "T_AMOUNT" ], "out": [ "T_RC", "T_ARG2", "T_STR4", - "T_ARG1" + "T_ARG1", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7507,13 +10783,20 @@ const SVC_SCREENS = [ "svc": "CM_STR_TRIM", "desc": "문자열 정규화(순수 계산): 입력(T_STR1)의 양끝 공백/탭을 제거하고, 내부의", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_STR4", "T_ARG1", "T_ARG2", - "T_ARG3" + "T_ARG3", + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7522,12 +10805,18 @@ const SVC_SCREENS = [ "svc": "CM_WEEKDAY", "desc": "* CM_WEEKDAY.pgc - cm 모듈 서비스 CM_WEEKDAY (one service = one file).", "in": [ - "T_STR1" + "T_STR1", + "T_BIZDATE", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_ARG1", "T_STR3", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG7", + "T_ARG6" ] }, { @@ -7536,11 +10825,19 @@ const SVC_SCREENS = [ "svc": "LG_ACCRUEDPAY", "desc": "미지급비용 계상: 미지급수수료비용(D)/미지급금(C)을 균형 계상.", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", - "T_AMOUNT" + "T_MERCHANT" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7549,12 +10846,21 @@ const SVC_SCREENS = [ "svc": "LG_ACCRUEDRECV", "desc": "미수수익 계상: 현재 RECV 잔액을 참조해 미수수익(D)/이자수익(C)을 계상.", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", + "T_MERCHANT", "T_COUNT" ], "out": [ + "T_RC", "T_NET", - "T_ID1" + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7563,11 +10869,20 @@ const SVC_SCREENS = [ "svc": "LG_ACCRUEREV", "desc": "계상 환입 분개: 기준일(T_BIZDATE) 이전의 미결 '계상' 전표를 커서로 찾아", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", - "T_ID2" + "T_ID2", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7576,13 +10891,22 @@ const SVC_SCREENS = [ "svc": "LG_ACCTGROUP", "desc": "계정그룹별 집계: 계정과목 접두어(prefix)로 묶어 그룹별 차대 합계를 순회.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", "T_AMT2", "T_AMT3", - "T_DELTA" + "T_DELTA", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7591,15 +10915,24 @@ const SVC_SCREENS = [ "svc": "LG_ACCTHIST", "desc": "계정 잔액 이력 조회: [T_BIZDATE ~ T_STR1] 구간의 계정잔액(lg_account_bal)", "in": [ - "T_KEY1", + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", + "T_MERCHANT", + "T_KEY1", "T_STR1" ], "out": [ + "T_RC", "T_COUNT", "T_AMT2", "T_AMT3", - "T_AMT4" + "T_AMT4", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7608,16 +10941,25 @@ const SVC_SCREENS = [ "svc": "LG_ACCTLEDGER", "desc": "계정과목별 원장조회: 지정 계정/영업일의 라인을 커서로 훑어 차변/대변", "in": [ - "T_KEY1", - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT", + "T_KEY1" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", "T_AMT2", "T_AMT3", "T_DELTA", - "T_ID1" + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7626,14 +10968,23 @@ const SVC_SCREENS = [ "svc": "LG_ACCTPIVOT", "desc": "계정x일자 피벗 잔액: 기간 내 일자별 순액을 누적해 누적잔액과 최고점을 산출.", "in": [ - "T_KEY1", + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", + "T_MERCHANT", + "T_KEY1", "T_STR1" ], "out": [ + "T_RC", "T_COUNT", "T_NET", - "T_AMT2" + "T_AMT2", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7642,15 +10993,24 @@ const SVC_SCREENS = [ "svc": "LG_ACCTRANGE", "desc": "계정과목 기간별 원장집계: [T_BIZDATE ~ T_STR1] 구간의 차변/대변 합계와", "in": [ - "T_KEY1", + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", + "T_MERCHANT", + "T_KEY1", "T_STR1" ], "out": [ + "T_RC", "T_AMT2", "T_AMT3", "T_COUNT", - "T_DELTA" + "T_DELTA", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7659,11 +11019,20 @@ const SVC_SCREENS = [ "svc": "LG_ACCTSUM", "desc": "계정 잔액 재집계(dbio) 후 잔액 반환.", "in": [ - "T_KEY1", - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT", + "T_KEY1" ], "out": [ - "T_NET" + "T_RC", + "T_NET", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7672,12 +11041,21 @@ const SVC_SCREENS = [ "svc": "LG_ACCTTOPN", "desc": "상위 N 계정 잔액: 계정별 순액 절대값 내림차순으로 상위 N개를 훑어 1위를 반환.", "in": [ - "T_COUNT", - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT", + "T_COUNT" ], "out": [ + "T_RC", "T_COUNT", - "T_NET" + "T_NET", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7686,15 +11064,22 @@ const SVC_SCREENS = [ "svc": "LG_ACQPOST", "desc": "매입 전기: 차변 RECV=amount, 대변 MERCH_PAY=net + FEE_INC=fee.", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", "T_MERCHANT", - "T_AMOUNT", "T_FEE", "T_NET", "T_PURCHASE_ID" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7703,14 +11088,21 @@ const SVC_SCREENS = [ "svc": "LG_ADJUST", "desc": "조정 전표: 부호에 따라 차변/대변 단일 분개.", "in": [ - "T_KEY1", + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", "T_MERCHANT", - "T_DELTA", + "T_KEY1", "T_PURCHASE_ID" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7719,13 +11111,22 @@ const SVC_SCREENS = [ "svc": "LG_ALLOWANCE", "desc": "대손충당금 설정: RECV 잔액의 일정률을 대손상각비(D)/대손충당금(C)으로 설정.", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", + "T_MERCHANT", "T_COUNT" ], "out": [ + "T_RC", "T_NET", "T_ID1", - "T_AMT2" + "T_AMT2", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7734,13 +11135,22 @@ const SVC_SCREENS = [ "svc": "LG_ALLOWRECALC", "desc": "충당금 재산정: 필요 충당금(RECV x 율)과 현재 충당금 잔액 차이를 조정 전기.", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", + "T_MERCHANT", "T_COUNT" ], "out": [ + "T_RC", "T_DELTA", "T_ID1", - "T_AMT2" + "T_AMT2", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7749,10 +11159,19 @@ const SVC_SCREENS = [ "svc": "LG_APPLY", "desc": "전표 전기확정(P): 라인들의 계정잔액을 잔액테이블에 반영.", "in": [ + "T_AMOUNT", "T_ID1", - "T_BIZDATE" + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], - "out": [] + "out": [ + "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" + ] }, { "mod": "lg", @@ -7760,11 +11179,20 @@ const SVC_SCREENS = [ "svc": "LG_APPRDELEG", "desc": "승인 위임: 작성(D) 전표의 승인 권한을 대리인에게 위임(메모 누적).", "in": [ + "T_AMOUNT", "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT", "T_STR1" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7773,11 +11201,20 @@ const SVC_SCREENS = [ "svc": "LG_APPRHIST", "desc": "승인 이력 조회: 당일 전기(P) 완료된 전표 수와 차변 총액을 집계.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", - "T_AMT2" + "T_AMT2", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7786,11 +11223,20 @@ const SVC_SCREENS = [ "svc": "LG_APPROVE", "desc": "전표 승인 워크플로: 작성(D) 전표만 대상으로 차변합=대변합 균형을 검증한 뒤", "in": [ - "T_ID1" + "T_ID1", + "T_AMOUNT", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_DELTA", - "T_AMT2" + "T_AMT2", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7798,13 +11244,24 @@ const SVC_SCREENS = [ "modnm": "원장", "svc": "LG_APPRQUEUE", "desc": "전표 승인 대기 큐 조회: 미결(D) 전표의 대기 건수와 전표 ID 범위(최소/최대),", - "in": [], + "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" + ], "out": [ + "T_RC", "T_COUNT", "T_ID1", "T_ID2", "T_ARG1", - "T_ARG2" + "T_ARG2", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7813,12 +11270,21 @@ const SVC_SCREENS = [ "svc": "LG_AUDITSCAN", "desc": "원장 감사 스캔: 취소(X) 전표인데 라인이 여전히 posted 인 불일치와 고아 라인을 탐지.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", - "T_COUNT" + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7828,12 +11294,19 @@ const SVC_SCREENS = [ "desc": "대손 처리 분개: 가맹점(T_MERCHANT) 채권 T_AMOUNT 를 상각 —", "in": [ "T_AMOUNT", - "T_MERCHANT", + "T_ID1", + "T_DELTA", "T_BIZDATE", + "T_MERCHANT", "T_REASON" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7843,12 +11316,19 @@ const SVC_SCREENS = [ "desc": "대손 회수(환입) 분개: 상각했던 채권이 회수되면 D:CASH / C:BADDEBT_RCV 로", "in": [ "T_AMOUNT", - "T_MERCHANT", - "T_BIZDATE" + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_AMT2", - "T_ID1" + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7857,13 +11337,22 @@ const SVC_SCREENS = [ "svc": "LG_BALCERT", "desc": "잔액증명서 발급: 계정 차변/대변을 재계산해 계정잔액에 upsert 하고 잔액을 증명.", "in": [ - "T_KEY1", - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT", + "T_KEY1" ], "out": [ + "T_RC", "T_AMT2", "T_AMT3", - "T_NET" + "T_NET", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7872,12 +11361,20 @@ const SVC_SCREENS = [ "svc": "LG_BALCHK", "desc": "전표 단위 대차 균형 점검 (차변합 vs 대변합).", "in": [ - "T_ID1" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_AMT2", "T_AMT3", - "T_RC" + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7886,13 +11383,22 @@ const SVC_SCREENS = [ "svc": "LG_BALPROOF", "desc": "잔액 증명: 계정잔액 테이블(lg_account_bal)의 기록 잔액과 원장 라인에서", "in": [ - "T_KEY1", - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT", + "T_KEY1" ], "out": [ + "T_RC", "T_AMT2", "T_AMT3", - "T_DELTA" + "T_DELTA", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7901,11 +11407,20 @@ const SVC_SCREENS = [ "svc": "LG_BATCHAPPROVE", "desc": "일괄 승인: 당일 작성(D) 전표를 순회하며 차대 균형이 맞는 것만 전기(P) 승인.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", - "T_ARG1" + "T_ARG1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7915,14 +11430,22 @@ const SVC_SCREENS = [ "desc": "고액 분개 조회: 임계금액(T_AMOUNT, 기본 1000만) 이상 라인의 건수·합계와", "in": [ "T_AMOUNT", - "T_BIZDATE" + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", "T_GROSS", "T_ID1", "T_ID2", - "T_AMT2" + "T_AMT2", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7931,14 +11454,21 @@ const SVC_SCREENS = [ "svc": "LG_BLACKADJUST", "desc": "흑색 정정분개: 과소계상분을 양수(흑색) 분개 쌍으로 추가 보정.", "in": [ - "T_KEY1", - "T_BIZDATE", + "T_AMOUNT", "T_ID1", - "T_DELTA" + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT", + "T_KEY1" ], "out": [ + "T_RC", "T_ID2", - "T_DELTA" + "T_DELTA", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7949,12 +11479,18 @@ const SVC_SCREENS = [ "in": [ "T_ID1", "T_AMOUNT", + "T_DELTA", "T_BIZDATE", "T_MERCHANT" ], "out": [ + "T_RC", "T_ID2", - "T_ARG1" + "T_ARG1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7963,12 +11499,21 @@ const SVC_SCREENS = [ "svc": "LG_BSSUMMARY", "desc": "재무상태 요약: 자산(RECV/CASH/PREPAID) - 부채(MERCH_PAY/UNEARNED_REV) = 자본.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_AMT2", "T_AMT3", - "T_NET" + "T_NET", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7977,10 +11522,19 @@ const SVC_SCREENS = [ "svc": "LG_CARRY", "desc": "이월: 당일 계정잔액을 익영업일 개시잔액으로 이월(upsert).", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ - "T_COUNT" + "T_RC", + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -7989,14 +11543,22 @@ const SVC_SCREENS = [ "svc": "LG_CLOSECHK", "desc": "결산 체크리스트: 당일 (1)미결 전표 (2)불균형 전표 (3)0원 라인", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", "T_ARG3", "T_ARG4", - "T_RC" + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8005,13 +11567,21 @@ const SVC_SCREENS = [ "svc": "LG_CLOSELIST", "desc": "결산 체크리스트: 미결전표/시산표 불균형/무분개 전표를 집계해 마감 준비도를 판정.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", "T_ARG3", - "T_RC" + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8020,10 +11590,19 @@ const SVC_SCREENS = [ "svc": "LG_CLOSE_LINK", "desc": "마감연계: 전표 라인 closed + 전표 상태 S(마감연계).", "in": [ - "T_ID1" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ - "T_COUNT" + "T_RC", + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8032,10 +11611,18 @@ const SVC_SCREENS = [ "svc": "LG_CONFIRM", "desc": "전표 확정: 대차 균형이면 P로 확정, 아니면 실패.", "in": [ - "T_ID1" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ - "T_RC" + "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8044,13 +11631,21 @@ const SVC_SCREENS = [ "svc": "LG_CORPLEDGER", "desc": "법인(가맹점)별 원장 집계: 가맹점 원장 라인의 차변/대변 잔액을 산출.", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_AMT2", "T_AMT3", - "T_NET" + "T_NET", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8059,11 +11654,20 @@ const SVC_SCREENS = [ "svc": "LG_CORPSUM", "desc": "법인별 순액 집계: 가맹점별 (차변-대변) 순액의 절대값 합과 법인 수를 순회.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", - "T_NET" + "T_NET", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8072,10 +11676,19 @@ const SVC_SCREENS = [ "svc": "LG_CORRECT", "desc": "원장 라인 금액 정정.", "in": [ + "T_AMOUNT", "T_ID1", - "T_AMOUNT" + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], - "out": [] + "out": [ + "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" + ] }, { "mod": "lg", @@ -8083,14 +11696,21 @@ const SVC_SCREENS = [ "svc": "LG_CORRPAIR", "desc": "정정 짝분개: 잘못 분류된 계정을 올바른 계정으로 재분류(D 신계정/C 구계정).", "in": [ - "T_KEY1", - "T_KEY2", + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", "T_MERCHANT", - "T_AMOUNT" + "T_KEY1", + "T_KEY2" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8101,11 +11721,18 @@ const SVC_SCREENS = [ "in": [ "T_ID1", "T_AMOUNT", - "T_KEY1", + "T_DELTA", "T_BIZDATE", - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], - "out": [] + "out": [ + "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" + ] }, { "mod": "lg", @@ -8113,10 +11740,19 @@ const SVC_SCREENS = [ "svc": "LG_DAILYSUM", "desc": "당일 계정별 잔액 일괄 재집계(전 계정 upsert).", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ - "T_COUNT" + "T_RC", + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8125,11 +11761,20 @@ const SVC_SCREENS = [ "svc": "LG_DAYCLOSE", "desc": "일마감: 당일 균형(차변합=대변합) 미결 전표를 일괄 P 승격한 뒤, 당일 사용된", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_ARG1", - "T_COUNT" + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8140,11 +11785,18 @@ const SVC_SCREENS = [ "in": [ "T_ID1", "T_AMOUNT", - "T_KEY1", + "T_DELTA", "T_BIZDATE", - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], - "out": [] + "out": [ + "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" + ] }, { "mod": "lg", @@ -8152,14 +11804,23 @@ const SVC_SCREENS = [ "svc": "LG_DEPTLEDGER", "desc": "부문(전표유형)별 원장 조회: 유형별 차변/대변 합계와 분개 건수를 집계.", "in": [ - "T_KEY1", - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT", + "T_KEY1" ], "out": [ + "T_RC", "T_AMT2", "T_AMT3", "T_DELTA", - "T_COUNT" + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8168,12 +11829,21 @@ const SVC_SCREENS = [ "svc": "LG_DEPTPL", "desc": "부문별 손익 조회: 당일 매입을 업종/부문(category)별로 묶어 수수료 손익을", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", "T_GROSS", - "T_AMT2" + "T_AMT2", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8182,15 +11852,24 @@ const SVC_SCREENS = [ "svc": "LG_DEPTRANGE", "desc": "부문 기간 금액범위: 전표유형의 기간 내 분개 최소/최대 금액과 건수를 조회.", "in": [ - "T_KEY1", + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", + "T_MERCHANT", + "T_KEY1", "T_STR1" ], "out": [ + "T_RC", "T_AMT2", "T_AMT3", "T_COUNT", - "T_DELTA" + "T_DELTA", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8199,11 +11878,20 @@ const SVC_SCREENS = [ "svc": "LG_DEPTSUM", "desc": "부문별 차변 합계 최대 유형 탐색: 전표유형별 차변합을 순회해 최대 부문을 선별.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_AMT2", - "T_AMT3" + "T_AMT3", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8212,13 +11900,21 @@ const SVC_SCREENS = [ "svc": "LG_DIFFCHK", "desc": "당일 전체 대차 차액 점검(총차변-총대변).", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_AMT2", "T_AMT3", "T_DELTA", - "T_RC" + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8227,15 +11923,22 @@ const SVC_SCREENS = [ "svc": "LG_DOUBLE", "desc": "신규 전표 즉시 전기(posted): 차변/대변 + 양 계정잔액 반영.", "in": [ - "T_KEY1", - "T_KEY2", + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", "T_MERCHANT", - "T_AMOUNT", + "T_KEY1", + "T_KEY2", "T_PURCHASE_ID" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8244,13 +11947,22 @@ const SVC_SCREENS = [ "svc": "LG_DUPSCAN", "desc": "중복 분개 스캔: 당일 동일 (전표, 계정, 차대, 금액) 조합이 2건 이상인 중복", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", "T_ARG1", "T_ID1", - "T_ARG2" + "T_ARG2", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8259,12 +11971,21 @@ const SVC_SCREENS = [ "svc": "LG_EQUITYMOVE", "desc": "자본 변동: 기간 내 이익잉여금(RETAINED) 대변순액 변동을 집계.", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", + "T_MERCHANT", "T_STR1" ], "out": [ + "T_RC", "T_NET", - "T_COUNT" + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8273,11 +11994,20 @@ const SVC_SCREENS = [ "svc": "LG_EXPENSEROLL", "desc": "비용 롤업: 마감일까지 비용계정(_EXP)별 차변순액을 순회해 총비용을 집계.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", - "T_NET" + "T_NET", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8286,11 +12016,20 @@ const SVC_SCREENS = [ "svc": "LG_FEEACCRUE", "desc": "수수료 미수 계상: 대사완료(M)했지만 아직 미정산인 매입의 수수료를", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_FEE", - "T_ID1" + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8299,13 +12038,21 @@ const SVC_SCREENS = [ "svc": "LG_FEEPOST", "desc": "수수료수익 전기: 차변 RECV=fee, 대변 FEE_INC=fee.", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", "T_MERCHANT", "T_FEE", "T_PURCHASE_ID" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8314,12 +12061,21 @@ const SVC_SCREENS = [ "svc": "LG_FEERECOG", "desc": "수수료 수익 인식: 당일 정산완료(S) 매입 수수료 합계를", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_FEE", "T_ID1", - "T_COUNT" + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8329,13 +12085,22 @@ const SVC_SCREENS = [ "desc": "수수료 부문 배부: 당일 FEE_INC 대변 합계를 배부율(T_ARG1 bps)로 부문 A/B 에", "in": [ "T_ARG1", - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_GROSS", "T_ID1", "T_AMT2", - "T_AMT3" + "T_AMT3", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8344,12 +12109,21 @@ const SVC_SCREENS = [ "svc": "LG_HALFCLOSE", "desc": "반기 결산 분개: 반기 FEE_INC 순액을 이익잉여금(RETAINED)으로 균형 대체.", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", + "T_MERCHANT", "T_STR1" ], "out": [ + "T_RC", "T_NET", - "T_ID1" + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8358,11 +12132,20 @@ const SVC_SCREENS = [ "svc": "LG_INCOMEROLL", "desc": "수익 롤업: 마감일까지 수익계정(_INC)별 대변순액을 순회해 총수익을 집계.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", - "T_NET" + "T_NET", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8371,9 +12154,19 @@ const SVC_SCREENS = [ "svc": "LG_INQUIRY", "desc": "전표 상태 조회(dbio).", "in": [ - "T_ID1" + "T_ID1", + "T_AMOUNT", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], - "out": [] + "out": [ + "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" + ] }, { "mod": "lg", @@ -8381,10 +12174,19 @@ const SVC_SCREENS = [ "svc": "LG_JOURNAL", "desc": "당일 분개(원장 라인) 건수 조회.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ - "T_COUNT" + "T_RC", + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8393,12 +12195,21 @@ const SVC_SCREENS = [ "svc": "LG_LEDGSETTLE", "desc": "원장-정산 대사: base 원장(ledger) SETTLE 합계와 원장라인 지급 합계를 대조.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_AMT2", "T_AMT3", - "T_DELTA" + "T_DELTA", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8407,13 +12218,22 @@ const SVC_SCREENS = [ "svc": "LG_LINECOUNT", "desc": "원장 라인 집계: 당일 라인의 posted/closed 상태별 건수, 관련 전표 수,", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", "T_COUNT", - "T_ARG3" + "T_ARG3", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8422,11 +12242,20 @@ const SVC_SCREENS = [ "svc": "LG_MASKINQ", "desc": "원장 마스킹 조회: 라인 ID(T_ID1)의 원장 라인을 가맹점 ID 마스킹(앞 2자리", "in": [ - "T_ID1" + "T_ID1", + "T_AMOUNT", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_ID2", - "T_AMOUNT" + "T_AMOUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8435,13 +12264,22 @@ const SVC_SCREENS = [ "svc": "LG_MEMOAUDIT", "desc": "전표 메모 감사: 당일 전기(P) 전표 중 메모가 비어 있는 건수와 표본 전표 ID,", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", "T_ARG1", "T_ID1", - "T_ARG2" + "T_ARG2", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8450,10 +12288,19 @@ const SVC_SCREENS = [ "svc": "LG_MERCHBAL", "desc": "가맹점 잔액 재집계(dbio).", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], - "out": [] + "out": [ + "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" + ] }, { "mod": "lg", @@ -8461,13 +12308,21 @@ const SVC_SCREENS = [ "svc": "LG_MERCHCERT", "desc": "가맹점 잔액증명: 가맹점잔액 기록치와 라인 재계산치를 대조해 증명(OK/NG).", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_AMT2", "T_AMT3", - "T_DELTA" + "T_DELTA", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8476,14 +12331,22 @@ const SVC_SCREENS = [ "svc": "LG_MERCHPROOF", "desc": "가맹점 잔액 증명: 가맹점 잔액 테이블(lg_merch_bal)의 차변/대변 누계와", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_AMT2", "T_AMT3", "T_AMT4", - "T_ARG1" + "T_ARG1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8492,12 +12355,21 @@ const SVC_SCREENS = [ "svc": "LG_MONCLOSEJE", "desc": "월차 결산 분개: 당월 RECV 순증가를 미수수익(RECV_ACCRUED)으로 계상.", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", + "T_MERCHANT", "T_STR1" ], "out": [ + "T_RC", "T_NET", - "T_ID1" + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8506,12 +12378,21 @@ const SVC_SCREENS = [ "svc": "LG_MONTHCLOSE", "desc": "월차 결산 분개: [T_BIZDATE=월초 ~ T_STR1=월말] 구간 FEE_INC 순액(대변-차변)을", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", + "T_MERCHANT", "T_STR1" ], "out": [ + "T_RC", "T_AMT2", - "T_ID1" + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8520,12 +12401,21 @@ const SVC_SCREENS = [ "svc": "LG_MONTHLYSUM", "desc": "월 계정과목별 차/대 집계 건수 조회(YYYY-MM).", "in": [ - "T_STR1", - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT", + "T_STR1" ], "out": [ + "T_RC", "T_COUNT", - "T_GROSS" + "T_GROSS", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8534,12 +12424,21 @@ const SVC_SCREENS = [ "svc": "LG_NETPOS", "desc": "순포지션 조회: 당일 채권(RECV) 잔액과 가맹점미지급(MERCH_PAY) 잔액을", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_AMT2", "T_AMT3", - "T_NET" + "T_NET", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8548,13 +12447,21 @@ const SVC_SCREENS = [ "svc": "LG_PARTREVERSE", "desc": "부분 역분개: 원전표에서 특정 계정 라인만 차대를 뒤집어 신규 전표로 복사.", "in": [ + "T_AMOUNT", "T_ID1", - "T_KEY1", - "T_BIZDATE" + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT", + "T_KEY1" ], "out": [ + "T_RC", "T_ID2", - "T_COUNT" + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8563,13 +12470,22 @@ const SVC_SCREENS = [ "svc": "LG_PAYACCRUE", "desc": "미지급 계상 분개: 당일 정산 순액 합계와 원장 MERCH_PAY 대변 합계의 갭을", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_NET", "T_AMT3", "T_DELTA", - "T_ID1" + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8578,12 +12494,21 @@ const SVC_SCREENS = [ "svc": "LG_PAYMATCH", "desc": "지급 매칭: 원장 지급(MERCH_PAY 대변) 건수와 정산 건수를 대조해 매칭율 판정.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", - "T_DELTA" + "T_DELTA", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8592,14 +12517,22 @@ const SVC_SCREENS = [ "svc": "LG_PAYRECON", "desc": "원장-지급 대사: 가맹점(T_MERCHANT)의 원장 라인 순액(대변-차변)과 정산", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_AMT2", "T_NET", "T_DELTA", - "T_COUNT" + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8607,13 +12540,24 @@ const SVC_SCREENS = [ "modnm": "원장", "svc": "LG_PENDAGE", "desc": "미결전표 경과일 집계: 작성(D) 전표를 경과일 구간(0-1일 / 2-7일 / 8일+)별로", - "in": [], + "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" + ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", "T_ARG3", "T_ARG4", - "T_COUNT" + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8622,10 +12566,19 @@ const SVC_SCREENS = [ "svc": "LG_PENDCLEAR", "desc": "미결전표 정리: 기준일 이전 작성(D) 전표를 취소(X)하고 라인을 마감 처리.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ - "T_COUNT" + "T_RC", + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8634,11 +12587,20 @@ const SVC_SCREENS = [ "svc": "LG_PENDESCAL", "desc": "미결 에스컬레이션: 기준일보다 오래 방치된 작성(D) 전표 건수를 집계.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", - "T_AMT2" + "T_AMT2", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8646,11 +12608,22 @@ const SVC_SCREENS = [ "modnm": "원장", "svc": "LG_PENDLIST", "desc": "미결전표 관리(목록 집계): 작성(D) 상태 전표의 건수, 연계 라인 총차변액,", - "in": [], + "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" + ], "out": [ + "T_RC", "T_COUNT", "T_ID1", - "T_AMT2" + "T_AMT2", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8659,11 +12632,20 @@ const SVC_SCREENS = [ "svc": "LG_PENDROUTE", "desc": "미결전표 배정: 특정 작성(D) 전표의 메모에 담당자를 기록해 배정.", "in": [ + "T_AMOUNT", "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT", "T_STR1" ], "out": [ - "T_COUNT" + "T_RC", + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8672,11 +12654,20 @@ const SVC_SCREENS = [ "svc": "LG_PENDSTAT", "desc": "미결 상태별 집계: 당일 전표를 상태(D/P/M/S/X)별로 순회 집계.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", - "T_ARG1" + "T_ARG1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8685,12 +12676,21 @@ const SVC_SCREENS = [ "svc": "LG_PERIODTB", "desc": "기간별 시산표: [T_BIZDATE ~ T_STR1] 구간을 계정별 GROUP BY 커서로 집계해", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", + "T_MERCHANT", "T_STR1" ], "out": [ + "T_RC", "T_COUNT", - "T_DELTA" + "T_DELTA", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8699,11 +12699,20 @@ const SVC_SCREENS = [ "svc": "LG_PLCLOSE", "desc": "손익 대체 분개(일 단위): 당일 손익계정(FEE_INC, BADDEBT_EXP)의 순액을 각각", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", - "T_NET" + "T_NET", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8712,12 +12721,21 @@ const SVC_SCREENS = [ "svc": "LG_PLSUMMARY", "desc": "손익 요약: 당일 수익(_INC)과 비용(_EXP)의 순액을 계산해 순손익을 산출.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_AMT2", "T_AMT3", - "T_NET" + "T_NET", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8726,15 +12744,22 @@ const SVC_SCREENS = [ "svc": "LG_POST", "desc": "전표 작성(draft) + 차변/대변 균형 분개 2줄.", "in": [ - "T_KEY1", - "T_KEY2", + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", "T_MERCHANT", - "T_PURCHASE_ID", - "T_AMOUNT" + "T_KEY1", + "T_KEY2", + "T_PURCHASE_ID" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8743,13 +12768,21 @@ const SVC_SCREENS = [ "svc": "LG_PREPAIDAMORT", "desc": "선급 상각: 선급비용 총액을 개월(N)로 나눠 1개월분을 비용(D)/선급(C) 상각.", "in": [ - "T_BIZDATE", "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT", "T_COUNT" ], "out": [ + "T_RC", "T_ID1", - "T_NET" + "T_NET", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8758,11 +12791,19 @@ const SVC_SCREENS = [ "svc": "LG_PREPAIDPOST", "desc": "선급비용 계상: 선급비용(D)/현금(C)으로 지출 시점 자산 계상.", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", - "T_AMOUNT" + "T_MERCHANT" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8772,11 +12813,21 @@ const SVC_SCREENS = [ "desc": "분기 결산 분개: 연도(T_ARG1)/분기(T_ARG2)로 분기 구간을 산출해 그 구간의", "in": [ "T_ARG1", - "T_ARG2" + "T_ARG2", + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_ID1", - "T_AMT2" + "T_AMT2", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8785,12 +12836,21 @@ const SVC_SCREENS = [ "svc": "LG_QTRCLOSEJE", "desc": "분기 결산 분개: 당분기 대손비용 발생분을 미지급(ACCRUED_PAY)으로 계상.", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", + "T_MERCHANT", "T_STR1" ], "out": [ + "T_RC", "T_NET", - "T_ID1" + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8799,10 +12859,20 @@ const SVC_SCREENS = [ "svc": "LG_RECALC", "desc": "계정 잔액 원장 라인 기준 재계산(dbio).", "in": [ - "T_KEY1", - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT", + "T_KEY1" ], - "out": [] + "out": [ + "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" + ] }, { "mod": "lg", @@ -8810,12 +12880,19 @@ const SVC_SCREENS = [ "svc": "LG_RECOVERY", "desc": "상각채권 회수: 제각 채권 회수를 현금(D)/대손충당금환입(C)으로 인식.", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", - "T_MERCHANT", - "T_AMOUNT" + "T_MERCHANT" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8824,12 +12901,21 @@ const SVC_SCREENS = [ "svc": "LG_RECVACCRUE", "desc": "미수 계상 분개: 접수(A) 상태 매입 총액을 D:RECV_ACCR / C:SUSP 로 미결 계상", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", "T_ID1", - "T_AMT2" + "T_AMT2", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8838,14 +12924,21 @@ const SVC_SCREENS = [ "svc": "LG_REDADJUST", "desc": "적색 정정분개: 과대계상된 차변을 음수(적색) 분개 쌍으로 취소 감액.", "in": [ - "T_KEY1", - "T_BIZDATE", + "T_AMOUNT", "T_ID1", - "T_DELTA" + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT", + "T_KEY1" ], "out": [ + "T_RC", "T_ID2", - "T_DELTA" + "T_DELTA", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8854,12 +12947,21 @@ const SVC_SCREENS = [ "svc": "LG_REDBLACKNET", "desc": "적흑 상계: 원전표에 연결된 적색/흑색 정정 금액을 합산해 순정정액을 반환.", "in": [ - "T_ID1" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_AMT2", "T_AMT3", - "T_NET" + "T_NET", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8869,11 +12971,19 @@ const SVC_SCREENS = [ "desc": "적색 정정 분개: 원전표(T_ID1)의 모든 라인을 차/대변 반전(INSERT..SELECT CASE)", "in": [ "T_ID1", - "T_BIZDATE" + "T_AMOUNT", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_ID2", - "T_COUNT" + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8883,9 +12993,19 @@ const SVC_SCREENS = [ "desc": "전표 반려 워크플로: 작성(D) 전표를 X(취소) 처리하고 반려 사유(T_REASON)를", "in": [ "T_ID1", + "T_AMOUNT", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT", "T_REASON" ], - "out": [] + "out": [ + "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" + ] }, { "mod": "lg", @@ -8893,10 +13013,19 @@ const SVC_SCREENS = [ "svc": "LG_REOPEN", "desc": "취소 전표 재개: X(취소) 전표를 D(작성)로 되돌리고 closed 라인을 posted 로", "in": [ - "T_ID1" + "T_ID1", + "T_AMOUNT", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ - "T_COUNT" + "T_RC", + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8905,12 +13034,20 @@ const SVC_SCREENS = [ "svc": "LG_REPOST", "desc": "재전기: 취소(X)된 전표의 라인을 방향 그대로 신규 전기(P) 전표로 복제.", "in": [ + "T_AMOUNT", "T_ID1", - "T_BIZDATE" + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_ID2", - "T_COUNT" + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8919,12 +13056,19 @@ const SVC_SCREENS = [ "svc": "LG_REVACCRUE", "desc": "수익 발생 계상: 발생주의 수익을 미수수익(D)/수수료수익(C)으로 균형 계상.", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", - "T_MERCHANT", - "T_AMOUNT" + "T_MERCHANT" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8933,12 +13077,19 @@ const SVC_SCREENS = [ "svc": "LG_REVDEFER", "desc": "수익 이연: 미실현 수익을 수수료수익(D)/선수수익(C)으로 이연 대체.", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", - "T_MERCHANT", - "T_AMOUNT" + "T_MERCHANT" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8947,12 +13098,20 @@ const SVC_SCREENS = [ "svc": "LG_REVERSE", "desc": "역분개 전표: 원전표 라인의 차/대를 뒤집어 신규 전표에 복사.", "in": [ + "T_AMOUNT", "T_ID1", - "T_BIZDATE" + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_ID2", - "T_COUNT" + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8961,12 +13120,19 @@ const SVC_SCREENS = [ "svc": "LG_REVRELEASE", "desc": "이연수익 환입: 선수수익(D)을 수수료수익(C)으로 기간 경과분 환입 인식.", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", - "T_MERCHANT", - "T_AMOUNT" + "T_MERCHANT" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8975,10 +13141,19 @@ const SVC_SCREENS = [ "svc": "LG_ROLLFWD", "desc": "전 계정 잔액 익일 이월(개시잔액 롤포워드).", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ - "T_COUNT" + "T_RC", + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -8987,11 +13162,20 @@ const SVC_SCREENS = [ "svc": "LG_ROLLTB", "desc": "누적 시산표: 마감일까지 계정별 누계 차대를 순회해 시산표 upsert 및 총차액 검증.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", - "T_DELTA" + "T_DELTA", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9000,12 +13184,21 @@ const SVC_SCREENS = [ "svc": "LG_SEQAUDIT", "desc": "전표 채번 감사: 당일 전표번호 min/max/건수로 채번 누락(gap)을 검출.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", "T_ARG1", - "T_DELTA" + "T_DELTA", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9014,12 +13207,21 @@ const SVC_SCREENS = [ "svc": "LG_SETTLEPAYGAP", "desc": "정산-지급 차이: 정산 net 합계와 원장 지급(MERCH_PAY 대변) 합계의 갭을 산출.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_AMT2", "T_AMT3", - "T_DELTA" + "T_DELTA", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9028,13 +13230,21 @@ const SVC_SCREENS = [ "svc": "LG_SETTLEPOST", "desc": "정산 전기: 차변 MERCH_PAY, 대변 CASH (net).", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", "T_MERCHANT", "T_NET", "T_PURCHASE_ID" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9043,13 +13253,22 @@ const SVC_SCREENS = [ "svc": "LG_SETTLERECON", "desc": "원장-정산 대사: 당일 정산(settlement) 순액 합계와 원장 MERCH_PAY 대변", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_NET", "T_AMT3", "T_DELTA", - "T_COUNT" + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9058,14 +13277,23 @@ const SVC_SCREENS = [ "svc": "LG_SPANTB", "desc": "기간 시산표: 계정의 기간 차변/대변 합계와 차액을 산출해 시산표에 upsert.", "in": [ - "T_KEY1", + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", + "T_MERCHANT", + "T_KEY1", "T_STR1" ], "out": [ + "T_RC", "T_AMT2", "T_AMT3", - "T_DELTA" + "T_DELTA", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9074,11 +13302,20 @@ const SVC_SCREENS = [ "svc": "LG_STATACCT", "desc": "원장 통계 세분(계정): 당일 사용된 계정과목 수와 잔액 절대값이 가장 큰", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", - "T_AMT2" + "T_AMT2", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9087,16 +13324,25 @@ const SVC_SCREENS = [ "svc": "LG_STATDC", "desc": "원장 통계 세분(차대변): 당일 차변/대변 각각의 건수·합계·최대금액을 구하고", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", "T_AMT2", "T_AMT3", "T_ARG3", "T_ARG4", - "T_AMT4" + "T_AMT4", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9105,13 +13351,22 @@ const SVC_SCREENS = [ "svc": "LG_STATMERCH", "desc": "원장 통계 세분(가맹점): 특정 가맹점의 원장 라인 건수, 최초/최종 영업일,", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", "T_AMT2", "T_AMT3", - "T_DELTA" + "T_DELTA", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9120,11 +13375,20 @@ const SVC_SCREENS = [ "svc": "LG_STATS", "desc": "당일 전표/원장 통계(전표수, 총차변).", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", - "T_GROSS" + "T_GROSS", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9133,12 +13397,21 @@ const SVC_SCREENS = [ "svc": "LG_STATTYPE", "desc": "원장 통계 세분(전표유형): 당일 전표를 유형(voucher_type)별로 집계해", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_ARG1", "T_COUNT", - "T_ARG2" + "T_ARG2", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9148,9 +13421,19 @@ const SVC_SCREENS = [ "desc": "전표 상태 직접 변경(setter).", "in": [ "T_ID1", + "T_AMOUNT", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT", "T_STATUS" ], - "out": [] + "out": [ + "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" + ] }, { "mod": "lg", @@ -9158,12 +13441,21 @@ const SVC_SCREENS = [ "svc": "LG_SUSPENSE", "desc": "가지급(SUSP) 정리 분개: 당일 SUSP 계정 순액(차-대)을 0 으로 만드는 반대", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_DELTA", "T_ID1", - "T_AMT2" + "T_AMT2", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9172,12 +13464,21 @@ const SVC_SCREENS = [ "svc": "LG_TBDIFF", "desc": "시산표 차액 조회: 당일 시산표(lg_trial_bal)에서 불균형(diff<>0) 계정 수,", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", "T_GROSS", - "T_DELTA" + "T_DELTA", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9186,14 +13487,23 @@ const SVC_SCREENS = [ "svc": "LG_THREEWAY", "desc": "원장-정산-매입 3단 대사: 원장 지급대변, 정산 net, 매입 net 을 대조해 차이 산출.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_AMT2", "T_AMT3", "T_AMT4", "T_DELTA", - "T_NET" + "T_NET", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9202,14 +13512,21 @@ const SVC_SCREENS = [ "svc": "LG_TRANSFER", "desc": "계정간 대체: 출금계정 대변, 입금계정 차변(균형).", "in": [ - "T_KEY1", - "T_KEY2", + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", "T_MERCHANT", - "T_AMOUNT" + "T_KEY1", + "T_KEY2" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9218,13 +13535,22 @@ const SVC_SCREENS = [ "svc": "LG_TRIALBAL", "desc": "계정별 시산표 산출 후 lg_trial_bal upsert.", "in": [ - "T_KEY1", - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT", + "T_KEY1" ], "out": [ + "T_RC", "T_AMT2", "T_AMT3", - "T_DELTA" + "T_DELTA", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9233,10 +13559,19 @@ const SVC_SCREENS = [ "svc": "LG_UNAPPROVE", "desc": "승인 취소: 마감(S)되지 않은 전기(P) 전표를 작성(D) 상태로 되돌림.", "in": [ - "T_ID1" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9245,12 +13580,19 @@ const SVC_SCREENS = [ "svc": "LG_UNEARNPOST", "desc": "선수수익 계상: 현금(D)/선수수익(C)으로 선수금 수령 시점 부채 계상.", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", - "T_MERCHANT", - "T_AMOUNT" + "T_MERCHANT" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9259,13 +13601,21 @@ const SVC_SCREENS = [ "svc": "LG_UNEARNRECOG", "desc": "선수수익 인식: 선수수익 총액을 기간(N)으로 나눠 1기분을 수익 인식.", "in": [ - "T_BIZDATE", "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT", "T_COUNT" ], "out": [ + "T_RC", "T_ID1", - "T_NET" + "T_NET", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9274,10 +13624,19 @@ const SVC_SCREENS = [ "svc": "LG_UNSETTLED", "desc": "미마감(status=posted) 원장 라인 건수.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ - "T_COUNT" + "T_RC", + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9286,10 +13645,19 @@ const SVC_SCREENS = [ "svc": "LG_VOIDVOUCHER", "desc": "전표 취소: 마감연계(S) 상태가 아닌 전표를 X 처리하고 소속 라인 전부를", "in": [ - "T_ID1" + "T_ID1", + "T_AMOUNT", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ - "T_COUNT" + "T_RC", + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9299,12 +13667,21 @@ const SVC_SCREENS = [ "desc": "빈 전표 헤더 생성(작성 상태).", "in": [ "T_PURCHASE_ID", - "T_STR2", + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", + "T_MERCHANT", + "T_STR2", "T_MSG" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9313,14 +13690,22 @@ const SVC_SCREENS = [ "svc": "LG_VOUCHTRACE", "desc": "전표 추적: 전표(T_ID1)의 헤더(유형/상태/연계 ID)와 라인 통계(건수, 차변합,", "in": [ - "T_ID1" + "T_ID1", + "T_AMOUNT", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_ID2", "T_COUNT", "T_AMT2", "T_AMT3", - "T_RC" + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9329,11 +13714,20 @@ const SVC_SCREENS = [ "svc": "LG_WEEKCLOSE", "desc": "주차 결산: 지정 주(기간) 내 작성(D) 전표를 일괄 전기(P)로 승격.", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", + "T_MERCHANT", "T_STR1" ], "out": [ - "T_COUNT" + "T_RC", + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9342,12 +13736,19 @@ const SVC_SCREENS = [ "svc": "LG_WRITEOFF", "desc": "대손 상각: 회수불능 채권을 대손충당금(D)/미수금(C)으로 제각.", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", - "T_MERCHANT", - "T_AMOUNT" + "T_MERCHANT" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9356,13 +13757,21 @@ const SVC_SCREENS = [ "svc": "LG_XFERPOST", "desc": "대체 전기: 두 계정 간 대체(D 대상/C 원천) 전기 후 양 계정 잔액을 재계산.", "in": [ - "T_KEY1", - "T_KEY2", + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", - "T_AMOUNT" + "T_MERCHANT", + "T_KEY1", + "T_KEY2" ], "out": [ - "T_ID1" + "T_RC", + "T_ID1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9371,11 +13780,20 @@ const SVC_SCREENS = [ "svc": "LG_YEARCARRY", "desc": "연말 이월: 마감일까지 계정별 누계잔액을 익년 개시일 계정잔액으로 이월(upsert).", "in": [ + "T_AMOUNT", + "T_ID1", + "T_DELTA", "T_BIZDATE", + "T_MERCHANT", "T_STR1" ], "out": [ - "T_COUNT" + "T_RC", + "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9384,11 +13802,20 @@ const SVC_SCREENS = [ "svc": "LG_YEARCLOSE", "desc": "연말 결산 대체: 손익계정(_INC/_EXP) 누계 잔액을 계정별로 RETAINED 로 대체.", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", - "T_NET" + "T_NET", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9397,11 +13824,20 @@ const SVC_SCREENS = [ "svc": "LG_ZEROLINE", "desc": "0원 분개 검출/정리: 당일 금액 0 인 posted 라인을 검출해 closed 처리하고", "in": [ - "T_BIZDATE" + "T_AMOUNT", + "T_ID1", + "T_DELTA", + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", - "T_ARG1" + "T_ARG1", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_ARG6" ] }, { @@ -9410,9 +13846,22 @@ const SVC_SCREENS = [ "svc": "MG_ACK", "desc": "* MG_ACK.pgc - mg 모듈 서비스 MG_ACK (one service = one file).", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ID1" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STATUS" ] }, @@ -9423,14 +13872,23 @@ const SVC_SCREENS = [ "desc": "0220 매입 어드바이스 수신: 응답이 필요 없는 통지 전문이라", "in": [ "T_CHANNEL", - "T_STR3", "T_BIZDATE", - "T_AMOUNT" + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", + "T_STR3" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ID1", - "T_COUNT", - "T_RC" + "T_COUNT" ] }, { @@ -9439,13 +13897,24 @@ const SVC_SCREENS = [ "svc": "MG_AMTCHECK", "desc": "전문 금액/카드 사전검증: 금액 범위(0 < 금액 <= 한도)와", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", "T_AMOUNT", "T_AMT4", "T_STR5" ], "out": [ - "T_AMT4", - "T_RC" + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", + "T_AMT4" ] }, { @@ -9454,15 +13923,25 @@ const SVC_SCREENS = [ "svc": "MG_AUTHADV", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_CHANNEL", + "T_STR1", + "T_STR4", "T_STR3", - "T_BIZDATE", - "T_AMOUNT" + "T_STR5", + "T_AMOUNT", + "T_CHANNEL", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ID1", - "T_ARG1", - "T_RC" + "T_ARG1" ] }, { @@ -9471,12 +13950,24 @@ const SVC_SCREENS = [ "svc": "MG_AUTHADVRSP", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_STR1", + "T_STR4", + "T_STR3", + "T_STR5", + "T_AMOUNT", "T_CHANNEL", "T_BIZDATE", "T_ARG1" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ID1", "T_ID2" ] @@ -9488,11 +13979,21 @@ const SVC_SCREENS = [ "desc": "PAN 앞6/뒤4 만 남기고 '*' 마스킹 (승인요청 로그 저장용). */", "in": [ "T_CHANNEL", - "T_STR3", "T_BIZDATE", - "T_AMOUNT" + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", + "T_STR3" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR2", "T_STR5", "T_ID1", @@ -9507,13 +14008,22 @@ const SVC_SCREENS = [ "in": [ "T_CHANNEL", "T_BIZDATE", - "T_ID1", - "T_AMOUNT" + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", + "T_ID1" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR2", - "T_ID2", - "T_RC" + "T_ID2" ] }, { @@ -9522,11 +14032,23 @@ const SVC_SCREENS = [ "svc": "MG_BALINQ", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_CHANNEL", + "T_STR1", + "T_STR4", "T_STR3", + "T_STR5", + "T_AMOUNT", + "T_CHANNEL", "T_BIZDATE" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR2", "T_STR4", "T_ID1", @@ -9539,10 +14061,23 @@ const SVC_SCREENS = [ "svc": "MG_BITMAP", "desc": "* MG_BITMAP.pgc - mg 모듈 서비스 MG_BITMAP (one service = one file).", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ARG1", "T_ID1" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR6" ] }, @@ -9552,11 +14087,23 @@ const SVC_SCREENS = [ "svc": "MG_BMPMERGE", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_STR1", + "T_STR4", + "T_STR3", + "T_STR5", + "T_AMOUNT", "T_STR6", "T_STR7" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR6", "T_ARG3" ] @@ -9567,10 +14114,22 @@ const SVC_SCREENS = [ "svc": "MG_BMPPARSE", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_STR1", + "T_STR4", + "T_STR3", + "T_STR5", + "T_AMOUNT", "T_STR6" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR3", "T_COUNT" ] @@ -9583,10 +14142,20 @@ const SVC_SCREENS = [ "in": [ "T_CHANNEL", "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ARG1" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ID1", "T_ID2" ] @@ -9598,13 +14167,22 @@ const SVC_SCREENS = [ "desc": "0200 매입요청 전문 처리: MDR(bps) 수수료/정산액을 산출해", "in": [ "T_CHANNEL", - "T_STR3", "T_BIZDATE", + "T_STR1", + "T_MERCHANT", "T_AMOUNT", + "T_STR3", "T_ARG2" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_FEE", "T_NET", "T_ID1" @@ -9616,11 +14194,23 @@ const SVC_SCREENS = [ "svc": "MG_CARDVER", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_CHANNEL", + "T_STR1", + "T_STR4", + "T_STR3", "T_STR5", + "T_AMOUNT", + "T_CHANNEL", "T_BIZDATE" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR2", "T_STR6", "T_COUNT", @@ -9633,10 +14223,21 @@ const SVC_SCREENS = [ "svc": "MG_CHANAUTH", "desc": "채널 인증: UP/SIGNON 상태만 통과. */", "in": [ - "T_CHANNEL" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STATUS" ] }, @@ -9646,10 +14247,21 @@ const SVC_SCREENS = [ "svc": "MG_CHANCLOSE", "desc": "채널 폐쇄: 채널을 DOWN 으로 내리고 세션키를 폐기하되,", "in": [ - "T_CHANNEL" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STATUS", "T_COUNT" ] @@ -9661,10 +14273,20 @@ const SVC_SCREENS = [ "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ "T_CHANNEL", - "T_STR1" + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STATUS", "T_STR5" ] @@ -9676,14 +14298,22 @@ const SVC_SCREENS = [ "desc": "채널 종합 건강점검: 상태(UP 여부) + 당일 오류율(bps) +", "in": [ "T_CHANNEL", - "T_BIZDATE" + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ "T_RC", - "T_STATUS", - "T_ARG3", + "T_MSG", + "T_ARG6", "T_ARG7", - "T_ARG8" + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", + "T_STATUS", + "T_ARG3" ] }, { @@ -9691,12 +14321,25 @@ const SVC_SCREENS = [ "modnm": "전문게이트웨이", "svc": "MG_CHANLIST", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", - "in": [], + "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" + ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR3", "T_ARG2", - "T_ARG3", - "T_RC" + "T_ARG3" ] }, { @@ -9705,10 +14348,21 @@ const SVC_SCREENS = [ "svc": "MG_CHANMAP", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_STR1" + "T_STR1", + "T_STR4", + "T_STR3", + "T_STR5", + "T_AMOUNT" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_CHANNEL", "T_STR5" ] @@ -9719,10 +14373,21 @@ const SVC_SCREENS = [ "svc": "MG_CHANOPEN", "desc": "채널 개통: DOWN 채널만 UP 으로 올리고 sign-on 시각을 기록.", "in": [ - "T_CHANNEL" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STATUS", "T_STR5" ] @@ -9733,10 +14398,21 @@ const SVC_SCREENS = [ "svc": "MG_CHANPING", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_CHANNEL" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STATUS" ] }, @@ -9746,10 +14422,21 @@ const SVC_SCREENS = [ "svc": "MG_CHANRESET", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_CHANNEL" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR5", "T_STATUS" ] @@ -9760,9 +14447,21 @@ const SVC_SCREENS = [ "svc": "MG_CHANSTAT", "desc": "* MG_CHANSTAT.pgc - mg 모듈 서비스 MG_CHANSTAT (one service = one file).", "in": [ - "T_CHANNEL" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STATUS" ] }, @@ -9773,10 +14472,21 @@ const SVC_SCREENS = [ "desc": "채널 절체: 주채널을 DOWN, 예비채널을 UP 으로 한 트랜잭션", "in": [ "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_STR2" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR5", "T_STR2", "T_COUNT" @@ -9788,9 +14498,22 @@ const SVC_SCREENS = [ "svc": "MG_CONFIRM", "desc": "* MG_CONFIRM.pgc - mg 모듈 서비스 MG_CONFIRM (one service = one file).", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ID1" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STATUS" ] }, @@ -9801,12 +14524,21 @@ const SVC_SCREENS = [ "desc": "* MG_CONVERT.pgc - mg 모듈 서비스 MG_CONVERT (one service = one file).", "in": [ "T_CHANNEL", - "T_STR1", - "T_STR3", "T_BIZDATE", - "T_AMOUNT" + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", + "T_STR3" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR1", "T_ID1" ] @@ -9817,10 +14549,21 @@ const SVC_SCREENS = [ "svc": "MG_DECODE", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_STR3" + "T_STR1", + "T_STR4", + "T_STR3", + "T_STR5", + "T_AMOUNT" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR1", "T_STR4", "T_AMOUNT", @@ -9833,8 +14576,22 @@ const SVC_SCREENS = [ "modnm": "전문게이트웨이", "svc": "MG_DEQUEUE", "desc": "* MG_DEQUEUE.pgc - mg 모듈 서비스 MG_DEQUEUE (one service = one file).", - "in": [], + "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" + ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT", "T_ID2", "T_ARG1", @@ -9848,10 +14605,22 @@ const SVC_SCREENS = [ "svc": "MG_DIRECTADPT", "desc": "구간 [s, s+n) 이 전부 십진 숫자인지 검사 (직연동 전문 필드 검증). */", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_STR3" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR1", "T_AMOUNT", "T_CHANNEL", @@ -9864,11 +14633,22 @@ const SVC_SCREENS = [ "svc": "MG_DUPCHK", "desc": "중복전문 검출: 동일 STAN 이 당일 원장에 이미 몇 건인지 세어", "in": [ + "T_CHANNEL", "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ARG1" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT", "T_ARG2" ] @@ -9879,11 +14659,22 @@ const SVC_SCREENS = [ "svc": "MG_DUPPURGE", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_CHANNEL", "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ARG1" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT", "T_ID1" ] @@ -9895,9 +14686,20 @@ const SVC_SCREENS = [ "desc": "* MG_ECHO.pgc - mg 모듈 서비스 MG_ECHO (one service = one file).", "in": [ "T_CHANNEL", - "T_BIZDATE" + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ID1", "T_STATUS" ] @@ -9910,14 +14712,23 @@ const SVC_SCREENS = [ "in": [ "T_STR1", "T_STR4", - "T_CHANNEL", + "T_STR3", + "T_STR5", "T_AMOUNT", + "T_CHANNEL", "T_ARG1" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR3", - "T_COUNT", - "T_RC" + "T_COUNT" ] }, { @@ -9926,10 +14737,22 @@ const SVC_SCREENS = [ "svc": "MG_ERRCLASS", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_STR1", + "T_STR4", + "T_STR3", + "T_STR5", + "T_AMOUNT", "T_STR2" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR5", "T_ARG3" ] @@ -9940,13 +14763,24 @@ const SVC_SCREENS = [ "svc": "MG_ERRESCAL", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_CHANNEL", "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ARG4" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT", - "T_ARG4", - "T_RC" + "T_ARG4" ] }, { @@ -9955,14 +14789,25 @@ const SVC_SCREENS = [ "svc": "MG_ERRLIST", "desc": "오류전문 대기목록 요약: 당일 NACK(N) 전문의 건수, 금액합,", "in": [ - "T_BIZDATE" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT", "T_GROSS", "T_ID1", - "T_ID2", - "T_RC" + "T_ID2" ] }, { @@ -9972,12 +14817,22 @@ const SVC_SCREENS = [ "desc": "* MG_ERRMSG.pgc - mg 모듈 서비스 MG_ERRMSG (one service = one file).", "in": [ "T_CHANNEL", - "T_STR1", - "T_STR3", "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", + "T_STR3", "T_RC" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ID1" ] }, @@ -9987,11 +14842,22 @@ const SVC_SCREENS = [ "svc": "MG_ERRPURGE", "desc": "오류전문 폐기: 재처리를 포기한 당일 NACK(N) 전문 중", "in": [ + "T_CHANNEL", "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_STR2" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT" ] }, @@ -10001,11 +14867,22 @@ const SVC_SCREENS = [ "svc": "MG_ERRREQUEUE", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_CHANNEL", "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ID1" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ID2", "T_ARG1" ] @@ -10016,10 +14893,22 @@ const SVC_SCREENS = [ "svc": "MG_ERRRETRY", "desc": "오류전문 단건 재처리: NACK(N) 된 전문을 rc=00/R 로 리셋하고", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ID1" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ID2" ] }, @@ -10029,13 +14918,24 @@ const SVC_SCREENS = [ "svc": "MG_FEP", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_STR3" + "T_STR1", + "T_STR4", + "T_STR3", + "T_STR5", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR3", "T_COUNT", - "T_ARG2", - "T_RC" + "T_ARG2" ] }, { @@ -10044,12 +14944,23 @@ const SVC_SCREENS = [ "svc": "MG_FIELDGET", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_STR1", + "T_STR4", "T_STR3", + "T_STR5", + "T_AMOUNT", "T_ARG1", "T_ARG2" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR5", "T_COUNT" ] @@ -10060,12 +14971,22 @@ const SVC_SCREENS = [ "svc": "MG_FIELDSET", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_STR1", + "T_STR4", "T_STR3", "T_STR5", + "T_AMOUNT", "T_ARG1" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR3", "T_COUNT", "T_ARG2" @@ -10077,16 +14998,26 @@ const SVC_SCREENS = [ "svc": "MG_FINADV", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_CHANNEL", + "T_STR1", + "T_STR4", "T_STR3", - "T_BIZDATE", - "T_AMOUNT" + "T_STR5", + "T_AMOUNT", + "T_CHANNEL", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR4", "T_ID1", - "T_ARG1", - "T_RC" + "T_ARG1" ] }, { @@ -10095,12 +15026,24 @@ const SVC_SCREENS = [ "svc": "MG_FINADVRSP", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_STR1", + "T_STR4", + "T_STR3", + "T_STR5", + "T_AMOUNT", "T_CHANNEL", "T_BIZDATE", "T_ARG1" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR5", "T_ID1" ] @@ -10111,12 +15054,23 @@ const SVC_SCREENS = [ "svc": "MG_FINREQ", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_CHANNEL", + "T_STR1", + "T_STR4", "T_STR3", - "T_BIZDATE", - "T_AMOUNT" + "T_STR5", + "T_AMOUNT", + "T_CHANNEL", + "T_BIZDATE" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR2", "T_ID1", "T_ARG1", @@ -10130,16 +15084,27 @@ const SVC_SCREENS = [ "svc": "MG_FINRSP", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_STR1", + "T_STR4", + "T_STR3", + "T_STR5", + "T_AMOUNT", "T_CHANNEL", "T_BIZDATE", - "T_AMOUNT", "T_ARG2", "T_ARG1" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR2", - "T_ID1", - "T_RC" + "T_ID1" ] }, { @@ -10148,10 +15113,22 @@ const SVC_SCREENS = [ "svc": "MG_FROMISO", "desc": "ISO -> 내부전문 변환: 고정길이 ISO 전문을 잘라", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_STR3" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR3", "T_STR1", "T_STR4", @@ -10165,14 +15142,24 @@ const SVC_SCREENS = [ "svc": "MG_GAPSCAN", "desc": "STAN 순번 갭 검출: 당일 최소~최대 순번 구간 폭에서 실제", "in": [ - "T_BIZDATE" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ARG1", "T_ARG2", - "T_COUNT", - "T_ARG6", - "T_RC" + "T_COUNT" ] }, { @@ -10180,10 +15167,23 @@ const SVC_SCREENS = [ "modnm": "전문게이트웨이", "svc": "MG_HEALTH", "desc": "게이트웨이 헬스: 활성 채널 수 조회. */", - "in": [], + "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" + ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", + "T_COUNT" ] }, { @@ -10192,15 +15192,25 @@ const SVC_SCREENS = [ "svc": "MG_HOSTADPT", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_CHANNEL", "T_STR1", - "T_STR3" - ], - "out": [ + "T_STR4", "T_STR3", "T_STR5", - "T_COUNT", - "T_RC" + "T_AMOUNT", + "T_CHANNEL" + ], + "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", + "T_STR3", + "T_STR5", + "T_COUNT" ] }, { @@ -10211,12 +15221,22 @@ const SVC_SCREENS = [ "in": [ "T_CHANNEL", "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ARG4" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT", - "T_ARG4", - "T_RC" + "T_ARG4" ] }, { @@ -10225,15 +15245,27 @@ const SVC_SCREENS = [ "svc": "MG_INQRSP", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_STR1", + "T_STR4", + "T_STR3", + "T_STR5", + "T_AMOUNT", "T_CHANNEL", "T_BIZDATE", "T_AMT1", "T_ARG1" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR3", - "T_ID1", - "T_RC" + "T_ID1" ] }, { @@ -10243,11 +15275,21 @@ const SVC_SCREENS = [ "desc": "* MG_ISO8583.pgc - mg 모듈 서비스 MG_ISO8583 (one service = one file).", "in": [ "T_CHANNEL", - "T_STR3", "T_BIZDATE", - "T_AMOUNT" + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", + "T_STR3" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR1", "T_STR4", "T_AMOUNT", @@ -10260,10 +15302,21 @@ const SVC_SCREENS = [ "svc": "MG_ISOADPT", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_STR3" + "T_STR1", + "T_STR4", + "T_STR3", + "T_STR5", + "T_AMOUNT" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR1", "T_STR5", "T_CHANNEL" @@ -10276,10 +15329,21 @@ const SVC_SCREENS = [ "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_KEY2" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_KEY1", "T_ARG3" ] @@ -10291,9 +15355,21 @@ const SVC_SCREENS = [ "desc": "키 교환: 키버전 채번 + 세션키 갱신. */", "in": [ "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ARG2" ] }, @@ -10303,13 +15379,24 @@ const SVC_SCREENS = [ "svc": "MG_KEYEXPIRE", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ARG3", "T_ARG4" ], "out": [ - "T_ARG1", + "T_RC", + "T_MSG", "T_ARG6", - "T_RC" + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", + "T_ARG1" ] }, { @@ -10318,12 +15405,23 @@ const SVC_SCREENS = [ "svc": "MG_KEYLOAD", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_CHANNEL" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_KEY1", - "T_ARG3", - "T_RC" + "T_ARG3" ] }, { @@ -10332,9 +15430,21 @@ const SVC_SCREENS = [ "svc": "MG_KEYROTATE", "desc": "키 전환(교체): KEYVER 카운터를 올려 새 세대키 번호를 따고,", "in": [ - "T_CHANNEL" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_KEY1", "T_ARG3", "T_ARG2" @@ -10346,10 +15456,21 @@ const SVC_SCREENS = [ "svc": "MG_KEYSTATUS", "desc": "키 상태 조회: 현재 키버전(KEYVER)과 채널의 세션키/사인온", "in": [ - "T_CHANNEL" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_KEY1", "T_ARG3", "T_ARG4" @@ -10361,10 +15482,21 @@ const SVC_SCREENS = [ "svc": "MG_LOADBAL", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_BIZDATE" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_CHANNEL", "T_COUNT", "T_ARG3" @@ -10377,9 +15509,20 @@ const SVC_SCREENS = [ "desc": "* MG_LOG.pgc - mg 모듈 서비스 MG_LOG (one service = one file).", "in": [ "T_CHANNEL", - "T_BIZDATE" + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT" ] }, @@ -10389,10 +15532,23 @@ const SVC_SCREENS = [ "svc": "MG_MAC", "desc": "simple additive 4-hex MAC/checksum over a buffer (전문 무결성 데모). */", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_STR3", "T_ID1" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_KEY1" ] }, @@ -10402,10 +15558,22 @@ const SVC_SCREENS = [ "svc": "MG_MACGEN", "desc": "XOR-회전 8-hex MAC: 키버전을 시드로 섞어 세대별로 값이 달라진다. */", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_STR3" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_KEY1", "T_ARG3" ] @@ -10417,10 +15585,21 @@ const SVC_SCREENS = [ "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_STR3" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_KEY1" ] }, @@ -10430,10 +15609,22 @@ const SVC_SCREENS = [ "svc": "MG_MACVERIFY", "desc": "MG_MAC 과 동일 계열의 가산 체크섬 (검증측 독립 구현, 4-hex). */", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ID1" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_KEY2" ] }, @@ -10443,10 +15634,23 @@ const SVC_SCREENS = [ "svc": "MG_NACK", "desc": "* MG_NACK.pgc - mg 모듈 서비스 MG_NACK (one service = one file).", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ID1", "T_RC" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STATUS" ] }, @@ -10457,10 +15661,21 @@ const SVC_SCREENS = [ "desc": "0800 망관리 전문 라우터: 처리코드 001=사인온(UP),", "in": [ "T_CHANNEL", - "T_STR4", - "T_BIZDATE" + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", + "T_STR4" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR1", "T_STR2", "T_ID1" @@ -10472,16 +15687,27 @@ const SVC_SCREENS = [ "svc": "MG_NMADVICE", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_CHANNEL", + "T_STR1", + "T_STR4", "T_STR3", + "T_STR5", + "T_AMOUNT", + "T_CHANNEL", "T_BIZDATE" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_KEY1", "T_STR4", "T_ID1", - "T_ARG3", - "T_RC" + "T_ARG3" ] }, { @@ -10490,14 +15716,25 @@ const SVC_SCREENS = [ "svc": "MG_NMRSP", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_CHANNEL", + "T_STR1", + "T_STR4", "T_STR3", + "T_STR5", + "T_AMOUNT", + "T_CHANNEL", "T_BIZDATE" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR4", - "T_ID1", - "T_RC" + "T_ID1" ] }, { @@ -10506,11 +15743,22 @@ const SVC_SCREENS = [ "svc": "MG_PACK", "desc": "고정길이 전문 조립 (순수 계산): MTI(4)+처리코드(6)+금액(12). */", "in": [ + "T_CHANNEL", + "T_BIZDATE", "T_STR1", - "T_STR4", - "T_AMOUNT" + "T_MERCHANT", + "T_AMOUNT", + "T_STR4" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR3", "T_COUNT" ] @@ -10521,11 +15769,23 @@ const SVC_SCREENS = [ "svc": "MG_PANMASK", "desc": "BIN(앞6)/뒤4 보존 마스킹. 12자리 미만 짧은 PAN 은 전체 마스킹. */", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_STR5", "T_ID1" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR5" ] }, @@ -10535,9 +15795,22 @@ const SVC_SCREENS = [ "svc": "MG_PARSE", "desc": "* MG_PARSE.pgc - mg 모듈 서비스 MG_PARSE (one service = one file).", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_STR3" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR1", "T_STR4", "T_COUNT" @@ -10549,11 +15822,22 @@ const SVC_SCREENS = [ "svc": "MG_PINBLOCK", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_KEY1", - "T_STR5" + "T_STR1", + "T_STR4", + "T_STR3", + "T_STR5", + "T_AMOUNT", + "T_KEY1" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_KEY2", "T_COUNT" ] @@ -10564,13 +15848,24 @@ const SVC_SCREENS = [ "svc": "MG_PREAUTH", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_CHANNEL", + "T_STR1", + "T_STR4", "T_STR3", - "T_BIZDATE", + "T_STR5", "T_AMOUNT", + "T_CHANNEL", + "T_BIZDATE", "T_ARG4" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_AMT1", "T_ARG4", "T_ID1", @@ -10583,15 +15878,25 @@ const SVC_SCREENS = [ "svc": "MG_QDEPTH", "desc": "송수신 큐 깊이 조회: 대기(Q)/송신(S)/완료(D)/실패(F) 상태별", "in": [ - "T_BIZDATE" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ARG1", "T_ARG2", "T_ARG3", - "T_ARG4", - "T_ARG7", - "T_RC" + "T_ARG4" ] }, { @@ -10601,12 +15906,22 @@ const SVC_SCREENS = [ "desc": "* MG_QUEUE.pgc - mg 모듈 서비스 MG_QUEUE (one service = one file).", "in": [ "T_CHANNEL", - "T_STR1", - "T_STR3", "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", + "T_STR3", "T_ARG1" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ID2", "T_ARG1" ] @@ -10618,13 +15933,23 @@ const SVC_SCREENS = [ "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ARG4" ], "out": [ - "T_COUNT", - "T_ARG4", + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", "T_ARG8", - "T_RC" + "T_AMT2", + "T_AMT3", + "T_STR8", + "T_COUNT", + "T_ARG4" ] }, { @@ -10633,14 +15958,24 @@ const SVC_SCREENS = [ "svc": "MG_READYZ", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_BIZDATE" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ARG1", "T_ARG2", - "T_ARG3", - "T_ARG8", - "T_RC" + "T_ARG3" ] }, { @@ -10649,10 +15984,22 @@ const SVC_SCREENS = [ "svc": "MG_REASM", "desc": "분할전문 재조립: 세그먼트 T_STR5~T_STR8 (최대 4편) 을 순서대로", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ARG1" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR3", "T_COUNT", "T_ARG2" @@ -10665,13 +16012,22 @@ const SVC_SCREENS = [ "desc": "* MG_RECV.pgc - mg 모듈 서비스 MG_RECV (one service = one file).", "in": [ "T_CHANNEL", - "T_STR1", - "T_STR3", - "T_STR5", "T_BIZDATE", - "T_AMOUNT" + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", + "T_STR3", + "T_STR5" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ARG1", "T_ID1" ] @@ -10682,10 +16038,21 @@ const SVC_SCREENS = [ "svc": "MG_RELAYADPT", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_STR3" + "T_STR1", + "T_STR4", + "T_STR3", + "T_STR5", + "T_AMOUNT" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_CHANNEL", "T_STR3", "T_STR5", @@ -10698,13 +16065,23 @@ const SVC_SCREENS = [ "svc": "MG_RESEND", "desc": "전문 재전송: STAN 으로 원전문 찾아 큐에 적재. */", "in": [ - "T_ARG1", "T_CHANNEL", + "T_BIZDATE", "T_STR1", - "T_STR3", - "T_BIZDATE" + "T_MERCHANT", + "T_AMOUNT", + "T_ARG1", + "T_STR3" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ID2" ] }, @@ -10714,11 +16091,23 @@ const SVC_SCREENS = [ "svc": "MG_RESNDONE", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ID2", "T_ARG4" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT" ] }, @@ -10728,9 +16117,22 @@ const SVC_SCREENS = [ "svc": "MG_RETRY", "desc": "큐 재시도: retry_cnt +1, status=Q. */", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ID2" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT" ] }, @@ -10742,11 +16144,21 @@ const SVC_SCREENS = [ "in": [ "T_CHANNEL", "T_BIZDATE", - "T_STR3", + "T_STR1", + "T_MERCHANT", "T_AMOUNT", + "T_STR3", "T_ARG1" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ID1", "T_ARG1" ] @@ -10757,12 +16169,24 @@ const SVC_SCREENS = [ "svc": "MG_REVADVRSP", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_STR1", + "T_STR4", + "T_STR3", + "T_STR5", + "T_AMOUNT", "T_CHANNEL", "T_BIZDATE", "T_ARG1" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ID1", "T_ARG2" ] @@ -10775,11 +16199,20 @@ const SVC_SCREENS = [ "in": [ "T_CHANNEL", "T_BIZDATE", - "T_ARG1", - "T_AMOUNT" + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", + "T_ARG1" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ID1", "T_ID2" ] @@ -10790,13 +16223,24 @@ const SVC_SCREENS = [ "svc": "MG_REVRSP", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_STR1", + "T_STR4", + "T_STR3", + "T_STR5", + "T_AMOUNT", "T_CHANNEL", "T_BIZDATE", - "T_AMOUNT", "T_ARG1" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR2", "T_ID1" ] @@ -10808,10 +16252,20 @@ const SVC_SCREENS = [ "desc": "* MG_ROUTE.pgc - mg 모듈 서비스 MG_ROUTE (one service = one file).", "in": [ "T_CHANNEL", - "T_STR1" + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR3" ] }, @@ -10822,13 +16276,22 @@ const SVC_SCREENS = [ "desc": "라우팅 룰 등록: 채널+MTI -> 목적지 룰을 신규 채번해 추가.", "in": [ "T_CHANNEL", - "T_STR1", - "T_STR3", "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", + "T_STR3", "T_ARG4" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ID1" ] }, @@ -10838,13 +16301,24 @@ const SVC_SCREENS = [ "svc": "MG_RTCOUNT", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_CHANNEL" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT", "T_ARG2", - "T_ARG3", - "T_RC" + "T_ARG3" ] }, { @@ -10853,13 +16327,23 @@ const SVC_SCREENS = [ "svc": "MG_RTDISABLE", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_ID1", "T_CHANNEL", - "T_STR1" + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", + "T_ID1" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", + "T_COUNT" ] }, { @@ -10869,10 +16353,20 @@ const SVC_SCREENS = [ "desc": "라우팅 룰 비활성: 채널+MTI 의 활성 룰을 전부 내려서", "in": [ "T_CHANNEL", - "T_STR1" + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT" ] }, @@ -10882,10 +16376,22 @@ const SVC_SCREENS = [ "svc": "MG_RTENABLE", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ID1" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT" ] }, @@ -10896,11 +16402,21 @@ const SVC_SCREENS = [ "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ "T_CHANNEL", - "T_STR1" + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ - "T_COUNT", "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", + "T_COUNT", "T_STR3", "T_ARG4" ] @@ -10911,14 +16427,24 @@ const SVC_SCREENS = [ "svc": "MG_RTLIST", "desc": "라우팅 룰 열람 요약: 채널의 활성/비활성 룰 수와 최우선(최소)", "in": [ - "T_CHANNEL" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR3", "T_ARG4", - "T_COUNT", - "T_ARG7", - "T_RC" + "T_COUNT" ] }, { @@ -10928,11 +16454,21 @@ const SVC_SCREENS = [ "desc": "라우팅 목적지 절체: 채널+MTI 활성 룰의 dest 를 새 목적지로", "in": [ "T_CHANNEL", + "T_BIZDATE", "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_STR3" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR5", "T_COUNT" ] @@ -10944,12 +16480,21 @@ const SVC_SCREENS = [ "desc": "* MG_SEND.pgc - mg 모듈 서비스 MG_SEND (one service = one file).", "in": [ "T_CHANNEL", - "T_STR1", - "T_STR3", "T_BIZDATE", - "T_AMOUNT" + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", + "T_STR3" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ARG1", "T_ID1" ] @@ -10961,13 +16506,22 @@ const SVC_SCREENS = [ "desc": "채널 순번 감사: 채널이 기억하는 last_stan 과 원장의 실제", "in": [ "T_CHANNEL", - "T_BIZDATE" + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ARG1", - "T_ARG2", - "T_ARG6" + "T_ARG2" ] }, { @@ -10978,10 +16532,20 @@ const SVC_SCREENS = [ "in": [ "T_CHANNEL", "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ARG1" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT" ] }, @@ -10992,12 +16556,22 @@ const SVC_SCREENS = [ "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ARG1" ], "out": [ "T_RC", - "T_ARG2", - "T_ARG6" + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", + "T_ARG2" ] }, { @@ -11007,10 +16581,21 @@ const SVC_SCREENS = [ "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ARG1" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ARG2", "T_ARG3" ] @@ -11020,8 +16605,22 @@ const SVC_SCREENS = [ "modnm": "전문게이트웨이", "svc": "MG_SESSION", "desc": "* MG_SESSION.pgc - mg 모듈 서비스 MG_SESSION (one service = one file).", - "in": [], + "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" + ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ARG2" ] }, @@ -11032,9 +16631,20 @@ const SVC_SCREENS = [ "desc": "0500 정산집계 요청 생성: 당일 채널의 수신 0200 건수/금액을", "in": [ "T_CHANNEL", - "T_BIZDATE" + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT", "T_GROSS", "T_ID1", @@ -11049,15 +16659,25 @@ const SVC_SCREENS = [ "in": [ "T_CHANNEL", "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_COUNT", "T_GROSS" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR2", "T_ARG1", "T_AMT1", - "T_ID1", - "T_RC" + "T_ID1" ] }, { @@ -11066,9 +16686,21 @@ const SVC_SCREENS = [ "svc": "MG_SIGNOFF", "desc": "* MG_SIGNOFF.pgc - mg 모듈 서비스 MG_SIGNOFF (one service = one file).", "in": [ - "T_CHANNEL" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STATUS" ] }, @@ -11079,9 +16711,21 @@ const SVC_SCREENS = [ "desc": "* MG_SIGNON.pgc - mg 모듈 서비스 MG_SIGNON (one service = one file).", "in": [ "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_KEY1" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STATUS" ] }, @@ -11091,13 +16735,24 @@ const SVC_SCREENS = [ "svc": "MG_STALEMARK", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_CHANNEL", "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ARG4" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT", - "T_ARG5", - "T_RC" + "T_ARG5" ] }, { @@ -11105,8 +16760,22 @@ const SVC_SCREENS = [ "modnm": "전문게이트웨이", "svc": "MG_STAN", "desc": "* MG_STAN.pgc - mg 모듈 서비스 MG_STAN (one service = one file).", - "in": [], + "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" + ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ARG1" ] }, @@ -11116,14 +16785,24 @@ const SVC_SCREENS = [ "svc": "MG_STATAMT", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_CHANNEL", "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_AMT1" ], "out": [ - "T_ARG1", - "T_ARG2", + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", "T_ARG8", - "T_AMT2" + "T_AMT2", + "T_AMT3", + "T_STR8", + "T_ARG1", + "T_ARG2" ] }, { @@ -11133,13 +16812,23 @@ const SVC_SCREENS = [ "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ "T_CHANNEL", - "T_BIZDATE" + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ARG1", "T_ARG2", - "T_GROSS", - "T_ARG8" + "T_GROSS" ] }, { @@ -11148,15 +16837,24 @@ const SVC_SCREENS = [ "svc": "MG_STATDIR", "desc": "송수신 방향별 통계: 당일 IN/OUT 각각의 건수와 금액합,", "in": [ - "T_BIZDATE" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ARG1", "T_AMT1", - "T_ARG2", - "T_AMT2", - "T_ARG8", - "T_RC" + "T_ARG2" ] }, { @@ -11165,14 +16863,24 @@ const SVC_SCREENS = [ "svc": "MG_STATERR", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ + "T_CHANNEL", "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ARG4" ], "out": [ - "T_COUNT", - "T_ARG2", + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", "T_ARG8", - "T_RC" + "T_AMT2", + "T_AMT3", + "T_STR8", + "T_COUNT", + "T_ARG2" ] }, { @@ -11181,14 +16889,25 @@ const SVC_SCREENS = [ "svc": "MG_STATHOUR", "desc": "시간대별 통계: 요청 시(0~23)의 당일 건수/금액합과 함께", "in": [ + "T_CHANNEL", "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ARG5" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT", "T_GROSS", - "T_ARG5", - "T_ARG6" + "T_ARG5" ] }, { @@ -11197,14 +16916,24 @@ const SVC_SCREENS = [ "svc": "MG_STATMTI", "desc": "전문유형(MTI)별 당일 통계: 지정 MTI 의 건수/금액합/최대금액과", "in": [ + "T_CHANNEL", + "T_BIZDATE", "T_STR1", - "T_BIZDATE" + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT", "T_GROSS", - "T_AMT1", - "T_ARG8" + "T_AMT1" ] }, { @@ -11213,13 +16942,24 @@ const SVC_SCREENS = [ "svc": "MG_STATPEAK", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_BIZDATE" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ARG1", "T_COUNT", - "T_ARG3", - "T_RC" + "T_ARG3" ] }, { @@ -11228,14 +16968,24 @@ const SVC_SCREENS = [ "svc": "MG_STATRC", "desc": "응답코드 분포 조회: 당일 정상(00)/거절 건수, 고유 거절코드", "in": [ - "T_BIZDATE" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ARG1", "T_ARG2", - "T_ARG7", - "T_STR2", - "T_ARG6" + "T_STR2" ] }, { @@ -11245,9 +16995,20 @@ const SVC_SCREENS = [ "desc": "채널 당일 통계: 수신/송신/오류 건수. */", "in": [ "T_CHANNEL", - "T_BIZDATE" + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_ARG1", "T_ARG2", "T_ARG3", @@ -11260,15 +17021,26 @@ const SVC_SCREENS = [ "svc": "MG_STATSUM", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_BIZDATE" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT", "T_GROSS", "T_AMT1", "T_ARG2", - "T_ARG3", - "T_RC" + "T_ARG3" ] }, { @@ -11277,9 +17049,22 @@ const SVC_SCREENS = [ "svc": "MG_STATUS", "desc": "* MG_STATUS.pgc - mg 모듈 서비스 MG_STATUS (one service = one file).", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ID1" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STATUS" ] }, @@ -11291,13 +17076,22 @@ const SVC_SCREENS = [ "in": [ "T_CHANNEL", "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ARG4" ], "out": [ - "T_COUNT", - "T_ARG4", + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", "T_ARG8", - "T_RC" + "T_AMT2", + "T_AMT3", + "T_STR8", + "T_COUNT", + "T_ARG4" ] }, { @@ -11306,9 +17100,21 @@ const SVC_SCREENS = [ "svc": "MG_TIMEOUT", "desc": "전문 타임아웃 스윕: 임계초(TMOUTSEC, 기본 60초)를 넘겨도", "in": [ - "T_BIZDATE" + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_COUNT", "T_ARG5" ] @@ -11319,11 +17125,23 @@ const SVC_SCREENS = [ "svc": "MG_TIMEOUTSET", "desc": "타임아웃 임계초 설정: 운영자가 지정한 초(5~600 범위 강제)를", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_ARG5" ], "out": [ - "T_ARG5", - "T_RC" + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", + "T_ARG5" ] }, { @@ -11332,10 +17150,22 @@ const SVC_SCREENS = [ "svc": "MG_TOISO", "desc": "내부전문 -> ISO 변환: 콜론 구분 내부포맷 \"MTI:처리코드:금액\" 을", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_STR3" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR3", "T_STR6", "T_STR1", @@ -11347,13 +17177,24 @@ const SVC_SCREENS = [ "modnm": "전문게이트웨이", "svc": "MG_TPSCHECK", "desc": "순간 처리율 점검: 최근 60초 유입 전문 수로 근사 TPS(x100)를", - "in": [], + "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT" + ], "out": [ - "T_ARG1", - "T_ARG2", - "T_ARG8", + "T_RC", + "T_MSG", "T_ARG6", - "T_RC" + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", + "T_ARG1", + "T_ARG2" ] }, { @@ -11362,9 +17203,22 @@ const SVC_SCREENS = [ "svc": "MG_UNPACK", "desc": "* MG_UNPACK.pgc - mg 모듈 서비스 MG_UNPACK (one service = one file).", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_STR3" ], "out": [ + "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR1", "T_STR4", "T_AMOUNT" @@ -11376,10 +17230,22 @@ const SVC_SCREENS = [ "svc": "MG_VAN1ADPT", "desc": "VAN1 채널 어댑터: 파이프 구분 전문 \"V1|MTI|금액|처리코드\" 를", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_STR3" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR3", "T_STR1", "T_AMOUNT", @@ -11392,10 +17258,22 @@ const SVC_SCREENS = [ "svc": "MG_VAN2ADPT", "desc": "VAN2 채널 어댑터: 키=값 세미콜론 전문 \"M=0200;A=1000;P=000000\"", "in": [ + "T_CHANNEL", + "T_BIZDATE", + "T_STR1", + "T_MERCHANT", + "T_AMOUNT", "T_STR3" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR3", "T_STR1", "T_AMOUNT", @@ -11408,10 +17286,21 @@ const SVC_SCREENS = [ "svc": "MG_VAN3ADPT", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_STR3" + "T_STR1", + "T_STR4", + "T_STR3", + "T_STR5", + "T_AMOUNT" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR3", "T_STR1", "T_AMOUNT", @@ -11424,10 +17313,21 @@ const SVC_SCREENS = [ "svc": "MG_VAN4ADPT", "desc": "신규 스케일아웃 서비스; 캘러의 XA 브랜치에서 실행. mg_svr 가 광고.", "in": [ - "T_STR3" + "T_STR1", + "T_STR4", + "T_STR3", + "T_STR5", + "T_AMOUNT" ], "out": [ "T_RC", + "T_MSG", + "T_ARG6", + "T_ARG7", + "T_ARG8", + "T_AMT2", + "T_AMT3", + "T_STR8", "T_STR3", "T_COUNT", "T_CHANNEL" @@ -11439,7 +17339,8 @@ const SVC_SCREENS = [ "svc": "MM_ACCT_INQ", "desc": "기능: 정산계좌 원문 조회 (등록 여부 판정 포함).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC", @@ -11452,7 +17353,8 @@ const SVC_SCREENS = [ "svc": "MM_ACCT_MASK", "desc": "기능: 정산계좌 마스킹 조회 (앞3 뒤2만 노출, 중간은 * 처리하여 반환 - 개인정보 보호).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC" @@ -11465,7 +17367,8 @@ const SVC_SCREENS = [ "desc": "기능: 정산계좌 등록/변경 (숫자 10~16자리 형식 검증 통과분만 마스터에 반영, 검증 이력 기록).", "in": [ "T_MERCHANT", - "T_STR1" + "T_STR1", + "T_KEY1" ], "out": [ "T_RC" @@ -11477,7 +17380,8 @@ const SVC_SCREENS = [ "svc": "MM_ACCT_VERIFY", "desc": "기능: 정산계좌 검증 실행 (형식 검사 + ACCT_VRF 검증 이력 적재).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC" @@ -11489,7 +17393,8 @@ const SVC_SCREENS = [ "svc": "MM_ACCT_VRF_HIST", "desc": "기능: 정산계좌 검증 이력 조회 (누적 횟수 + 최근 결과/일시).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC", @@ -11501,7 +17406,10 @@ const SVC_SCREENS = [ "modnm": "마스터", "svc": "MM_ACCT_VRF_STAT", "desc": "기능: 정산계좌 검증 전사 통계 (총 검증 수, 검증 대상 가맹점 수, 실패 건수).", - "in": [], + "in": [ + "T_MERCHANT", + "T_KEY1" + ], "out": [ "T_COUNT", "T_ARG1", @@ -11515,7 +17423,8 @@ const SVC_SCREENS = [ "svc": "MM_AGENT_INQ", "desc": "기능: 모집인 단건 조회 (AGENT 코드그룹에서 명칭/상태 반환).", "in": [ - "T_KEY1" + "T_KEY1", + "T_STR1" ], "out": [ "T_RC" @@ -11542,7 +17451,8 @@ const SVC_SCREENS = [ "desc": "기능: 모집인 상태 변경 (ACTIVE/SUSPENDED/RETIRED 허용값 검증 후 갱신).", "in": [ "T_KEY1", - "T_STATUS" + "T_STATUS", + "T_STR1" ], "out": [ "T_RC" @@ -11555,7 +17465,8 @@ const SVC_SCREENS = [ "desc": "기능: 가입 심사 승인 (REVIEW -> ACTIVE, 기본 수수료율/한도 자동 개설).", "in": [ "T_MERCHANT", - "T_ARG1" + "T_ARG1", + "T_KEY1" ], "out": [ "T_RC" @@ -11568,7 +17479,8 @@ const SVC_SCREENS = [ "desc": "기능: 심사 담당자 배정 (담당자 코드를 REVIEWER 매핑 이력에 기록).", "in": [ "T_MERCHANT", - "T_STR1" + "T_STR1", + "T_KEY1" ], "out": [ "T_RC" @@ -11580,7 +17492,9 @@ const SVC_SCREENS = [ "svc": "MM_APPLY_CNT", "desc": "기능: 심사 상태별 신청 건수 집계 (대시보드용, 특정 상태 또는 전 워크플로 상태).", "in": [ - "T_STR1" + "T_STR1", + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_COUNT", @@ -11593,7 +17507,8 @@ const SVC_SCREENS = [ "svc": "MM_APPLY_DOCCHK", "desc": "기능: 심사 서류 접수 확인 (필수 마스터 필드 충족도 점검, 심사 서브단계).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC", @@ -11607,7 +17522,8 @@ const SVC_SCREENS = [ "desc": "기능: 심사 진행 중 신청 건 보류 (APPLIED->HOLD, 사유 이력 기록).", "in": [ "T_MERCHANT", - "T_REASON" + "T_REASON", + "T_KEY1" ], "out": [ "T_RC" @@ -11622,7 +17538,8 @@ const SVC_SCREENS = [ "T_MERCHANT", "T_STR1", "T_STR2", - "T_STR3" + "T_STR3", + "T_KEY1" ], "out": [ "T_RC" @@ -11635,7 +17552,8 @@ const SVC_SCREENS = [ "desc": "기능: 가입 심사 반려 (APPLIED/REVIEW -> REJECTED, 반려 사유 필수).", "in": [ "T_MERCHANT", - "T_REASON" + "T_REASON", + "T_KEY1" ], "out": [ "T_RC" @@ -11647,7 +17565,8 @@ const SVC_SCREENS = [ "svc": "MM_APPLY_RESUME", "desc": "기능: 보류된 심사 재개 (HOLD->APPLIED, 심사 대기열 복귀).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC", @@ -11660,7 +17579,8 @@ const SVC_SCREENS = [ "svc": "MM_APPLY_REVIEW", "desc": "기능: 가입 신청 심사 개시 (APPLIED -> REVIEW, MCC 유효성 사전점검).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC", @@ -11673,7 +17593,8 @@ const SVC_SCREENS = [ "svc": "MM_APPLY_SCORE", "desc": "기능: 심사 점수 산정 (MCC 리스크 + 서류 완비도 가중 합산, 등급 예비판정).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC", @@ -11686,7 +17607,8 @@ const SVC_SCREENS = [ "svc": "MM_APPLY_STATUS", "desc": "기능: 가입 심사 진행 상태 조회 (단계 코드 + 워크플로 이력 건수).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC", @@ -11701,7 +17623,8 @@ const SVC_SCREENS = [ "desc": "기능: 신청 철회 (신청/보류 단계 건을 WITHDRAWN 처리, 활성 가맹점은 철회 불가).", "in": [ "T_MERCHANT", - "T_REASON" + "T_REASON", + "T_KEY1" ], "out": [ "T_RC" @@ -11713,7 +17636,8 @@ const SVC_SCREENS = [ "svc": "MM_AUDIT_DIFF", "desc": "기능: 마스터 최근 변경 전후 비교 (가장 최신 이력 1건의 액션/이전값/이후값 반환).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC" @@ -11725,7 +17649,9 @@ const SVC_SCREENS = [ "svc": "MM_AUDIT_PURGE", "desc": "기능: 감사 이력 보존기한 경과분 삭제 (삭제 내역 자체도 감사 기록으로 남김).", "in": [ - "T_ARG1" + "T_ARG1", + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC", @@ -11738,7 +17664,8 @@ const SVC_SCREENS = [ "svc": "MM_AUDIT_RECENT", "desc": "기능: 가맹점 최근 변경 감사 조회 (최신 이력 1건: 액션/변경값/메모/일자).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC" @@ -11750,7 +17677,9 @@ const SVC_SCREENS = [ "svc": "MM_AUDIT_STAT", "desc": "기능: 전 마스터 감사 통계 (특정 액션의 전체 발생 건수 + 관련 가맹점 수 집계).", "in": [ - "T_STR1" + "T_STR1", + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_COUNT", @@ -11765,7 +17694,8 @@ const SVC_SCREENS = [ "desc": "기능: 가맹점 마스터 변경 감사 추적 (액션 필터별 이력 건수 + 발생 액션 종류 수).", "in": [ "T_MERCHANT", - "T_STR1" + "T_STR1", + "T_KEY1" ], "out": [ "T_COUNT", @@ -11779,7 +17709,9 @@ const SVC_SCREENS = [ "svc": "MM_BIN_INQ", "desc": "기능: BIN 조회.", "in": [ - "T_KEY1" + "T_KEY1", + "T_ISSUER", + "T_CCY" ], "out": [] }, @@ -11791,7 +17723,8 @@ const SVC_SCREENS = [ "in": [ "T_KEY1", "T_ISSUER", - "T_STR5" + "T_STR5", + "T_CCY" ], "out": [] }, @@ -11801,7 +17734,9 @@ const SVC_SCREENS = [ "svc": "MM_BIZNO_CHECK", "desc": "기능: 사업자등록번호 검증 (10자리 형식 + 국세청 검증공식 checksum + 마스터 중복 사용 확인).", "in": [ - "T_STR2" + "T_STR2", + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC", @@ -11815,7 +17750,8 @@ const SVC_SCREENS = [ "desc": "기능: 사업자등록번호 변경 (형식 검사 + 타 가맹점 중복 차단 + 변경 이력).", "in": [ "T_MERCHANT", - "T_STR2" + "T_STR2", + "T_KEY1" ], "out": [ "T_RC" @@ -11828,7 +17764,8 @@ const SVC_SCREENS = [ "desc": "기능: 폐업 확정 처리 (상태 CLOSED, 미결 계약 잔여 여부를 함께 점검하여 경고 코드 반환).", "in": [ "T_MERCHANT", - "T_REASON" + "T_REASON", + "T_KEY1" ], "out": [ "T_RC", @@ -11841,7 +17778,8 @@ const SVC_SCREENS = [ "svc": "MM_BIZ_REOPEN", "desc": "기능: 재개업 처리 (SUSPENDED/DORMANT 가맹점을 ACTIVE로 복귀, 폐업/삭제 건은 불가).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC" @@ -11854,7 +17792,8 @@ const SVC_SCREENS = [ "desc": "기능: 휴업 신고 처리 (ACTIVE 가맹점을 SUSPENDED로, 소속 활성 단말도 함께 정지).", "in": [ "T_MERCHANT", - "T_REASON" + "T_REASON", + "T_KEY1" ], "out": [ "T_RC", @@ -11867,7 +17806,8 @@ const SVC_SCREENS = [ "svc": "MM_BRANCH_INQ", "desc": "기능: 영업점 조회 (\"명칭|상위코드\" 저장값을 명칭만 분리하여 반환).", "in": [ - "T_KEY1" + "T_KEY1", + "T_STR1" ], "out": [ "T_RC" @@ -11878,7 +17818,10 @@ const SVC_SCREENS = [ "modnm": "마스터", "svc": "MM_BRANCH_LIST", "desc": "기능: 영업점 계층 요약 (전체 수 + 최상위(상위=-) 영업점 수 집계).", - "in": [], + "in": [ + "T_KEY1", + "T_STR1" + ], "out": [ "T_COUNT", "T_ARG1", @@ -11904,7 +17847,10 @@ const SVC_SCREENS = [ "modnm": "마스터", "svc": "MM_CHK_ACCT", "desc": "기능: 정합성 점검 - 정산계좌 미등록 활성 가맹점 적발 (정산 지급 리스크 사전 탐지).", - "in": [], + "in": [ + "T_MERCHANT", + "T_KEY1" + ], "out": [ "T_COUNT", "T_ARG1", @@ -11916,7 +17862,10 @@ const SVC_SCREENS = [ "modnm": "마스터", "svc": "MM_CHK_FEERATE", "desc": "기능: 수수료율 마스터 정합성 점검 (활성 가맹점 요율 누락 / bps 범위 이탈 / 고아 요율).", - "in": [], + "in": [ + "T_MERCHANT", + "T_KEY1" + ], "out": [ "T_COUNT", "T_ARG1", @@ -11929,7 +17878,10 @@ const SVC_SCREENS = [ "modnm": "마스터", "svc": "MM_CHK_GRADE", "desc": "기능: 정합성 점검 - 마스터 등급과 누적 매출 기준 산정 등급의 불일치 적발.", - "in": [], + "in": [ + "T_MERCHANT", + "T_KEY1" + ], "out": [ "T_COUNT", "T_ARG1", @@ -11941,7 +17893,10 @@ const SVC_SCREENS = [ "modnm": "마스터", "svc": "MM_CHK_LIMIT", "desc": "기능: 한도 마스터 정합성 점검 (활성 가맹점 한도 누락 + 한도 체계 역전 검출).", - "in": [], + "in": [ + "T_MERCHANT", + "T_KEY1" + ], "out": [ "T_COUNT", "T_ARG1", @@ -11953,7 +17908,10 @@ const SVC_SCREENS = [ "modnm": "마스터", "svc": "MM_CHK_MCC", "desc": "기능: 정합성 점검 - MCC 마스터에 없는 업종코드를 참조하는 가맹점 적발 (외래키 무결성).", - "in": [], + "in": [ + "T_MERCHANT", + "T_KEY1" + ], "out": [ "T_COUNT", "T_ARG1", @@ -11965,7 +17923,10 @@ const SVC_SCREENS = [ "modnm": "마스터", "svc": "MM_CHK_ORPHAN", "desc": "기능: 정합성 점검 - 대표가 비활성/부재인 대표-하위 관계 적발 (MERCH_REL 고아 링크).", - "in": [], + "in": [ + "T_MERCHANT", + "T_KEY1" + ], "out": [ "T_COUNT", "T_ARG1", @@ -11977,7 +17938,10 @@ const SVC_SCREENS = [ "modnm": "마스터", "svc": "MM_CHK_TERM", "desc": "기능: 단말 마스터 정합성 점검 (소속 가맹점 없는 고아 단말 + 폐업 가맹점의 가동 단말).", - "in": [], + "in": [ + "T_MERCHANT", + "T_KEY1" + ], "out": [ "T_COUNT", "T_ARG1", @@ -12031,7 +17995,8 @@ const SVC_SCREENS = [ "svc": "MM_CODE_CACHE", "desc": "기능: 코드그룹의 활성 코드 수 (캐시 워밍 시뮬).", "in": [ - "T_STR1" + "T_STR1", + "T_KEY1" ], "out": [ "T_COUNT" @@ -12066,7 +18031,8 @@ const SVC_SCREENS = [ "svc": "MM_CONFIRM", "desc": "기능: 가맹점 승인 확정 (상태 ACTIVE + 이력).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [] }, @@ -12077,7 +18043,8 @@ const SVC_SCREENS = [ "desc": "기능: 유효 계약의 조건 변경 (만기일 연장/단축, 변경 전후를 이력에 함께 기록).", "in": [ "T_MERCHANT", - "T_ARG1" + "T_ARG1", + "T_KEY1" ], "out": [ "T_RC" @@ -12090,7 +18057,8 @@ const SVC_SCREENS = [ "desc": "기능: 가맹 계약 해지 (CONT_END 이력 + 가맹점 상태 HOLD 전환).", "in": [ "T_MERCHANT", - "T_REASON" + "T_REASON", + "T_KEY1" ], "out": [ "T_RC" @@ -12103,7 +18071,8 @@ const SVC_SCREENS = [ "desc": "기능: 만기 임박 점검 (최신 계약 만기일까지 남은 일수 산출, 갱신 필요 판정).", "in": [ "T_MERCHANT", - "T_ARG1" + "T_ARG1", + "T_KEY1" ], "out": [ "T_RC", @@ -12116,7 +18085,8 @@ const SVC_SCREENS = [ "svc": "MM_CONTRACT_HIST", "desc": "기능: 가맹점 계약 변경 이력 요약 (체결/변경 횟수 + 최신 만기일 반환).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_COUNT", @@ -12129,7 +18099,8 @@ const SVC_SCREENS = [ "svc": "MM_CONTRACT_INQ", "desc": "기능: 가맹 계약 현황 조회 (체결/갱신 횟수, 최종 만기일, 유효 여부).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC", @@ -12144,7 +18115,8 @@ const SVC_SCREENS = [ "desc": "기능: 가맹 계약 체결 (기간 개월수로 만기일 산출, CONTRACT 이력 기록).", "in": [ "T_MERCHANT", - "T_ARG1" + "T_ARG1", + "T_KEY1" ], "out": [ "T_RC" @@ -12156,7 +18128,8 @@ const SVC_SCREENS = [ "svc": "MM_CONTRACT_REACT", "desc": "기능: 정지된 계약 재개 (미해제 CONTRACT_SUSP가 있을 때만 CONTRACT_REACT 기록).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC" @@ -12169,7 +18142,8 @@ const SVC_SCREENS = [ "desc": "기능: 가맹 계약 갱신 (직전 만기일 기준 개월수 연장).", "in": [ "T_MERCHANT", - "T_ARG1" + "T_ARG1", + "T_KEY1" ], "out": [ "T_RC" @@ -12182,7 +18156,8 @@ const SVC_SCREENS = [ "desc": "기능: 계약 일시정지 (활성 가맹점을 유지하되 CONTRACT_SUSP 이력 마킹, 중복 정지 차단).", "in": [ "T_MERCHANT", - "T_REASON" + "T_REASON", + "T_KEY1" ], "out": [ "T_RC" @@ -12194,7 +18169,8 @@ const SVC_SCREENS = [ "svc": "MM_FEERATE_HIST", "desc": "기능: 수수료율 변경 이력 건수 (dbio).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_COUNT" @@ -12207,7 +18183,8 @@ const SVC_SCREENS = [ "desc": "기능: 수수료율 조회 (dbio).", "in": [ "T_MERCHANT", - "T_STR4" + "T_STR4", + "T_KEY1" ], "out": [ "T_ARG1" @@ -12221,7 +18198,8 @@ const SVC_SCREENS = [ "in": [ "T_MERCHANT", "T_STR4", - "T_ARG1" + "T_ARG1", + "T_KEY1" ], "out": [] }, @@ -12232,7 +18210,9 @@ const SVC_SCREENS = [ "desc": "기능: 환율 조회 + 금액 환산 (rate_bps는 100 기준 bps).", "in": [ "T_CCY", - "T_FX_AMT" + "T_FX_AMT", + "T_KEY1", + "T_ISSUER" ], "out": [ "T_ARG2", @@ -12246,7 +18226,9 @@ const SVC_SCREENS = [ "desc": "기능: 환율 마스터 upsert.", "in": [ "T_CCY", - "T_ARG2" + "T_ARG2", + "T_KEY1", + "T_ISSUER" ], "out": [] }, @@ -12258,7 +18240,8 @@ const SVC_SCREENS = [ "in": [ "T_MERCHANT", "T_STR2", - "T_REASON" + "T_REASON", + "T_KEY1" ], "out": [ "T_RC" @@ -12269,7 +18252,10 @@ const SVC_SCREENS = [ "modnm": "마스터", "svc": "MM_GRADE_DIST", "desc": "기능: 가맹점 등급 분포 조회 (최근 등급 통계일 기준 S/A/B/C 세분 현황).", - "in": [], + "in": [ + "T_MERCHANT", + "T_KEY1" + ], "out": [ "T_RC", "T_ARG1", @@ -12285,7 +18271,8 @@ const SVC_SCREENS = [ "svc": "MM_GRADE_HIST", "desc": "기능: 등급 변경 이력 요약 (등급 관련 이력 건수 + 마스터 현행 등급 반환).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC", @@ -12298,7 +18285,8 @@ const SVC_SCREENS = [ "svc": "MM_GRADE_PREVIEW", "desc": "기능: 가맹점 등급 산정 시뮬레이션 (누적 매출로 등급 산출, 마스터는 불변).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC", @@ -12313,7 +18301,10 @@ const SVC_SCREENS = [ "desc": "기능: 공휴일 마스터 등록.", "in": [ "T_BIZDATE", - "T_STR1" + "T_STR1", + "T_KEY1", + "T_ISSUER", + "T_CCY" ], "out": [] }, @@ -12323,7 +18314,9 @@ const SVC_SCREENS = [ "svc": "MM_ISSUER_INQ", "desc": "기능: 발급사 조회.", "in": [ - "T_ISSUER" + "T_ISSUER", + "T_KEY1", + "T_CCY" ], "out": [] }, @@ -12335,7 +18328,9 @@ const SVC_SCREENS = [ "in": [ "T_ISSUER", "T_STR1", - "T_STR2" + "T_STR2", + "T_KEY1", + "T_CCY" ], "out": [] }, @@ -12345,7 +18340,8 @@ const SVC_SCREENS = [ "svc": "MM_LIMIT_INQ", "desc": "기능: 가맹점 한도 조회.", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_AMT1", @@ -12362,7 +18358,8 @@ const SVC_SCREENS = [ "T_MERCHANT", "T_AMT1", "T_AMT2", - "T_AMT3" + "T_AMT3", + "T_KEY1" ], "out": [] }, @@ -12373,7 +18370,9 @@ const SVC_SCREENS = [ "desc": "기능: 가맹점 업종(MCC) 재배정 (마스터에 등록된 MCC로만 변경, 변경 이력 기록).", "in": [ "T_MERCHANT", - "T_STR1" + "T_STR1", + "T_CATEGORY", + "T_KEY1" ], "out": [ "T_RC" @@ -12385,7 +18384,9 @@ const SVC_SCREENS = [ "svc": "MM_MCC_CNT", "desc": "기능: 특정 MCC를 사용하는 가맹점 수 + 해당 MCC 리스크 등급 동시 반환.", "in": [ - "T_STR1" + "T_STR1", + "T_CATEGORY", + "T_KEY1" ], "out": [ "T_RC", @@ -12399,7 +18400,9 @@ const SVC_SCREENS = [ "svc": "MM_MCC_DEL", "desc": "기능: MCC 업종코드 삭제 (참조하는 가맹점이 없을 때만 물리 삭제, 안전 삭제).", "in": [ - "T_STR1" + "T_STR1", + "T_CATEGORY", + "T_KEY1" ], "out": [ "T_RC", @@ -12412,7 +18415,8 @@ const SVC_SCREENS = [ "svc": "MM_MCC_INQ", "desc": "기능: MCC 업종 조회 (명칭/위험도 + 해당 업종 활성 가맹점 수).", "in": [ - "T_CATEGORY" + "T_CATEGORY", + "T_KEY1" ], "out": [ "T_RC", @@ -12428,7 +18432,9 @@ const SVC_SCREENS = [ "in": [ "T_STR6", "T_STR1", - "T_ARG1" + "T_ARG1", + "T_CATEGORY", + "T_KEY1" ], "out": [] }, @@ -12438,7 +18444,9 @@ const SVC_SCREENS = [ "svc": "MM_MCC_RISKLIST", "desc": "기능: 고위험 MCC 현황 (기준 위험도 이상 MCC 수, 최고 위험도, 소속 활성 가맹점 수).", "in": [ - "T_ARG1" + "T_ARG1", + "T_CATEGORY", + "T_KEY1" ], "out": [ "T_COUNT", @@ -12454,7 +18462,9 @@ const SVC_SCREENS = [ "desc": "기능: MCC 리스크 등급 설정 (1~5 범위 검증 후 mm_mcc.risk_level 갱신).", "in": [ "T_STR1", - "T_ARG1" + "T_ARG1", + "T_CATEGORY", + "T_KEY1" ], "out": [ "T_RC", @@ -12469,7 +18479,8 @@ const SVC_SCREENS = [ "in": [ "T_CATEGORY", "T_STR1", - "T_ARG1" + "T_ARG1", + "T_KEY1" ], "out": [ "T_RC" @@ -12482,7 +18493,8 @@ const SVC_SCREENS = [ "desc": "기능: 정산계좌 변경 (직접 UPDATE + 이력).", "in": [ "T_MERCHANT", - "T_STR3" + "T_STR3", + "T_KEY1" ], "out": [] }, @@ -12494,7 +18506,8 @@ const SVC_SCREENS = [ "in": [ "T_MERCHANT", "T_STR2", - "T_COUNT" + "T_COUNT", + "T_KEY1" ], "out": [ "T_COUNT" @@ -12507,7 +18520,8 @@ const SVC_SCREENS = [ "desc": "기능: 가맹점 폐업 처리 (DELETED 전환 + 전체 단말 폐기 + 하위 관계 해제).", "in": [ "T_MERCHANT", - "T_REASON" + "T_REASON", + "T_KEY1" ], "out": [ "T_RC", @@ -12520,7 +18534,8 @@ const SVC_SCREENS = [ "svc": "MM_MERCH_DEL", "desc": "기능: 가맹점 논리 삭제 (상태 DELETED + 이력).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [] }, @@ -12531,7 +18546,8 @@ const SVC_SCREENS = [ "desc": "기능: 장기 무거래 가맹점 휴면 전환 (ACTIVE->DORMANT, 최근 집계 매출 0 확인).", "in": [ "T_MERCHANT", - "T_REASON" + "T_REASON", + "T_KEY1" ], "out": [ "T_RC", @@ -12545,7 +18561,8 @@ const SVC_SCREENS = [ "desc": "기능: 가맹점 등급 변경 (dbio + 이력).", "in": [ "T_MERCHANT", - "T_STR2" + "T_STR2", + "T_KEY1" ], "out": [] }, @@ -12555,7 +18572,8 @@ const SVC_SCREENS = [ "svc": "MM_MERCH_INQ", "desc": "기능: 가맹점 마스터 조회 (dbio).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [] }, @@ -12566,7 +18584,8 @@ const SVC_SCREENS = [ "desc": "기능: 가맹점 휴업 처리 (ACTIVE -> HOLD, 가동 단말 일괄 SUSPENDED).", "in": [ "T_MERCHANT", - "T_REASON" + "T_REASON", + "T_KEY1" ], "out": [ "T_RC", @@ -12582,7 +18601,8 @@ const SVC_SCREENS = [ "T_MERCHANT", "T_STR1", "T_STR2", - "T_STR3" + "T_STR3", + "T_KEY1" ], "out": [] }, @@ -12592,7 +18612,8 @@ const SVC_SCREENS = [ "svc": "MM_MERCH_RESUME", "desc": "기능: 가맹점 영업 재개 (HOLD -> ACTIVE, 정지 단말 재가동).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC", @@ -12606,7 +18627,8 @@ const SVC_SCREENS = [ "desc": "기능: 가맹점 상태 변경 (dbio + 이력).", "in": [ "T_MERCHANT", - "T_STATUS" + "T_STATUS", + "T_KEY1" ], "out": [] }, @@ -12616,7 +18638,9 @@ const SVC_SCREENS = [ "svc": "MM_MERCH_SUM", "desc": "기능: 상태별 가맹점 수 집계 (ACTIVE 기준).", "in": [ - "T_STATUS" + "T_STATUS", + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_COUNT" @@ -12630,7 +18654,8 @@ const SVC_SCREENS = [ "in": [ "T_MERCHANT", "T_STR1", - "T_STR3" + "T_STR3", + "T_KEY1" ], "out": [] }, @@ -12640,7 +18665,8 @@ const SVC_SCREENS = [ "svc": "MM_PARTNER_INQ", "desc": "기능: 제휴사 마스터 단건 조회 (명칭/상태/표시순서).", "in": [ - "T_KEY1" + "T_KEY1", + "T_STR1" ], "out": [ "T_RC", @@ -12652,7 +18678,10 @@ const SVC_SCREENS = [ "modnm": "마스터", "svc": "MM_PARTNER_LIST", "desc": "기능: 활성 제휴사 총수 조회 (PARTNER 코드그룹 중 ACTIVE 상태 집계).", - "in": [], + "in": [ + "T_KEY1", + "T_STR1" + ], "out": [ "T_COUNT", "T_ARG1", @@ -12680,7 +18709,8 @@ const SVC_SCREENS = [ "desc": "기능: 제휴사 사용 상태 변경 (ACTIVE/INACTIVE 만 허용).", "in": [ "T_KEY1", - "T_STATUS" + "T_STATUS", + "T_STR1" ], "out": [ "T_RC" @@ -12706,7 +18736,8 @@ const SVC_SCREENS = [ "svc": "MM_RATE_APPROVE", "desc": "기능: 수수료율 변경 승인 (대기 요청을 실제 요율 마스터에 반영, RATE_APV 이력).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC", @@ -12719,7 +18750,8 @@ const SVC_SCREENS = [ "svc": "MM_RATE_CANCEL", "desc": "기능: 대기중 요율 변경 요청 철회 (미처리 RATE_REQ가 있을 때만 RATE_REJ로 자체 종결).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC" @@ -12731,7 +18763,8 @@ const SVC_SCREENS = [ "svc": "MM_RATE_HIST", "desc": "기능: 요율 승인 워크플로 이력 집계 (요청/승인/반려 건수를 각각 반환).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_COUNT", @@ -12745,7 +18778,10 @@ const SVC_SCREENS = [ "modnm": "마스터", "svc": "MM_RATE_PENDING", "desc": "기능: 전 가맹점 미처리 요율 요청 수 집계 (승인/반려로 종결되지 않은 RATE_REQ 카운트).", - "in": [], + "in": [ + "T_MERCHANT", + "T_KEY1" + ], "out": [ "T_COUNT", "T_ARG1", @@ -12759,7 +18795,8 @@ const SVC_SCREENS = [ "desc": "기능: 수수료율 변경 반려 (대기 요청을 반려 처리, 요율 마스터는 불변).", "in": [ "T_MERCHANT", - "T_REASON" + "T_REASON", + "T_KEY1" ], "out": [ "T_RC" @@ -12774,7 +18811,8 @@ const SVC_SCREENS = [ "T_MERCHANT", "T_STR2", "T_REASON", - "T_ARG1" + "T_ARG1", + "T_KEY1" ], "out": [ "T_RC", @@ -12787,7 +18825,8 @@ const SVC_SCREENS = [ "svc": "MM_RATE_STATUS", "desc": "기능: 최신 요율 변경 요청의 진행 상태 판정 (요청 이후 승인/반려 이력 존재 여부).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_ARG1", @@ -12800,7 +18839,8 @@ const SVC_SCREENS = [ "svc": "MM_REL_CHILDREN", "desc": "기능: 대표 가맹점에 연결된 하위 가맹점 수 집계 (MERCH_REL code_name=대표 기준).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC", @@ -12826,7 +18866,8 @@ const SVC_SCREENS = [ "svc": "MM_REL_LIST", "desc": "기능: 대표가맹점의 하위가맹점 현황 (활성 연결 수 + 최초 하위 ID).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC", @@ -12852,7 +18893,8 @@ const SVC_SCREENS = [ "svc": "MM_REL_PARENT", "desc": "기능: 하위 가맹점의 대표 가맹점 조회 (MERCH_REL 코드그룹에서 code=하위 -> code_name=대표).", "in": [ - "T_KEY1" + "T_KEY1", + "T_MERCHANT" ], "out": [ "T_RC" @@ -12864,7 +18906,8 @@ const SVC_SCREENS = [ "svc": "MM_REL_UNLINK", "desc": "기능: 대표가맹점-하위가맹점 관계 해제 (관계 행 INACTIVE 전환 + 이력).", "in": [ - "T_KEY1" + "T_KEY1", + "T_MERCHANT" ], "out": [ "T_RC" @@ -12876,7 +18919,8 @@ const SVC_SCREENS = [ "svc": "MM_REL_VERIFY", "desc": "기능: 대표-하위 관계 정합성 검증 (대표가 활성 마스터에 존재하는지, 순환/자기참조 여부).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC", @@ -12890,7 +18934,8 @@ const SVC_SCREENS = [ "svc": "MM_RISK_INQ", "desc": "기능: 현행 리스크 등급 조회 (최신 RISK_SET 수동값 우선, 없으면 최신 RISK 산정 밴드).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC", @@ -12903,7 +18948,8 @@ const SVC_SCREENS = [ "svc": "MM_RISK_SCORE", "desc": "기능: 가맹점 리스크 점수 산정 (MCC 위험도*20 + 저등급 가산 + 비활성 상태 가산).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC", @@ -12917,7 +18963,8 @@ const SVC_SCREENS = [ "desc": "기능: 가맹점 리스크 등급 수동 설정 (1~5, 심사역 재량 오버라이드를 RISK_SET 이력에 기록).", "in": [ "T_MERCHANT", - "T_ARG1" + "T_ARG1", + "T_KEY1" ], "out": [ "T_RC" @@ -12928,7 +18975,10 @@ const SVC_SCREENS = [ "modnm": "마스터", "svc": "MM_STATUS", "desc": "기능: 서비스 상태 (전체 가맹점 수, RC=0).", - "in": [], + "in": [ + "T_MERCHANT", + "T_KEY1" + ], "out": [ "T_COUNT", "T_RC" @@ -12940,7 +18990,9 @@ const SVC_SCREENS = [ "svc": "MM_TERMINAL_INQ", "desc": "기능: 단말 조회.", "in": [ - "T_STR5" + "T_STR5", + "T_STR6", + "T_KEY1" ], "out": [] }, @@ -12952,7 +19004,8 @@ const SVC_SCREENS = [ "in": [ "T_STR5", "T_MERCHANT", - "T_STR6" + "T_STR6", + "T_KEY1" ], "out": [] }, @@ -12962,7 +19015,9 @@ const SVC_SCREENS = [ "svc": "MM_TERM_ACTIVATE", "desc": "기능: 단말 개통 (정지/비활성 단말을 ACTIVE로 전환).", "in": [ - "T_STR5" + "T_STR5", + "T_STR6", + "T_KEY1" ], "out": [ "T_RC" @@ -12974,7 +19029,10 @@ const SVC_SCREENS = [ "svc": "MM_TERM_CNT", "desc": "기능: 가맹점별 단말기 보유 현황 집계 (전체/가동/폐기 건수).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_STR5", + "T_STR6", + "T_KEY1" ], "out": [ "T_COUNT", @@ -12989,7 +19047,9 @@ const SVC_SCREENS = [ "svc": "MM_TERM_DEACT", "desc": "기능: 단말 개통 해지 (ACTIVE 단말을 INACTIVE로, 사유 기록).", "in": [ - "T_STR5" + "T_STR5", + "T_STR6", + "T_KEY1" ], "out": [ "T_RC", @@ -13002,7 +19062,9 @@ const SVC_SCREENS = [ "svc": "MM_TERM_HIST", "desc": "기능: 단말 상태 확인 조회 (현재 상태/소속/모델 반환, 존재 여부 판정).", "in": [ - "T_STR5" + "T_STR5", + "T_STR6", + "T_KEY1" ], "out": [ "T_RC" @@ -13015,7 +19077,9 @@ const SVC_SCREENS = [ "desc": "기능: 등록된 단말의 설치 확정 (상태 REGISTERED->ACTIVE, 가맹점 활성 여부 확인).", "in": [ "T_STR5", - "T_MERCHANT" + "T_MERCHANT", + "T_STR6", + "T_KEY1" ], "out": [ "T_RC" @@ -13027,7 +19091,10 @@ const SVC_SCREENS = [ "svc": "MM_TERM_LIST", "desc": "기능: 가맹점별 단말 현황 요약 (전체/활성/폐기 건수 집계).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_STR5", + "T_STR6", + "T_KEY1" ], "out": [ "T_COUNT", @@ -13043,7 +19110,8 @@ const SVC_SCREENS = [ "desc": "기능: 단말 모델(기종) 정보 변경. 출력: T_STR6에 이전 모델을 실어 반환.", "in": [ "T_STR5", - "T_STR6" + "T_STR6", + "T_KEY1" ], "out": [ "T_RC" @@ -13056,7 +19124,9 @@ const SVC_SCREENS = [ "desc": "기능: 단말기 가맹점간 이동 (소속 변경 + 반입 이력).", "in": [ "T_KEY1", - "T_MERCHANT" + "T_MERCHANT", + "T_STR5", + "T_STR6" ], "out": [ "T_RC" @@ -13069,7 +19139,9 @@ const SVC_SCREENS = [ "desc": "기능: 단말 소속 가맹점 재배정 (기존 소속에서 신규 활성 가맹점으로 이관).", "in": [ "T_STR5", - "T_MERCHANT" + "T_MERCHANT", + "T_STR6", + "T_KEY1" ], "out": [ "T_RC" @@ -13081,7 +19153,9 @@ const SVC_SCREENS = [ "svc": "MM_TERM_RETIRE", "desc": "기능: 단말 폐기 처리 (상태 RETIRED로 확정, 활성 단말은 폐기 전 해지 필요).", "in": [ - "T_STR5" + "T_STR5", + "T_STR6", + "T_KEY1" ], "out": [ "T_RC" @@ -13094,7 +19168,9 @@ const SVC_SCREENS = [ "desc": "기능: 단말기 폐기 (SCRAPPED 전환 + 폐기 사유 이력).", "in": [ "T_KEY1", - "T_REASON" + "T_REASON", + "T_STR5", + "T_STR6" ], "out": [ "T_RC" @@ -13106,7 +19182,9 @@ const SVC_SCREENS = [ "svc": "MM_TERM_SUSPEND", "desc": "기능: 단말기 사용정지/재개 토글 (ACTIVE<->SUSPENDED).", "in": [ - "T_KEY1" + "T_KEY1", + "T_STR5", + "T_STR6" ], "out": [ "T_RC" @@ -13119,7 +19197,8 @@ const SVC_SCREENS = [ "desc": "기능: 단말 교체 (구단말 폐기 + 신단말이 구단말의 가맹점/모델을 승계 등록).", "in": [ "T_STR5", - "T_STR6" + "T_STR6", + "T_KEY1" ], "out": [ "T_RC" @@ -13131,7 +19210,8 @@ const SVC_SCREENS = [ "svc": "MM_VALIDATE", "desc": "기능: 가맹점 마스터 정합성 검사 (존재+수수료율 존재).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_KEY1" ], "out": [ "T_RC" @@ -13185,10 +19265,9 @@ const SVC_SCREENS = [ "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", - "T_ARG1", - "T_GROSS", - "T_RC" + "T_GROSS" ] }, { @@ -13224,12 +19303,13 @@ const SVC_SCREENS = [ "svc": "PAY_ADJUST", "desc": "지급 조정: py_adjust 에 증감(delta)을 기록하고 지급건 net 에 즉시 반영.", "in": [ - "T_ID1", - "T_DELTA", "T_BIZDATE", + "T_DELTA", + "T_ID1", "T_REASON" ], "out": [ + "T_RC", "T_ADJ" ] }, @@ -13239,12 +19319,13 @@ const SVC_SCREENS = [ "svc": "PAY_ADVANCE", "desc": "선지급(advance): 선지급 수수료(1%)를 공제한 지급건(ADVANCE 채널)을 생성.", "in": [ - "T_MERCHANT", - "T_KEY1", "T_AMOUNT", - "T_BIZDATE" + "T_BIZDATE", + "T_KEY1", + "T_MERCHANT" ], "out": [ + "T_RC", "T_ID1", "T_FEE", "T_NET" @@ -13256,15 +19337,15 @@ const SVC_SCREENS = [ "svc": "PAY_ADVCALC", "desc": "선지급 가능액 산정: 가맹점 미지급(R/A) 잔액에 지급률(bps)을 곱한 한도에서", "in": [ - "T_MERCHANT", - "T_ARG1" + "T_ARG1", + "T_MERCHANT" ], "out": [ "T_RC", - "T_GROSS", "T_AMT1", "T_AMT2", - "T_ARG1" + "T_ARG1", + "T_GROSS" ] }, { @@ -13286,9 +19367,9 @@ const SVC_SCREENS = [ "svc": "PAY_ADVREPAY", "desc": "선지급 상환: ADVANCE 채널 지급건의 잔여 net 에서 상환액을 차감하고", "in": [ - "T_ID1", "T_AMOUNT", - "T_BIZDATE" + "T_BIZDATE", + "T_ID1" ], "out": [ "T_RC", @@ -13302,14 +19383,14 @@ const SVC_SCREENS = [ "svc": "PAY_APPRBULK", "desc": "소액 일괄 승인: 당일 요청(R)건 중 net 이 승인한도 이하인 건을 한 번의", "in": [ - "T_BIZDATE", - "T_AMT1" + "T_AMT1", + "T_BIZDATE" ], "out": [ "T_RC", "T_COUNT", - "T_ARG1", - "T_AMT1" + "T_AMT1", + "T_ARG1" ] }, { @@ -13333,8 +19414,8 @@ const SVC_SCREENS = [ "T_ID1" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_COUNT" ] }, { @@ -13343,9 +19424,9 @@ const SVC_SCREENS = [ "svc": "PAY_APPRLIMIT", "desc": "고액 승인 통제: 한도 초과 건은 승인자 지정 없이는 거절(2). 승인 시", "in": [ + "T_AMT1", "T_ID1", - "T_KEY2", - "T_AMT1" + "T_KEY2" ], "out": [ "T_RC", @@ -13361,8 +19442,8 @@ const SVC_SCREENS = [ "T_ID1" ], "out": [ - "T_AMOUNT", - "T_RC" + "T_RC", + "T_AMOUNT" ] }, { @@ -13372,9 +19453,10 @@ const SVC_SCREENS = [ "desc": "승인 대기 집계: 아직 승인 전인 요청(R) 지급건을 가맹점별로 묶어 대기 건수/", "in": [], "out": [ + "T_RC", "T_COUNT", "T_ARG2", - "T_RC" + "T_ARG3" ] }, { @@ -13383,12 +19465,12 @@ const SVC_SCREENS = [ "svc": "PAY_APPRREQ", "desc": "승인 대기 큐 조회: 당일 요청(R)건을 금액 내림차순 상위 N건 커서로 훑어", "in": [ - "T_BIZDATE", - "T_ARG1" + "T_ARG1", + "T_BIZDATE" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_COUNT" ] }, { @@ -13397,13 +19479,13 @@ const SVC_SCREENS = [ "svc": "PAY_BANKFILE", "desc": "은행별 지급파일 생성: 지정 은행코드의 승인(A) 지급건만 py_account 조인으로", "in": [ - "T_KEY1", - "T_BIZDATE" + "T_BIZDATE", + "T_KEY1" ], "out": [ "T_RC", - "T_COUNT", "T_ID2", + "T_COUNT", "T_GROSS" ] }, @@ -13416,11 +19498,11 @@ const SVC_SCREENS = [ "T_ID2" ], "out": [ + "T_RC", + "T_COUNT", "T_AMT1", "T_AMT2", - "T_COUNT", - "T_DELTA", - "T_RC" + "T_DELTA" ] }, { @@ -13429,13 +19511,14 @@ const SVC_SCREENS = [ "svc": "PAY_BULK", "desc": "대량 지급 생성: 동일 금액의 지급건(BULK 채널)을 N건 반복 등록.", "in": [ - "T_MERCHANT", - "T_KEY1", "T_AMOUNT", + "T_BIZDATE", "T_COUNT", - "T_BIZDATE" + "T_KEY1", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -13445,12 +19528,13 @@ const SVC_SCREENS = [ "svc": "PAY_CANCEL", "desc": "지급 취소: 취소 조정(-amount)을 py_adjust 에 기록하고 지급건 상태 C 로 전이.", "in": [ - "T_ID1", "T_AMOUNT", "T_BIZDATE", + "T_ID1", "T_REASON" ], "out": [ + "T_RC", "T_ADJ" ] }, @@ -13460,16 +19544,14 @@ const SVC_SCREENS = [ "svc": "PAY_CANCELHIST", "desc": "취소/환입 이력 집계: 가맹점 당일 취소(C)와 역분개/환입(V)을 각각", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_MERCHANT" ], "out": [ - "T_ARG1", - "T_AMT1", - "T_ARG2", - "T_AMT2", + "T_RC", "T_COUNT", - "T_RC" + "T_AMT2", + "T_ARG2" ] }, { @@ -13484,6 +19566,7 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_COUNT", "T_ADJ" ] }, @@ -13493,8 +19576,8 @@ const SVC_SCREENS = [ "svc": "PAY_CHARGEBACK", "desc": "지급 되물림: 분쟁 등으로 지급완료(P) 건을 되물릴 때, 원건은 보존한 채", "in": [ - "T_ID1", - "T_BIZDATE" + "T_BIZDATE", + "T_ID1" ], "out": [ "T_RC", @@ -13511,6 +19594,7 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -13533,8 +19617,8 @@ const SVC_SCREENS = [ "svc": "PAY_COMBINE", "desc": "지급 합산: 같은 계좌의 당일 요청(R) 건들을 하나로 합산 지급한다. 합계", "in": [ - "T_KEY1", - "T_BIZDATE" + "T_BIZDATE", + "T_KEY1" ], "out": [ "T_RC", @@ -13551,7 +19635,9 @@ const SVC_SCREENS = [ "in": [ "T_ID2" ], - "out": [] + "out": [ + "T_RC" + ] }, { "mod": "py", @@ -13576,8 +19662,8 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_COUNT" ] }, { @@ -13590,8 +19676,8 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", - "T_ARG1", - "T_COUNT" + "T_COUNT", + "T_ARG1" ] }, { @@ -13613,8 +19699,8 @@ const SVC_SCREENS = [ "svc": "PAY_FAILRATE", "desc": "지급 실패율 산출: 가맹점 당일 전체 지급건 대비 반려(J) 비중을 bps 로", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_MERCHANT" ], "out": [ "T_RC", @@ -13632,8 +19718,11 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", - "T_RC" + "T_AMT2", + "T_ARG6", + "T_ARG7" ] }, { @@ -13645,10 +19734,10 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", "T_GROSS", - "T_ARG2", - "T_RC" + "T_ARG2" ] }, { @@ -13671,16 +19760,16 @@ const SVC_SCREENS = [ "svc": "PAY_FEECAP", "desc": "수수료 상한 적용: 미지급 건 수수료가 상한(T_AMOUNT)을 넘으면 상한까지", "in": [ - "T_ID1", "T_AMOUNT", "T_BIZDATE", + "T_ID1", "T_REASON" ], "out": [ "T_RC", - "T_ADJ", "T_FEE", - "T_NET" + "T_NET", + "T_ADJ" ] }, { @@ -13689,11 +19778,12 @@ const SVC_SCREENS = [ "svc": "PAY_FEEDED", "desc": "수수료 공제(fee deduction): bps 로 수수료를 산출해 fee/net 을 재계산·갱신.", "in": [ - "T_ID1", "T_AMOUNT", - "T_ARG1" + "T_ARG1", + "T_ID1" ], "out": [ + "T_RC", "T_FEE", "T_NET" ] @@ -13704,8 +19794,8 @@ const SVC_SCREENS = [ "svc": "PAY_FEEFIXED", "desc": "정액 수수료 적용: 미지급(R/A) 건에 요청 정액 수수료(T_AMOUNT)를 부과하고", "in": [ - "T_ID1", - "T_AMOUNT" + "T_AMOUNT", + "T_ID1" ], "out": [ "T_RC", @@ -13719,8 +19809,8 @@ const SVC_SCREENS = [ "svc": "PAY_FEEMIN", "desc": "최소수수료 보정: 당일 요청(R) 건 중 수수료가 최소수수료 미만인 건을", "in": [ - "T_BIZDATE", - "T_AMT1" + "T_AMT1", + "T_BIZDATE" ], "out": [ "T_RC", @@ -13767,9 +19857,9 @@ const SVC_SCREENS = [ "out": [ "T_RC", "T_COUNT", + "T_NET", "T_FEE", - "T_ADJ", - "T_NET" + "T_ADJ" ] }, { @@ -13778,9 +19868,9 @@ const SVC_SCREENS = [ "svc": "PAY_FEEWAIVE", "desc": "수수료 면제: 미지급(R/A) 건의 수수료를 0 으로 하고 net 을 amount 로 복원.", "in": [ + "T_BIZDATE", "T_ID1", - "T_REASON", - "T_BIZDATE" + "T_REASON" ], "out": [ "T_RC", @@ -13823,6 +19913,7 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_ID2", "T_COUNT", "T_GROSS" @@ -13851,11 +19942,10 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_ARG1", - "T_ARG2", + "T_RC", "T_COUNT", "T_GROSS", - "T_RC" + "T_ARG2" ] }, { @@ -13867,11 +19957,11 @@ const SVC_SCREENS = [ "T_ID2" ], "out": [ - "T_ARG1", - "T_AMT1", + "T_RC", "T_COUNT", "T_GROSS", - "T_RC" + "T_AMT1", + "T_ARG1" ] }, { @@ -13880,8 +19970,8 @@ const SVC_SCREENS = [ "svc": "PAY_GARNISH", "desc": "가압류 등록: 활성 계좌를 GARNISH 로 표시하고, 그 계좌 미지급(R/A) 잔액을", "in": [ - "T_KEY1", - "T_BIZDATE" + "T_BIZDATE", + "T_KEY1" ], "out": [ "T_RC", @@ -13898,6 +19988,7 @@ const SVC_SCREENS = [ "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -13937,8 +20028,10 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", - "T_RC" + "T_AMT4", + "T_ARG5" ] }, { @@ -13950,8 +20043,8 @@ const SVC_SCREENS = [ "T_KEY1" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_COUNT" ] }, { @@ -13975,9 +20068,9 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", - "T_GROSS", - "T_RC" + "T_GROSS" ] }, { @@ -14015,9 +20108,14 @@ const SVC_SCREENS = [ "T_ID1" ], "out": [ + "T_RC", + "T_ARG3", "T_AMOUNT", "T_FEE", - "T_NET" + "T_NET", + "T_ID2", + "T_ARG1", + "T_ARG2" ] }, { @@ -14031,6 +20129,7 @@ const SVC_SCREENS = [ "T_ARG2" ], "out": [ + "T_RC", "T_AMT1", "T_NET" ] @@ -14044,9 +20143,11 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", "T_GROSS", - "T_RC" + "T_AMT2", + "T_AMT3" ] }, { @@ -14055,14 +20156,14 @@ const SVC_SCREENS = [ "svc": "PAY_LIMITCHK", "desc": "지급한도 점검: 가맹점 일한도(merchant.daily_limit) 대비 당일 확정성", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_AMT1", "T_AMT2", - "T_AMT3", - "T_RC" + "T_AMT3" ] }, { @@ -14075,8 +20176,8 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", - "T_ARG1", - "T_COUNT" + "T_COUNT", + "T_ARG1" ] }, { @@ -14088,8 +20189,9 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", - "T_RC" + "T_AMT2" ] }, { @@ -14098,8 +20200,8 @@ const SVC_SCREENS = [ "svc": "PAY_LIMITSET", "desc": "지급 일한도 설정: 가맹점의 일 지급한도(merchant.daily_limit)를 새 값으로", "in": [ - "T_MERCHANT", - "T_AMOUNT" + "T_AMOUNT", + "T_MERCHANT" ], "out": [ "T_RC", @@ -14112,12 +20214,13 @@ const SVC_SCREENS = [ "svc": "PAY_LIMITWARN", "desc": "지급 한도 근접 경보: 당일 사용액이 일한도의 지정 비율(T_ARG1 %, 기본 80)", "in": [ - "T_BIZDATE", - "T_ARG1" + "T_ARG1", + "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", - "T_RC" + "T_AMT2" ] }, { @@ -14126,10 +20229,11 @@ const SVC_SCREENS = [ "svc": "PAY_MERCHSUM", "desc": "가맹점 당일 지급 집계: py_payout 집계 후 dbio 로 py_summary upsert.", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", "T_GROSS" ] @@ -14140,13 +20244,13 @@ const SVC_SCREENS = [ "svc": "PAY_MERGE", "desc": "지급 합산: 가맹점 당일 요청(R) STD 건이 2건 이상이면 전부 취소(C) 표시 후", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_MERCHANT" ], "out": [ "T_RC", - "T_COUNT", "T_ID2", + "T_COUNT", "T_NET" ] }, @@ -14156,10 +20260,11 @@ const SVC_SCREENS = [ "svc": "PAY_NETOFF", "desc": "상계(net-off): 지급건의 net 을 조회해 차감액을 반영하고 갱신.", "in": [ - "T_ID1", - "T_AMOUNT" + "T_AMOUNT", + "T_ID1" ], "out": [ + "T_RC", "T_NET" ] }, @@ -14169,14 +20274,14 @@ const SVC_SCREENS = [ "svc": "PAY_NETRECALC", "desc": "순지급액 정합 검증: 당일 건 중 amount - fee != net 인 불일치 건수와", "in": [ - "T_BIZDATE", - "T_ARG1" + "T_ARG1", + "T_BIZDATE" ], "out": [ "T_RC", "T_COUNT", - "T_DELTA", - "T_ARG2" + "T_ARG2", + "T_DELTA" ] }, { @@ -14199,8 +20304,8 @@ const SVC_SCREENS = [ "svc": "PAY_NOTIFYEMAIL", "desc": "지급내역 이메일 통지(시뮬): 가맹점의 당일 지급완료(P) 건수/합계를 집계해", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_MERCHANT" ], "out": [ "T_RC", @@ -14293,15 +20398,15 @@ const SVC_SCREENS = [ "svc": "PAY_OFFSETAPPLY", "desc": "상계 적용: 미지급(R/A) 건의 net 에서 상계액을 차감(net 미만으로 캡)하고,", "in": [ - "T_ID1", "T_AMOUNT", "T_BIZDATE", + "T_ID1", "T_REASON" ], "out": [ "T_RC", - "T_ADJ", - "T_NET" + "T_NET", + "T_ADJ" ] }, { @@ -14313,8 +20418,8 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_COUNT" ] }, { @@ -14335,8 +20440,8 @@ const SVC_SCREENS = [ "svc": "PAY_RECLAIM", "desc": "과지급 환수: 지급완료(P) 건에서 요청 환수액(T_AMOUNT, net 한도)만큼을", "in": [ - "T_ID1", - "T_AMOUNT" + "T_AMOUNT", + "T_ID1" ], "out": [ "T_RC", @@ -14352,6 +20457,7 @@ const SVC_SCREENS = [ "T_ID2" ], "out": [ + "T_RC", "T_COUNT", "T_GROSS" ] @@ -14379,7 +20485,9 @@ const SVC_SCREENS = [ "T_ID1", "T_REASON" ], - "out": [] + "out": [ + "T_RC" + ] }, { "mod": "py", @@ -14400,8 +20508,8 @@ const SVC_SCREENS = [ "desc": "실지급 송금: 계좌 잔액을 확인해 부족하면 거절, 충분하면 차감 후 상태 P.", "in": [ "T_ID1", - "T_NET", - "T_KEY1" + "T_KEY1", + "T_NET" ], "out": [ "T_RC" @@ -14413,9 +20521,9 @@ const SVC_SCREENS = [ "svc": "PAY_REPAYACCT", "desc": "계좌변경 재지급: 반려(J)된 지급건의 금액/수수료를 승계해 새 계좌(T_KEY2)로", "in": [ + "T_BIZDATE", "T_ID1", - "T_KEY2", - "T_BIZDATE" + "T_KEY2" ], "out": [ "T_RC", @@ -14482,8 +20590,9 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", - "T_RC" + "T_ARG2" ] }, { @@ -14492,8 +20601,8 @@ const SVC_SCREENS = [ "svc": "PAY_RESVMODIFY", "desc": "예약 변경: 아직 실행되지 않은 예약(S) 의 실행일(run_date)을 새 일자로", "in": [ - "T_ID2", - "T_BIZDATE" + "T_BIZDATE", + "T_ID2" ], "out": [ "T_RC" @@ -14530,11 +20639,13 @@ const SVC_SCREENS = [ "svc": "PAY_REVERSE", "desc": "지급 역분개(reverse): 상태 V(역분개)로 전이하고 계좌 잔액을 원복.", "in": [ - "T_ID1", "T_AMOUNT", + "T_ID1", "T_KEY1" ], - "out": [] + "out": [ + "T_RC" + ] }, { "mod": "py", @@ -14569,10 +20680,11 @@ const SVC_SCREENS = [ "svc": "PAY_SCHEDULE", "desc": "지급 예약 등록: 지정 실행일로 py_schedule 예약(S) 레코드를 생성.", "in": [ - "T_ID1", - "T_BIZDATE" + "T_BIZDATE", + "T_ID1" ], "out": [ + "T_RC", "T_ID2" ] }, @@ -14596,9 +20708,9 @@ const SVC_SCREENS = [ "svc": "PAY_SEIZEAMT", "desc": "압류 금액 상계: 압류 계좌의 보류(H) 지급건 net 합을 압류가능액으로 산정하고,", "in": [ - "T_KEY1", + "T_AMOUNT", "T_BIZDATE", - "T_AMOUNT" + "T_KEY1" ], "out": [ "T_RC", @@ -14615,9 +20727,9 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", - "T_GROSS", - "T_RC" + "T_GROSS" ] }, { @@ -14627,8 +20739,8 @@ const SVC_SCREENS = [ "desc": "압류 계좌 목록: 압류(SEIZED) 상태 계좌를 커서로 훑어 계좌번호/예금주와 그", "in": [], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_COUNT" ] }, { @@ -14650,13 +20762,14 @@ const SVC_SCREENS = [ "svc": "PAY_SPLIT", "desc": "지급 분할: 금액을 N건으로 나눠 각각의 지급건(SPLIT 채널)으로 생성.", "in": [ - "T_MERCHANT", - "T_KEY1", "T_AMOUNT", + "T_BIZDATE", "T_COUNT", - "T_BIZDATE" + "T_KEY1", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -14666,9 +20779,9 @@ const SVC_SCREENS = [ "svc": "PAY_SPLITEVEN", "desc": "균등 분할: 요청(R) 지급건을 N 건으로 균등 분할하되 나머지는 첫 건에 몰아", "in": [ - "T_ID1", + "T_BIZDATE", "T_COUNT", - "T_BIZDATE" + "T_ID1" ], "out": [ "T_RC", @@ -14682,15 +20795,15 @@ const SVC_SCREENS = [ "svc": "PAY_SPLITRATIO", "desc": "비율 분할: 요청(R) 건을 bps 비율로 두 건의 SPLIT 지급건으로 쪼개고", "in": [ - "T_ID1", - "T_ARG1" + "T_ARG1", + "T_ID1" ], "out": [ "T_RC", "T_ID2", - "T_ARG2", "T_AMT1", - "T_AMT2" + "T_AMT2", + "T_ARG2" ] }, { @@ -14702,8 +20815,9 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", - "T_RC" + "T_ARG8" ] }, { @@ -14715,8 +20829,10 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", - "T_RC" + "T_AMT2", + "T_ARG3" ] }, { @@ -14728,10 +20844,10 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", "T_GROSS", - "T_ARG2", - "T_RC" + "T_ARG2" ] }, { @@ -14740,14 +20856,15 @@ const SVC_SCREENS = [ "svc": "PAY_STATDAILY", "desc": "일자별 지급 추이: 시작일부터 N일 구간을 일자별 커서 집계해 occurrence 로", "in": [ - "T_BIZDATE", - "T_ARG1" + "T_ARG1", + "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", "T_GROSS", "T_AMT2", - "T_RC" + "T_ARG2" ] }, { @@ -14756,14 +20873,15 @@ const SVC_SCREENS = [ "svc": "PAY_STATEMENT", "desc": "가맹점 지급 명세: 당일 지급완료(P) 건의 건수/금액/수수료/순액을 집계.", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", "T_GROSS", - "T_FEE", - "T_NET" + "T_NET", + "T_FEE" ] }, { @@ -14775,8 +20893,10 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", - "T_RC" + "T_AMT2", + "T_AMT3" ] }, { @@ -14789,8 +20909,10 @@ const SVC_SCREENS = [ "T_KEY2" ], "out": [ + "T_RC", "T_COUNT", - "T_RC" + "T_AMT2", + "T_ARG3" ] }, { @@ -14802,11 +20924,10 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_ARG1", - "T_ARG2", - "T_ARG3", + "T_RC", "T_COUNT", - "T_RC" + "T_ARG2", + "T_ARG3" ] }, { @@ -14818,8 +20939,11 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", - "T_RC" + "T_AMT3", + "T_AMT4", + "T_ARG4" ] }, { @@ -14830,7 +20954,13 @@ const SVC_SCREENS = [ "in": [ "T_ID1" ], - "out": [] + "out": [ + "T_RC", + "T_ARG3", + "T_ID2", + "T_ARG1", + "T_ARG2" + ] }, { "mod": "py", @@ -14838,10 +20968,11 @@ const SVC_SCREENS = [ "svc": "PAY_SUMMARY", "desc": "가맹점 일별 지급 집계 조회: py_summary 에서 건수/금액을 읽어 회신(무행 시 0).", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", "T_COUNT", "T_GROSS" ] @@ -14852,10 +20983,10 @@ const SVC_SCREENS = [ "svc": "PAY_URGENT", "desc": "긴급지급: 긴급 수수료(0.5%) 공제 후 잔액 확인 -> 즉시 지급완료(P) 건을", "in": [ - "T_MERCHANT", - "T_KEY1", "T_AMOUNT", - "T_BIZDATE" + "T_BIZDATE", + "T_KEY1", + "T_MERCHANT" ], "out": [ "T_RC", @@ -14870,12 +21001,14 @@ const SVC_SCREENS = [ "svc": "PAY_URGENTQ", "desc": "긴급 지급 큐: 당일 요청(R) 건 중 금액이 임계치(T_AMOUNT) 이상인 건을 금액", "in": [ - "T_BIZDATE", - "T_AMOUNT" + "T_AMOUNT", + "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", - "T_RC" + "T_AMT1", + "T_ARG1" ] }, { @@ -14913,8 +21046,9 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", - "T_RC" + "T_AMT2" ] }, { @@ -14923,13 +21057,13 @@ const SVC_SCREENS = [ "svc": "PAY_XFERFILE", "desc": "이체유형별 지급파일 생성: 지정 채널(STD/SPLIT/BULK/ADVANCE)의 승인(A) 건만", "in": [ - "T_CHANNEL", - "T_BIZDATE" + "T_BIZDATE", + "T_CHANNEL" ], "out": [ "T_RC", - "T_COUNT", "T_ID2", + "T_COUNT", "T_GROSS" ] }, @@ -14939,11 +21073,13 @@ const SVC_SCREENS = [ "svc": "PAY_XFERLINK", "desc": "이체 연계: 지급건에 대한 XFER 원장 엔트리를 py_ledger 에 기록.", "in": [ - "T_ID1", "T_AMOUNT", - "T_BIZDATE" + "T_BIZDATE", + "T_ID1" ], - "out": [] + "out": [ + "T_RC" + ] }, { "mod": "py", @@ -14954,10 +21090,10 @@ const SVC_SCREENS = [ "T_ID1" ], "out": [ + "T_RC", "T_NET", "T_AMT1", - "T_DELTA", - "T_RC" + "T_DELTA" ] }, { @@ -14969,9 +21105,9 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", - "T_GROSS", - "T_RC" + "T_GROSS" ] }, { @@ -14983,8 +21119,10 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", - "T_RC" + "T_AMT3", + "T_ARG4" ] }, { @@ -14996,19 +21134,19 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_COUNT" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_3WAY", - "desc": "3-way 대사: 승인+매입+원장 모두 존재 시 MATCHED. */", + "desc": "업무: 3-way 대사: 승인+매입+원장 모두 존재 시 MATCHED.", "in": [ + "T_BIZDATE", "T_PURCHASE_ID", - "T_NET", - "T_BIZDATE" + "T_NET" ], "out": [ "T_RC" @@ -15018,26 +21156,27 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_4WAY", - "desc": "승인-매입-입금-지급 4-way 대사: 승인(approval), 입금(settlement), 지급", + "desc": "업무: 승인-매입-입금-지급 4-way 대사.", "in": [ + "T_BIZDATE", "T_PURCHASE_ID", - "T_NET", - "T_BIZDATE" + "T_NET" ], "out": [ - "T_ID1", - "T_RC" + "T_RC", + "T_ID1" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_ACQMATCH", - "desc": "매입-입금 축 대사: 당일 매입 순액 합계와 원장 RECON 입금 합계를 비교해 차액을 산출.", + "desc": "업무: 매입-입금 축 대사: 당일 매입 순액 합계와 원장 RECON 입금 합계를 비교해 차액을 산출.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", "T_DELTA" @@ -15047,26 +21186,27 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_ACQ_DEP", - "desc": "매입-입금(원장) 대사: 원장 합계와 net 비교. */", + "desc": "업무: 매입-입금(원장) 대사: 원장 합계와 net 비교.", "in": [ + "T_BIZDATE", "T_PURCHASE_ID", - "T_NET", - "T_BIZDATE" + "T_NET" ], "out": [ - "T_GROSS", - "T_RC" + "T_RC", + "T_GROSS" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_AGEING", - "desc": "미대사 에이징: 미해결 예외를 당일 / 최근 7일 / 7일 초과 세 버킷으로 나눠", + "desc": "업무: 미해결 예외 에이징 (당일/7일/초과).", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", "T_ARG3", @@ -15077,14 +21217,15 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_ALERT", - "desc": "* RC_ALERT.pgc - rc 모듈 서비스 RC_ALERT (one service = one file).", + "desc": "업무: 대사 경보 등록.", "in": [ - "T_PURCHASE_ID", "T_BIZDATE", + "T_PURCHASE_ID", "T_MSG", "T_STR1" ], "out": [ + "T_RC", "T_ID1" ] }, @@ -15092,13 +21233,14 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_AMTDIFF", - "desc": "금액 차이 대사: 기대(net) vs 실제(원장합) 차이 기록. */", + "desc": "업무: 금액 차이 대사: 기대(net) vs 실제(원장합) 차이 기록.", "in": [ + "T_BIZDATE", "T_PURCHASE_ID", - "T_NET", - "T_BIZDATE" + "T_NET" ], "out": [ + "T_RC", "T_DELTA" ] }, @@ -15106,11 +21248,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_APPRGAP", - "desc": "승인 누락 감지: 당일 매입 중 승인 원장이 없는 건을 예외로 등록하고 누락 건수를 반환.", + "desc": "업무: 승인 누락 감지: 당일 매입 중 승인 원장이 없는 건을 예외로 등록하고 누락 건수를 반환.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -15118,11 +21261,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_APPRMATCH", - "desc": "승인-매입 축 대사: 당일 매입 중 승인건과 매칭된 건수와 승인 누락 건수를 분리 집계.", + "desc": "업무: 승인-매입 축 대사: 당일 매입 중 승인건과 매칭된 건수와 승인 누락 건수를 분리 집계.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", "T_COUNT" @@ -15132,27 +21276,28 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_APPR_ACQ", - "desc": "* RC_APPR_ACQ.pgc - rc 모듈 서비스 RC_APPR_ACQ (one service = one file).", + "desc": "업무: 승인-매입 대사 (승인 존재 여부 확인 후 대사 로그).", "in": [ + "T_BIZDATE", "T_PURCHASE_ID", - "T_NET", - "T_BIZDATE" + "T_NET" ], "out": [ - "T_ID1", - "T_RC" + "T_RC", + "T_ID1" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_AUDITADD", - "desc": "대사 감사추적 기록: 지정 매입의 대사 처리 이력을 AUDIT 유형 로그로 감사 추적에 적재.", + "desc": "업무: 대사 감사추적 기록: 지정 매입의 대사 처리 이력을 AUDIT 유형 로그로 감사 추적에 적재.", "in": [ "T_BIZDATE", "T_PURCHASE_ID" ], "out": [ + "T_RC", "T_ID1" ] }, @@ -15160,11 +21305,10 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_AUDITLOG", - "desc": "대사 감사 로그 기록: 호출 서비스명과 사유를 받아 원장에 RC_AUDIT 증적을", + "desc": "업무: 감사 이벤트 이중 기록 (원장 + 경보).", "in": [ - "T_PURCHASE_ID", "T_BIZDATE", - "T_SVCNAME", + "T_PURCHASE_ID", "T_REASON" ], "out": [ @@ -15175,11 +21319,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_AUDITTRAIL", - "desc": "대사 감사추적 조회: 특정 매입의 대사 이력 건수, 대사 관련 원장 증적 건수,", + "desc": "업무: 매입별 대사 감사추적(이력/증적/최종상태).", "in": [ "T_PURCHASE_ID" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2" ] @@ -15188,25 +21333,28 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_AUTOFIX", - "desc": "자동 보정: 예외 해소 + 보정 원장. */", + "desc": "업무: 자동 보정: 예외 해소 + 보정 원장.", "in": [ + "T_BIZDATE", "T_PURCHASE_ID", - "T_AMOUNT", - "T_BIZDATE" + "T_AMOUNT" ], - "out": [] + "out": [ + "T_RC" + ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_BACKDATE", - "desc": "소급 대사: 지정 매입을 과거 영업일자로 소급하여 대사 로그를 MATCHED 로 기록.", + "desc": "업무: 소급 대사: 지정 매입을 과거 영업일자로 소급하여 대사 로그를 MATCHED 로 기록.", "in": [ "T_BIZDATE", "T_PURCHASE_ID", "T_STR1" ], "out": [ + "T_RC", "T_ID1" ] }, @@ -15214,70 +21362,73 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_BRANDDIFF", - "desc": "브랜드별 차이 분석: 업종(category) 단위 매입 총액과 수수료 합계 비율을 점검.", + "desc": "업무: 브랜드별 차이 분석: 업종(category) 단위 매입 총액과 수수료 합계 비율을 점검.", "in": [ "T_BIZDATE", "T_CATEGORY" ], "out": [ - "T_GROSS", + "T_RC", "T_FEE", - "T_RC" + "T_GROSS" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_BRANDRECON", - "desc": "브랜드별 대사: 업종/브랜드(category) 단위로 당일 매입 총액과 건수를 집계.", + "desc": "업무: 브랜드별 대사: 업종/브랜드(category) 단위로 당일 매입 총액과 건수를 집계.", "in": [ "T_BIZDATE", "T_CATEGORY" ], "out": [ - "T_GROSS", - "T_COUNT" + "T_RC", + "T_COUNT", + "T_GROSS" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_BYCATEGORY", - "desc": "업종(카테고리)별 대사 세분: 요청 업종의 당일 MATCHED 건수와 EXCEPTION", + "desc": "업무: 업종별 대사 집계 + 대사율.", "in": [ "T_BIZDATE", "T_CATEGORY" ], "out": [ + "T_RC", "T_ARG1", - "T_ARG2", - "T_RC" + "T_ARG2" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_BYCHANNEL", - "desc": "채널별 대사 세분 집계: 요청 채널(POS/EDC/EDI/FOREIGN)의 당일 MATCHED 대사", + "desc": "업무: 채널별 대사 집계.", "in": [ "T_BIZDATE", "T_CHANNEL" ], "out": [ + "T_RC", "T_COUNT", - "T_GROSS", - "T_FEE" + "T_FEE", + "T_GROSS" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_BYDATE", - "desc": "* RC_BYDATE.pgc - rc 모듈 서비스 RC_BYDATE (one service = one file).", + "desc": "업무: 일자별 대사(MATCHED) 집계.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", "T_GROSS" ] @@ -15286,12 +21437,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_BYISSUER", - "desc": "* RC_BYISSUER.pgc - rc 모듈 서비스 RC_BYISSUER (one service = one file).", + "desc": "업무: 발급사별 매입 건수 집계.", "in": [ "T_BIZDATE", "T_ISSUER" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -15299,12 +21451,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_BYMERCH", - "desc": "* RC_BYMERCH.pgc - rc 모듈 서비스 RC_BYMERCH (one service = one file).", + "desc": "업무: 가맹점별 net 합계 조회.", "in": [ "T_BIZDATE", "T_MERCHANT" ], "out": [ + "T_RC", "T_GROSS" ] }, @@ -15312,12 +21465,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_BYMGROUP", - "desc": "가맹점그룹별 대사 세분: 가맹점 ID prefix (그룹 키) 로 묶어 당일 MATCHED", + "desc": "업무: 가맹점그룹(prefix)별 대사 집계.", "in": [ "T_BIZDATE", "T_KEY1" ], "out": [ + "T_RC", "T_COUNT", "T_GROSS" ] @@ -15326,12 +21480,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_CANCELREC", - "desc": "취소분 대사: 매입 금액과 취소(ac_cancel) 누적 금액을 비교하여 전액 취소면", + "desc": "업무: 취소분 대사 (전액취소=MATCHED / 잔량=HOLD).", "in": [ - "T_PURCHASE_ID", - "T_BIZDATE" + "T_BIZDATE", + "T_PURCHASE_ID" ], "out": [ + "T_RC", "T_AMT1", "T_AMT2", "T_ID1" @@ -15341,25 +21496,27 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_CANCMATCH", - "desc": "취소분 대사: 당일 취소(ac_cancel) 금액 합계와 취소 건수를 유형 무관하게 집계.", + "desc": "업무: 취소분 대사: 당일 취소(ac_cancel) 금액 합계와 취소 건수를 유형 무관하게 집계.", "in": [ "T_BIZDATE" ], "out": [ - "T_COUNT", - "T_AMOUNT" + "T_RC", + "T_AMOUNT", + "T_COUNT" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_CARRYFWD", - "desc": "익일 이월 처리: 당일 미해결 예외를 지정 익일자로 복제 등록하여 이월시킨다.", + "desc": "업무: 익일 이월 처리: 당일 미해결 예외를 지정 익일자로 복제 등록하여 이월시킨다.", "in": [ "T_BIZDATE", "T_STR1" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -15367,12 +21524,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_CARRYIN", - "desc": "전일 이월 대사: 전 영업일의 미해결 예외 건수를 조회해 당일 이월 대상으로 반환.", + "desc": "업무: 전일 이월 대사: 전 영업일의 미해결 예외 건수를 조회해 당일 이월 대상으로 반환.", "in": [ "T_BIZDATE", "T_STR1" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -15380,12 +21538,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_CARRYOVER", - "desc": "이월 대사: 이전 영업일의 EXCEPTION 대사 건들을 다음 영업일 HOLD 건으로", + "desc": "업무: EXCEPTION 건 익일 이월 (INSERT..SELECT).", "in": [ - "T_STR1", - "T_BIZDATE" + "T_BIZDATE", + "T_STR1" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -15393,12 +21552,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_CHANDIFF", - "desc": "채널별 차이 분석: 지정 채널의 매입 순액과 원장 SETTLE 합계 차이를 rc_diff 에 기록.", + "desc": "업무: 채널별 차이 분석: 지정 채널의 매입 순액과 원장 SETTLE 합계 차이를 rc_diff 에 기록.", "in": [ "T_BIZDATE", "T_CHANNEL" ], "out": [ + "T_RC", "T_DELTA" ] }, @@ -15406,28 +21566,29 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_CHANRECON", - "desc": "채널별 대사: 지정 채널의 당일 매입 건수와 대사완료(M/S) 건수를 대조.", + "desc": "업무: 채널별 대사: 지정 채널의 당일 매입 건수와 대사완료(M/S) 건수를 대조.", "in": [ "T_BIZDATE", "T_CHANNEL" ], "out": [ - "T_COUNT", - "T_ARG1" + "T_RC", + "T_ARG1", + "T_COUNT" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_CLOSEDAY", - "desc": "일마감 대사 확정: 미해결 예외나 보류가 남아 있으면 마감을 거부하고, 모두", + "desc": "업무: 일마감 확정 (예외/보류 잔량 시 거부).", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", - "T_RC", "T_COUNT", "T_GROSS" ] @@ -15436,11 +21597,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_CNTDIFF", - "desc": "건수 차이 대사: 매입 건수 vs 대사 로그 건수. */", + "desc": "업무: 건수 차이 대사: 매입 건수 vs 대사 로그 건수.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -15448,23 +21610,26 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_CONFIRM", - "desc": "* RC_CONFIRM.pgc - rc 모듈 서비스 RC_CONFIRM (one service = one file).", + "desc": "업무: 대사 확정 (MATCHED -> CONFIRMED).", "in": [ "T_PURCHASE_ID" ], - "out": [] + "out": [ + "T_RC" + ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_DELAY", - "desc": "* RC_DELAY.pgc - rc 모듈 서비스 RC_DELAY (one service = one file).", + "desc": "업무: 지연 대사 로그(익일 반영).", "in": [ + "T_BIZDATE", "T_PURCHASE_ID", - "T_NET", - "T_BIZDATE" + "T_NET" ], "out": [ + "T_RC", "T_ID1" ] }, @@ -15472,11 +21637,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_DEPGAP", - "desc": "입금 누락 감지: 매입은 있으나 RECON 원장이 없는 미입금 건수를 집계.", + "desc": "업무: 입금 누락 감지: 매입은 있으나 RECON 원장이 없는 미입금 건수를 집계.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -15484,12 +21650,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_DEPMATCH", - "desc": "입금(정산) 대사: 매입 net 금액과 settlement 입금 합계를 비교하여 일치 시", + "desc": "업무: 입금(정산) 대사.", "in": [ - "T_PURCHASE_ID", - "T_BIZDATE" + "T_BIZDATE", + "T_PURCHASE_ID" ], "out": [ + "T_RC", "T_AMT1", "T_AMT2", "T_ID1" @@ -15499,25 +21666,27 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_DEPPAY", - "desc": "입금-지급 축 대사: 원장 XFER 이체 합계와 정산 순액 합계의 지급 정합성을 대조.", + "desc": "업무: 입금-지급 축 대사: 원장 XFER 이체 합계와 정산 순액 합계의 지급 정합성을 대조.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_ARG1", - "T_GROSS", - "T_DELTA" + "T_DELTA", + "T_GROSS" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_DIFFCAUSE", - "desc": "대사 차이 원인분석: 당일 차이를 (1) 수수료 계산 불일치, (2) 취소 존재,", + "desc": "업무: 차이 원인(수수료/취소/승인부재)별 분해.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", "T_ARG3", @@ -15528,24 +21697,26 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_DIFFRANK", - "desc": "차이 순위 분석: 당일 절대 차이금액이 가장 큰 매입 1건과 그 차액을 반환.", + "desc": "업무: 차이 순위 분석: 당일 절대 차이금액이 가장 큰 매입 1건과 그 차액을 반환.", "in": [ "T_BIZDATE" ], "out": [ - "T_PURCHASE_ID", - "T_DELTA" + "T_RC", + "T_DELTA", + "T_PURCHASE_ID" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_DIFFRPT", - "desc": "* RC_DIFFRPT.pgc - rc 모듈 서비스 RC_DIFFRPT (one service = one file).", + "desc": "업무: 당일 차이 라인 건수 조회.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", "T_DELTA" ] @@ -15554,25 +21725,26 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_DIFFTREND", - "desc": "대사 차이 추이 분석: 당일 평균 |차이금액| 과 직전 7영업일 평균을 비교하여", + "desc": "업무: 당일 vs 직전 7일 평균 차이 추이.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_AMT1", - "T_AMT2", - "T_RC" + "T_AMT2" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_DUPCHK", - "desc": "중복 대사 검출: 당일 대사 로그에서 같은 매입이 두 번 이상 대사된 매입", + "desc": "업무: 중복 대사 매입 검출 + WARN 경보.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -15580,40 +21752,43 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_EDCRECON", - "desc": "EDC 채널 대사: EDC 단말 매입의 순액 합계와 미대사 건수를 함께 반환.", + "desc": "업무: EDC 채널 대사: EDC 단말 매입의 순액 합계와 미대사 건수를 함께 반환.", "in": [ "T_BIZDATE" ], "out": [ - "T_NET", - "T_ARG1" + "T_RC", + "T_ARG1", + "T_NET" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_EDIRECON", - "desc": "EDI 대사: 가맹점의 당일 EDI 전문 금액 합계와 매입(취소 제외) 금액 합계를", + "desc": "업무: 가맹점 EDI 전문 대 매입 합계 대사.", "in": [ "T_BIZDATE", "T_MERCHANT" ], "out": [ + "T_RC", "T_AMT1", "T_AMT2", - "T_DELTA", - "T_COUNT" + "T_COUNT", + "T_DELTA" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_ESCALATE", - "desc": "예외 에스컬레이션: 미해결 예외가 걸린 매입의 당일 WARN 경보들을 IN-서브", + "desc": "업무: 미해결 예외 경보 WARN -> CRIT 일괄 승격.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -15621,13 +21796,14 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_EXCEPTION", - "desc": "* RC_EXCEPTION.pgc - rc 모듈 서비스 RC_EXCEPTION (one service = one file).", + "desc": "업무: 대사 예외 등록.", "in": [ - "T_PURCHASE_ID", "T_BIZDATE", + "T_PURCHASE_ID", "T_REASON" ], "out": [ + "T_RC", "T_ID1" ] }, @@ -15635,11 +21811,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_EXCNOTIFY", - "desc": "대사 예외 알림: 당일 미해결 예외 건수를 집계해 임계 초과 시 rc_alert 경보를 발생.", + "desc": "업무: 대사 예외 알림: 당일 미해결 예외 건수를 집계해 임계 초과 시 rc_alert 경보를 발생.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -15647,25 +21824,27 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_FEEBRK", - "desc": "수수료 내역 대사: 당일 매입 수수료 원계와 수수료조정 누계를 비교해 순수수료 산출.", + "desc": "업무: 수수료 내역 대사: 당일 매입 수수료 원계와 수수료조정 누계를 비교해 순수수료 산출.", "in": [ "T_BIZDATE" ], "out": [ - "T_FEE", + "T_RC", + "T_ARG1", "T_DELTA", - "T_ARG1" + "T_FEE" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_FEEDIFF", - "desc": "수수료 차이 일괄 집계: 당일 매입 전체에서 MDR 기대치와 다른 수수료 건수와", + "desc": "업무: 당일 수수료 불일치 건수/차액 합계.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", "T_DELTA" ] @@ -15674,14 +21853,15 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_FEERECON", - "desc": "수수료 대사: 매입 수수료를 가맹점 MDR(bp) 기준 기대 수수료와 비교한다.", + "desc": "업무: MDR 기준 수수료 대사.", "in": [ - "T_PURCHASE_ID", - "T_BIZDATE" + "T_BIZDATE", + "T_PURCHASE_ID" ], "out": [ - "T_FEE", + "T_RC", "T_DELTA", + "T_FEE", "T_ID1" ] }, @@ -15689,11 +21869,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_FIXDUP", - "desc": "중복 자동보정: 동일 매입에 SETTLE 원장이 2건 이상 중복된 매입 건수를 탐지.", + "desc": "업무: 중복 자동보정: 동일 매입에 SETTLE 원장이 2건 이상 중복된 매입 건수를 탐지.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -15701,27 +21882,29 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_FIXFEE", - "desc": "수수료 자동보정: 매입 수수료가 MDR 이론치와 다르면 차이를 조정 델타로 반환.", + "desc": "업무: 수수료 자동보정: 매입 수수료가 MDR 이론치와 다르면 차이를 조정 델타로 반환.", "in": [ "T_BIZDATE", "T_PURCHASE_ID" ], "out": [ - "T_FEE", + "T_RC", "T_ARG1", - "T_DELTA" + "T_DELTA", + "T_FEE" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_FIXFX", - "desc": "외화 자동보정: ac_fx 의 원화환산액과 원통화*환율 이론치의 차이를 산출.", + "desc": "업무: 외화 자동보정: ac_fx 의 원화환산액과 원통화*환율 이론치의 차이를 산출.", "in": [ "T_BIZDATE", "T_PURCHASE_ID" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", "T_DELTA" @@ -15731,11 +21914,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_FIXROUND", - "desc": "반올림 자동보정: 절대차 1 이하 단수 차이를 수수료조정(ac_fee_adj)으로 상계 기록.", + "desc": "업무: 반올림 자동보정: 절대차 1 이하 단수 차이를 수수료조정(ac_fee_adj)으로 상계 기록.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -15743,11 +21927,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_FIXSIGN", - "desc": "부호 자동보정: 부호 반전 rc_diff 라인의 차액 부호를 정정하여 재기록.", + "desc": "업무: 부호 자동보정: 부호 반전 rc_diff 라인의 차액 부호를 정정하여 재기록.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -15755,11 +21940,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_FXDIFF", - "desc": "외화 환산 차이: 당일 ac_fx 원화환산 합계와 원통화*환율 이론합계의 차이를 집계.", + "desc": "업무: 외화 환산 차이: 당일 ac_fx 원화환산 합계와 원통화*환율 이론합계의 차이를 집계.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", "T_DELTA" @@ -15769,27 +21955,28 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_FXRATECHK", - "desc": "외화 환율 검증: 최신 외화 거래의 원통화 금액 x 환율(bp) 재계산 값이 저장된", + "desc": "업무: 환율 재계산 검증.", "in": [ "T_PURCHASE_ID" ], "out": [ - "T_FX_AMT", + "T_RC", "T_AMT1", "T_AMT2", - "T_RC" + "T_FX_AMT" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_FXRECON", - "desc": "외화 대사: 외화 거래(ac_fx)의 원화 환산 금액 합계와 매입 금액을 비교하여", + "desc": "업무: 외화 원화환산 대사.", "in": [ - "T_PURCHASE_ID", - "T_BIZDATE" + "T_BIZDATE", + "T_PURCHASE_ID" ], "out": [ + "T_RC", "T_AMT1", "T_AMT2", "T_ID1" @@ -15799,11 +21986,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_GAPSCAN", - "desc": "대사 누락 스캔: 대사완료(M) 상태인데 대사 로그가 하나도 없는 당일 매입", + "desc": "업무: 상태 M 인데 대사 로그 없는 누락 스캔.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", "T_ID1" ] @@ -15812,23 +22000,26 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_HOLD", - "desc": "* RC_HOLD.pgc - rc 모듈 서비스 RC_HOLD (one service = one file).", + "desc": "업무: 대사 보류 원장 기록. (KEEP)", "in": [ + "T_BIZDATE", "T_PURCHASE_ID", - "T_NET", - "T_BIZDATE" + "T_NET" ], - "out": [] + "out": [ + "T_RC" + ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_HOLDAGE", - "desc": "보류 장기화 조회: 기준일 이전부터 남아 있는 HOLD 대사 건수와 가장 오래된", + "desc": "업무: 장기 보류(HOLD) 건수 + 최고령 보류일.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -15836,12 +22027,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_HOLDCLR", - "desc": "대사 보류 해제: 보류(H) 매입을 접수(A)로 되돌리고 관련 예외를 해결 처리.", + "desc": "업무: 대사 보류 해제: 보류(H) 매입을 접수(A)로 되돌리고 관련 예외를 해결 처리.", "in": [ "T_BIZDATE", "T_PURCHASE_ID" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -15849,11 +22041,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_HOLDLIST", - "desc": "보류 목록 현황: 당일 보류(H) 상태 매입의 건수와 순액 합계를 반환.", + "desc": "업무: 보류 목록 현황: 당일 보류(H) 상태 매입의 건수와 순액 합계를 반환.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", "T_NET" ] @@ -15862,12 +22055,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_HOLDRSN", - "desc": "대사 보류 사유별 현황: 특정 사유의 당일 예외 건수를 미해결/해결로 나누어", + "desc": "업무: 보류 사유별 미해결/해결 건수.", "in": [ "T_BIZDATE", "T_REASON" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", "T_COUNT" @@ -15877,13 +22071,14 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_HOLDSET", - "desc": "대사 보류 설정: 지정 매입 상태를 보류(H)로 변경하고 보류 예외를 사유와 함께 등록.", + "desc": "업무: 대사 보류 설정: 지정 매입 상태를 보류(H)로 변경하고 보류 예외를 사유와 함께 등록.", "in": [ "T_BIZDATE", "T_PURCHASE_ID", "T_REASON" ], "out": [ + "T_RC", "T_ID1" ] }, @@ -15891,28 +22086,30 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_INSTRECON", - "desc": "할부 대사: 할부 개월수(months) 대비 등록된 회차(seq_no) 수를 비교하여", + "desc": "업무: 할부 회차 완납 대사.", "in": [ - "T_PURCHASE_ID", - "T_BIZDATE" + "T_BIZDATE", + "T_PURCHASE_ID" ], "out": [ - "T_INSTALL_N", + "T_RC", "T_COUNT", "T_GROSS", - "T_ID1" + "T_ID1", + "T_INSTALL_N" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_ISSDIFF", - "desc": "발급사별 차이 분석: 발급사 단위 매입 순액과 정산 순액 차이를 계산해 반환.", + "desc": "업무: 발급사별 차이 분석: 발급사 단위 매입 순액과 정산 순액 차이를 계산해 반환.", "in": [ "T_BIZDATE", "T_ISSUER" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", "T_DELTA" @@ -15922,12 +22119,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_ISSRECON", - "desc": "발급사별 대사: 지정 발급사의 당일 매입 순액과 정산 순액을 발급사 단위로 대조.", + "desc": "업무: 발급사별 대사: 지정 발급사의 당일 매입 순액과 정산 순액을 발급사 단위로 대조.", "in": [ "T_BIZDATE", "T_ISSUER" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", "T_DELTA" @@ -15937,11 +22135,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_KINDSPLIT", - "desc": "차이 유형 분류: 당일 rc_diff 를 금액(AMT)/건수(CNT) 유형으로 나누어 각각 집계.", + "desc": "업무: 차이 유형 분류: 당일 rc_diff 를 금액(AMT)/건수(CNT) 유형으로 나누어 각각 집계.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", "T_COUNT" @@ -15951,11 +22150,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_LEDGERBAL", - "desc": "원장 축 잔액 대사: 당일 RECON/SETTLE/XFER 기표 합계의 순잔액이 0 인지 확인.", + "desc": "업무: 원장 축 잔액 대사: 당일 RECON/SETTLE/XFER 기표 합계의 순잔액이 0 인지 확인.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", "T_ARG3", @@ -15966,39 +22166,42 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_LEDGERLINK", - "desc": "* RC_LEDGERLINK.pgc - rc 모듈 서비스 RC_LEDGERLINK (one service = one file).", - "in": [ - "T_PURCHASE_ID", - "T_NET", - "T_BIZDATE" - ], - "out": [] - }, - { - "mod": "rc", - "modnm": "대사", - "svc": "RC_LIMITCHK", - "desc": "가맹점 한도 대비 대사: 가맹점 일한도(daily_limit)와 당일 매입(취소 제외)", + "desc": "업무: 대사 원장 연계 기록 (dbio).", "in": [ "T_BIZDATE", - "T_MERCHANT" + "T_PURCHASE_ID", + "T_NET" ], "out": [ - "T_AMT1", - "T_AMT2", "T_RC" ] }, { "mod": "rc", "modnm": "대사", - "svc": "RC_MANAPPRV", - "desc": "수작업 대사 승인: HOLD 상태의 MANREG 수작업 대사 1건을 MATCHED 로 확정하고", + "svc": "RC_LIMITCHK", + "desc": "업무: 가맹점 일한도 초과 검증.", "in": [ - "T_ID1", - "T_BIZDATE" + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", + "T_AMT1", + "T_AMT2" + ] + }, + { + "mod": "rc", + "modnm": "대사", + "svc": "RC_MANAPPRV", + "desc": "업무: 수작업 대사 승인 (HOLD -> MATCHED).", + "in": [ + "T_BIZDATE", + "T_ID1" + ], + "out": [ + "T_RC", "T_COUNT", "T_PURCHASE_ID" ] @@ -16007,12 +22210,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_MANCANCEL", - "desc": "수작업 신청 취소: 아직 확정되지 않은 수작업 HOLD 신청 로그를 삭제.", + "desc": "업무: 수작업 신청 취소: 아직 확정되지 않은 수작업 HOLD 신청 로그를 삭제.", "in": [ "T_BIZDATE", "T_PURCHASE_ID" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -16020,11 +22224,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_MANLIST", - "desc": "수작업 신청 현황: 당일 수작업 대사 로그를 상태별(HOLD/확정)로 나누어 건수 집계.", + "desc": "업무: 수작업 신청 현황: 당일 수작업 대사 로그를 상태별(HOLD/확정)로 나누어 건수 집계.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2" ] @@ -16033,12 +22238,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_MANNG", - "desc": "수작업 대사 반려: 해당 매입의 수작업 HOLD 로그를 ROLLBACK 으로 반려 처리.", + "desc": "업무: 수작업 대사 반려: 해당 매입의 수작업 HOLD 로그를 ROLLBACK 으로 반려 처리.", "in": [ "T_BIZDATE", "T_PURCHASE_ID" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -16046,12 +22252,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_MANOK", - "desc": "수작업 대사 확정: 해당 매입의 수작업 HOLD 로그를 CONFIRMED 로 승인 처리.", + "desc": "업무: 수작업 대사 확정: 해당 매입의 수작업 HOLD 로그를 CONFIRMED 로 승인 처리.", "in": [ "T_BIZDATE", "T_PURCHASE_ID" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -16059,14 +22266,15 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_MANREG", - "desc": "수작업 대사 등록: 담당자가 입력한 수작업 대사를 HOLD 상태(승인 대기)로", + "desc": "업무: 수작업 대사 등록 (승인 대기 HOLD).", "in": [ - "T_PURCHASE_ID", "T_BIZDATE", - "T_REASON", - "T_NET" + "T_PURCHASE_ID", + "T_NET", + "T_REASON" ], "out": [ + "T_RC", "T_ID1" ] }, @@ -16074,13 +22282,14 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_MANREJECT", - "desc": "수작업 대사 반려: 승인 대기(HOLD) 중인 수작업 대사를 ROLLBACK 처리하고,", + "desc": "업무: 수작업 대사 반려 (HOLD -> ROLLBACK + 예외).", "in": [ - "T_ID1", "T_BIZDATE", - "T_REASON" + "T_REASON", + "T_ID1" ], "out": [ + "T_RC", "T_ID2", "T_PURCHASE_ID" ] @@ -16089,12 +22298,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_MANREQ", - "desc": "수작업 대사 신청: 지정 매입에 대해 수작업 대사 요청을 HOLD 상태 로그로 등록.", + "desc": "업무: 수작업 대사 신청: 지정 매입에 대해 수작업 대사 요청을 HOLD 상태 로그로 등록.", "in": [ "T_BIZDATE", "T_PURCHASE_ID" ], "out": [ + "T_RC", "T_ID1" ] }, @@ -16102,13 +22312,14 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_MANUAL", - "desc": "* RC_MANUAL.pgc - rc 모듈 서비스 RC_MANUAL (one service = one file).", + "desc": "업무: 수동 대사 로그.", "in": [ + "T_BIZDATE", "T_PURCHASE_ID", - "T_NET", - "T_BIZDATE" + "T_NET" ], "out": [ + "T_RC", "T_ID1" ] }, @@ -16116,12 +22327,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_MGRPDIFF", - "desc": "가맹점그룹별 차이 분석: 가맹점 접두 그룹의 매입 순액과 정산 순액 차액을 집계.", + "desc": "업무: 가맹점그룹별 차이 분석: 가맹점 접두 그룹의 매입 순액과 정산 순액 차액을 집계.", "in": [ "T_BIZDATE", "T_KEY1" ], "out": [ + "T_RC", "T_DELTA" ] }, @@ -16129,12 +22341,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_MGRPRECON", - "desc": "가맹점그룹별 대사: 가맹점 ID 접두 그룹(prefix) 단위로 매입 순액을 묶어 대사.", + "desc": "업무: 가맹점그룹별 대사: 가맹점 ID 접두 그룹(prefix) 단위로 매입 순액을 묶어 대사.", "in": [ "T_BIZDATE", "T_KEY1" ], "out": [ + "T_RC", "T_NET" ] }, @@ -16142,11 +22355,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_NOTIFY", - "desc": "대사 결과 통보: 당일 대사 요약(rc_summary)을 읽어 통보 메시지를 구성하고,", + "desc": "업무: 대사 결과 요약 통보 (경보 발송).", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_ARG1", "T_ARG2", "T_GROSS" @@ -16156,11 +22370,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_ORPHAN", - "desc": "고아 매입 검출: 승인(approval) 이 전혀 없는 당일 매입을 NOT EXISTS 로", + "desc": "업무: 승인 없는 고아 매입 적재.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -16168,11 +22383,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_OVERAMT", - "desc": "과다 차이 분석: 당일 rc_diff 중 실제가 기대를 초과(diff_amt<0)한 과다 건수/합계.", + "desc": "업무: 과다 차이 분석: 당일 rc_diff 중 실제가 기대를 초과(diff_amt<0)한 과다 건수/합계.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", "T_DELTA" ] @@ -16181,14 +22397,15 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_PARTACQ", - "desc": "부분매입 대사: 분할 매입(ac_install 회차) 합계가 원매입 금액과 일치하는지", + "desc": "업무: 부분매입(회차 합계) 대사.", "in": [ - "T_PURCHASE_ID", - "T_BIZDATE" + "T_BIZDATE", + "T_PURCHASE_ID" ], "out": [ - "T_COUNT", + "T_RC", "T_AMT2", + "T_COUNT", "T_ID1" ] }, @@ -16196,11 +22413,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_PARTDEP", - "desc": "부분 입금 대사: 매입 순액보다 RECON 입금 누계가 작은 부분입금 건수를 집계.", + "desc": "업무: 부분 입금 대사: 매입 순액보다 RECON 입금 누계가 작은 부분입금 건수를 집계.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -16208,13 +22426,14 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_PARTIAL", - "desc": "* RC_PARTIAL.pgc - rc 모듈 서비스 RC_PARTIAL (one service = one file).", + "desc": "업무: 부분 대사 로그.", "in": [ + "T_BIZDATE", "T_PURCHASE_ID", - "T_AMOUNT", - "T_BIZDATE" + "T_AMOUNT" ], "out": [ + "T_RC", "T_ID1" ] }, @@ -16222,11 +22441,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_PAYGAP", - "desc": "지급 누락 감지: 대사완료(M) 매입 중 정산 레코드가 없는 미지급 건수를 반환.", + "desc": "업무: 지급 누락 감지: 대사완료(M) 매입 중 정산 레코드가 없는 미지급 건수를 반환.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -16234,26 +22454,28 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_PAYLEDGER", - "desc": "지급-원장 축 대사: 정산 순액과 원장 SETTLE 기표 합계가 일치하는지 검증하고 차액 반환.", + "desc": "업무: 지급-원장 축 대사: 정산 순액과 원장 SETTLE 기표 합계가 일치하는지 검증하고 차액 반환.", "in": [ "T_BIZDATE" ], "out": [ - "T_GROSS", + "T_RC", "T_ARG1", - "T_DELTA" + "T_DELTA", + "T_GROSS" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_PAYMATCH", - "desc": "지급 대사: 매입 net 대비 원장(ledger) SETTLE/XFER 지급 합계를 비교하고,", + "desc": "업무: 지급(원장) 대사 + 차이 적재.", "in": [ - "T_PURCHASE_ID", - "T_BIZDATE" + "T_BIZDATE", + "T_PURCHASE_ID" ], "out": [ + "T_RC", "T_DELTA", "T_ID1" ] @@ -16262,20 +22484,20 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_POSRECON", - "desc": "POS 채널 대사: POS 채널 매입의 대사완료율을 건수 기준으로 산출(백분율).", + "desc": "업무: POS 채널 대사: POS 채널 매입의 대사완료율을 건수 기준으로 산출(백분율).", "in": [ "T_BIZDATE" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_COUNT" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_PROGRESS", - "desc": "* RC_PROGRESS.pgc - rc 모듈 서비스 RC_PROGRESS (one service = one file).", + "desc": "업무: 진행률(%) = MATCHED / 전체매입 * 100.", "in": [ "T_BIZDATE" ], @@ -16287,36 +22509,36 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_RATEBYISS", - "desc": "발급사별 대사율: 요청 발급사의 당일 대사 로그를 매입과 조인하여 총건수와", + "desc": "업무: 발급사별 대사율(bp).", "in": [ "T_BIZDATE", "T_ISSUER" ], "out": [ - "T_COUNT", + "T_RC", "T_ARG1", - "T_RC" + "T_COUNT" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_RATECHAN", - "desc": "채널별 대사율: 지정 채널의 당일 매입 대비 대사완료 비율을 백분율로 산출.", + "desc": "업무: 채널별 대사율: 지정 채널의 당일 매입 대비 대사완료 비율을 백분율로 산출.", "in": [ "T_BIZDATE", "T_CHANNEL" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_COUNT" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_RATEDAY", - "desc": "일별 대사율: 당일 매입 대비 대사완료 건수 비율을 백분율로 산출.", + "desc": "업무: 일별 대사율: 당일 매입 대비 대사완료 건수 비율을 백분율로 산출.", "in": [ "T_BIZDATE" ], @@ -16328,66 +22550,67 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_RATEMERCH", - "desc": "가맹점별 대사율: 지정 가맹점의 당일 매입 대비 대사완료 비율을 백분율로 산출.", + "desc": "업무: 가맹점별 대사율: 지정 가맹점의 당일 매입 대비 대사완료 비율을 백분율로 산출.", "in": [ "T_BIZDATE", "T_MERCHANT" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_COUNT" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_RATEMON", - "desc": "월별 대사율: 기준일이 속한 달 전체 매입 대비 대사완료 비율을 백분율로 산출.", + "desc": "업무: 월별 대사율: 기준일이 속한 달 전체 매입 대비 대사완료 비율을 백분율로 산출.", "in": [ "T_BIZDATE" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_COUNT" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_RATESTAT", - "desc": "대사율 통계: 당일 전체 대사 로그 대비 MATCHED 비율을 bp(1/10000) 단위로", + "desc": "업무: 당일 대사율(bp) 통계.", "in": [ "T_BIZDATE" ], "out": [ - "T_COUNT", + "T_RC", "T_ARG1", - "T_RC" + "T_COUNT" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_RATEWEEK", - "desc": "주별 대사율: 기준일 포함 최근 7영업일 매입 대비 대사완료 비율을 백분율로 산출.", + "desc": "업무: 주별 대사율: 기준일 포함 최근 7영업일 매입 대비 대사완료 비율을 백분율로 산출.", "in": [ "T_BIZDATE" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_COUNT" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_RECORR", - "desc": "정정후 재대사: 수수료조정이 반영된 매입을 대상으로 순액 재검증 후 로그를 남긴다.", + "desc": "업무: 정정후 재대사: 수수료조정이 반영된 매입을 대상으로 순액 재검증 후 로그를 남긴다.", "in": [ "T_BIZDATE", "T_PURCHASE_ID" ], "out": [ + "T_RC", "T_NET" ] }, @@ -16395,12 +22618,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_REDIFF", - "desc": "차이발생 재대사: 지정 매입의 기대 순액과 SETTLE 실적 차이를 재산출해 rc_diff 갱신.", + "desc": "업무: 차이발생 재대사: 지정 매입의 기대 순액과 SETTLE 실적 차이를 재산출해 rc_diff 갱신.", "in": [ "T_BIZDATE", "T_PURCHASE_ID" ], "out": [ + "T_RC", "T_DELTA" ] }, @@ -16408,38 +22632,40 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_REFULL", - "desc": "전체 재대사: 당일 전체 매입 대비 대사완료(M/S) 비율을 재계산해 진척률로 반환.", + "desc": "업무: 전체 재대사: 당일 전체 매입 대비 대사완료(M/S) 비율을 재계산해 진척률로 반환.", "in": [ "T_BIZDATE" ], "out": [ - "T_COUNT", + "T_RC", "T_ARG1", - "T_RC" + "T_COUNT" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_REFUNDREC", - "desc": "환불분 대사: 취소 유형이 REFUND 인 환불 금액과 건수를 별도로 대사 집계.", + "desc": "업무: 환불분 대사: 취소 유형이 REFUND 인 환불 금액과 건수를 별도로 대사 집계.", "in": [ "T_BIZDATE" ], "out": [ - "T_COUNT", - "T_AMOUNT" + "T_RC", + "T_AMOUNT", + "T_COUNT" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_RELATE", - "desc": "지연도착 재대사: 원장 기표가 매입일보다 늦게 도착한 지연 건을 재대사 대상으로 집계.", + "desc": "업무: 지연도착 재대사: 원장 기표가 매입일보다 늦게 도착한 지연 건을 재대사 대상으로 집계.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -16447,33 +22673,38 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_RELEASE", - "desc": "* RC_RELEASE.pgc - rc 모듈 서비스 RC_RELEASE (one service = one file).", + "desc": "업무: 대사 보류 해제 (HOLD -> RELEASED).", "in": [ "T_PURCHASE_ID" ], - "out": [] + "out": [ + "T_RC" + ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_REMATCH", - "desc": "* RC_REMATCH.pgc - rc 모듈 서비스 RC_REMATCH (one service = one file).", + "desc": "업무: 재대사 원장 기록. (KEEP)", "in": [ + "T_BIZDATE", "T_PURCHASE_ID", - "T_NET", - "T_BIZDATE" + "T_NET" ], - "out": [] + "out": [ + "T_RC" + ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_REOPEN", - "desc": "마감 재개: 확정(CONFIRMED)된 당일 대사 건을 다시 MATCHED 로 되돌려 마감을", + "desc": "업무: 일마감 해제 (CONFIRMED -> MATCHED).", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -16481,22 +22712,25 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_REPROC", - "desc": "재처리: EXCEPTION 대사 로그를 MATCHED 로 되돌림. */", + "desc": "업무: 재처리: EXCEPTION 대사 로그를 MATCHED 로 되돌림.", "in": [ "T_PURCHASE_ID" ], - "out": [] + "out": [ + "T_RC" + ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_RERUN", - "desc": "임계초과 재대사 실행: 임계값을 넘는 당일 차이 건들을 커서로 순회하며", + "desc": "업무: 임계초과 건 재대사 등록 (커서 순회).", "in": [ - "T_AMT1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMT1" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -16504,14 +22738,15 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_RETRO", - "desc": "소급 대사: 과거 영업일에 보류/예외로 남은 특정 매입의 대사를 새 영업일로", + "desc": "업무: 과거 보류/예외 건 소급 확정.", "in": [ + "T_BIZDATE", "T_PURCHASE_ID", "T_NET", - "T_STR1", - "T_BIZDATE" + "T_STR1" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -16519,13 +22754,14 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_RETURN", - "desc": "반품 대사: 반품 원장 + 음수 대사 로그. */", + "desc": "업무: 반품 대사: 반품 원장 + 음수 대사 로그.", "in": [ + "T_BIZDATE", "T_PURCHASE_ID", - "T_AMOUNT", - "T_BIZDATE" + "T_AMOUNT" ], "out": [ + "T_RC", "T_ID1" ] }, @@ -16533,21 +22769,24 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_ROLLBACK", - "desc": "* RC_ROLLBACK.pgc - rc 모듈 서비스 RC_ROLLBACK (one service = one file).", + "desc": "업무: 대사 롤백 (-> ROLLBACK 상태).", "in": [ "T_PURCHASE_ID" ], - "out": [] + "out": [ + "T_RC" + ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_ROUNDDIFF", - "desc": "반올림 차이 분석: 절대차가 1 이하인 단수 차이 건을 반올림 오차로 분류해 집계.", + "desc": "업무: 반올림 차이 분석: 절대차가 1 이하인 단수 차이 건을 반올림 오차로 분류해 집계.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -16555,11 +22794,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_SIGNDIFF", - "desc": "부호 오류 차이 분석: 기대와 실제의 부호가 서로 반대인 rc_diff 라인을 검출.", + "desc": "업무: 부호 오류 차이 분석: 기대와 실제의 부호가 서로 반대인 rc_diff 라인을 검출.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -16567,14 +22807,15 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_SNAPSHOT", - "desc": "대사 스냅샷: 당일 대사 로그의 총건수/MATCHED 건수/MATCHED 금액을 CASE", + "desc": "업무: 현재 시점 대사 현황 스냅샷 upsert.", "in": [ "T_BIZDATE" ], "out": [ - "T_COUNT", + "T_RC", "T_ARG1", "T_ARG2", + "T_COUNT", "T_GROSS" ] }, @@ -16582,48 +22823,51 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_STATS", - "desc": "* RC_STATS.pgc - rc 모듈 서비스 RC_STATS (one service = one file).", + "desc": "업무: MATCHED/EXCEPTION 통계 (dbio count).", "in": [ "T_BIZDATE" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_COUNT" ] }, { "mod": "rc", "modnm": "대사", "svc": "RC_STATUS", - "desc": "* RC_STATUS.pgc - rc 모듈 서비스 RC_STATUS (one service = one file).", + "desc": "업무: 최근 대사 상태 조회.", "in": [ "T_PURCHASE_ID" ], - "out": [] - }, - { - "mod": "rc", - "modnm": "대사", - "svc": "RC_SUMCHK", - "desc": "요약-상세 정합성 검증: rc_summary 의 확정 matched 건수와 대사 로그 실측", - "in": [ - "T_BIZDATE" - ], "out": [ - "T_ARG1", - "T_ARG2", "T_RC" ] }, { "mod": "rc", "modnm": "대사", - "svc": "RC_SUMMARY", - "desc": "* RC_SUMMARY.pgc - rc 모듈 서비스 RC_SUMMARY (one service = one file).", + "svc": "RC_SUMCHK", + "desc": "업무: 요약 대 실측 정합성 검증.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG1", + "T_ARG2" + ] + }, + { + "mod": "rc", + "modnm": "대사", + "svc": "RC_SUMMARY", + "desc": "업무: 대사 요약 upsert (dbio).", + "in": [ + "T_BIZDATE" + ], + "out": [ + "T_RC", "T_COUNT", "T_GROSS" ] @@ -16632,12 +22876,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_TAXRECON", - "desc": "세금계산서 대사: 발행된 세금계산서(공급가+부가세) 합계와 매입 금액을", + "desc": "업무: 세금계산서(공급가+VAT) 대사.", "in": [ - "T_PURCHASE_ID", - "T_BIZDATE" + "T_BIZDATE", + "T_PURCHASE_ID" ], "out": [ + "T_RC", "T_DELTA", "T_ID1" ] @@ -16646,12 +22891,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_THRESHOLD", - "desc": "임계초과 검출: 당일 차이 중 |차이금액| 이 임계값을 초과하는 건수를 세고,", + "desc": "업무: 임계초과 차이 검출 + CRIT 경보.", "in": [ - "T_AMT1", - "T_BIZDATE" + "T_BIZDATE", + "T_AMT1" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -16659,11 +22905,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_TIMEDIFF", - "desc": "기표시점 차이 분석: 매입 영업일과 원장 기표 영업일이 다른 이월/선기표 건수를 집계.", + "desc": "업무: 기표시점 차이 분석: 매입 영업일과 원장 기표 영업일이 다른 이월/선기표 건수를 집계.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -16671,12 +22918,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_TOLERANCE", - "desc": "허용오차 대사: |차이| <= 허용치면 MATCHED. */", + "desc": "업무: 허용오차 대사: |차이| <= 허용치면 MATCHED.", "in": [ + "T_BIZDATE", "T_PURCHASE_ID", "T_DELTA", - "T_ARG1", - "T_BIZDATE" + "T_ARG1" ], "out": [ "T_RC" @@ -16686,11 +22933,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_TOPDIFF", - "desc": "차이 상위 조회: 당일 |차이금액| 의 최대값과 평균값을 구한 뒤 평균을 넘는", + "desc": "업무: 최대/평균 차이 + 평균 초과 건수.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_AMT1", "T_AMT2", "T_COUNT" @@ -16700,12 +22948,13 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_TRAILQRY", - "desc": "대사 감사추적 조회: 지정 매입의 전체 대사 로그 이력 건수와 최종 로그 ID 를 반환.", + "desc": "업무: 대사 감사추적 조회: 지정 매입의 전체 대사 로그 이력 건수와 최종 로그 ID 를 반환.", "in": [ "T_BIZDATE", "T_PURCHASE_ID" ], "out": [ + "T_RC", "T_COUNT", "T_ID1" ] @@ -16714,11 +22963,12 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_UNDERAMT", - "desc": "과소 차이 분석: 당일 rc_diff 중 실제가 기대에 미달(diff_amt>0)한 과소 건수/합계.", + "desc": "업무: 과소 차이 분석: 당일 rc_diff 중 실제가 기대에 미달(diff_amt>0)한 과소 건수/합계.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT", "T_DELTA" ] @@ -16727,51 +22977,55 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RC_UNMATCH", - "desc": "* RC_UNMATCH.pgc - rc 모듈 서비스 RC_UNMATCH (one service = one file).", + "desc": "업무: 미대사 건수 조회. (KEEP)", "in": [ "T_BIZDATE" ], - "out": [] - }, - { - "mod": "rc", - "modnm": "대사", - "svc": "RC_VOIDREC", - "desc": "VOID 매입 대사: 매입 상태가 V(void) 인지와 VOID 유형 취소 증빙 존재를", - "in": [ - "T_PURCHASE_ID", - "T_BIZDATE" - ], "out": [ - "T_ID1", - "T_RC" + "T_RC", + "T_COUNT" ] }, { "mod": "rc", "modnm": "대사", - "svc": "RC_WHTRECON", - "desc": "원천세 대사: 매입의 원천징수(ac_wht) 세액 합계가 정산 net 금액을 넘지", + "svc": "RC_VOIDREC", + "desc": "업무: VOID 매입 대사 (상태 V + VOID 취소 증빙).", "in": [ - "T_PURCHASE_ID", - "T_BIZDATE" + "T_BIZDATE", + "T_PURCHASE_ID" ], "out": [ - "T_TAX", - "T_NET", "T_RC", "T_ID1" ] }, + { + "mod": "rc", + "modnm": "대사", + "svc": "RC_WHTRECON", + "desc": "업무: 원천세 한도 대사.", + "in": [ + "T_BIZDATE", + "T_PURCHASE_ID" + ], + "out": [ + "T_RC", + "T_ID1", + "T_NET", + "T_TAX" + ] + }, { "mod": "rc", "modnm": "대사", "svc": "RC_ZEROAMT", - "desc": "이상금액 검출: 당일 매입 중 금액 0원 또는 음수 net 인 이상 거래를 찾아", + "desc": "업무: 0원/음수 net 이상 거래 예외 적재.", "in": [ "T_BIZDATE" ], "out": [ + "T_RC", "T_COUNT" ] }, @@ -16779,13 +23033,18 @@ const SVC_SCREENS = [ "mod": "rc", "modnm": "대사", "svc": "RECONCILE", - "desc": "* RECONCILE.pgc - rc 모듈 서비스 RECONCILE (one service = one file).", + "desc": "업무: RECONCILE 대사 처리", "in": [ + "T_BIZDATE", "T_PURCHASE_ID", "T_NET", - "T_BIZDATE" + "T_AMOUNT", + "T_FEE", + "T_MERCHANT" ], - "out": [] + "out": [ + "T_RC" + ] }, { "mod": "st", @@ -16806,957 +23065,1832 @@ const SVC_SCREENS = [ "mod": "st", "modnm": "정산/수수료", "svc": "ST_ADJHIST", - "desc": "조정 이력 조회: 매입에 걸린 정산 조정을 최신순 커서로 순회해 건수·증감", + "desc": "매입별 조정 이력 요약. (심화판).", "in": [ - "T_PURCHASE_ID" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", + "T_DELTA", + "T_AMT1", + "T_AMT2", "T_COUNT", - "T_DELTA" + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_ADJREVERSE", - "desc": "조정 취소(역분개): 기존 정산 조정(T_ADJ) 행을 조회해 동일 금액의 반대 부호 조정을", + "desc": "기존 조정을 반대부호로 상쇄. (심화판).", "in": [ - "T_ADJ", - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", "T_ADJ", - "T_DELTA" + "T_DELTA", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_ADJUST", - "desc": "* ST_ADJUST.pgc - st 모듈 서비스 ST_ADJUST (one service = one file).", + "desc": "정산 조정 (가감) 기록. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_DELTA", + "T_MERCHANT", "T_BIZDATE", - "T_REASON" + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_ADJ" + "T_RC", + "T_ARG6", + "T_ADJ", + "T_DELTA", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_ADVANCE", - "desc": "선지급 예약: 지급 레코드(SCHEDULED) 생성 (dbio). */", + "desc": "선지급 예약: 지급 레코드(SCHEDULED) 생성. (심화판).", "in": [ - "T_NET", "T_MERCHANT", "T_BIZDATE", - "T_KEY1" + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_ID1" + "T_RC", + "T_ARG6", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_APPEALSTAT", - "desc": "이의신청 통계: 정산 조정 중 이의신청 관련(reason LIKE 'DISPUTE%') 건수와 청구", + "desc": "이의신청 조정 건수/금액 통계. (심화판).", "in": [ - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_COUNT", + "T_RC", + "T_ARG6", "T_DELTA", - "T_RC" + "T_AMT1", + "T_AMT2", + "T_COUNT", + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_APPROVE", - "desc": "정산 승인 워크플로: 최신 정산 상태를 검증해 CONFIRMED 인 건만 APPROVED 로", + "desc": "정산 승인 (CONFIRMED -> APPROVED). (심화판).", "in": [ - "T_PURCHASE_ID" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_RC" + "T_RC", + "T_ARG6", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_APPROVEALL", - "desc": "정산 일괄 승인: 당일 지급 예정(SCHEDULED) 전 건을 한 번에 지급 완료(PAID)로", + "desc": "당일 지급 예정 일괄 승인(->PAID). (심화판).", "in": [ - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG6", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_BADDEBT", - "desc": "대손 상각: 특정 가맹점의 장기 보류(HELD) 지급 건을 커서로 순회해 총 미수액을", + "desc": "장기 미수 대손 상각(상계+취소). (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", "T_ADJ", - "T_COUNT", - "T_DELTA" + "T_DELTA", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_BANDRATE", - "desc": "* ST_BANDRATE.pgc - st 모듈 서비스 ST_BANDRATE (one service = one file).", + "desc": "금액 구간 요율(bps) 조회. (심화판).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", "T_AMOUNT", - "T_STR1" + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_RC" + "T_RC", + "T_ARG6", + "T_FEE", + "T_AMT1", + "T_AMT2", + "T_COUNT", + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_BANDSIM", - "desc": "구간별 요율 시뮬레이션: 입력 금액에 적용 가능한(band_min <= amount) 모든 구간 중", + "desc": "적용가능 구간 중 최고 요율 시뮬. (심화판).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", "T_AMOUNT", - "T_STR1" + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", + "T_FEE", + "T_AMT1", + "T_AMT2", "T_COUNT", - "T_FEE" + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_BIWEEKLY", - "desc": "격주 정산: 가맹점 당일 정산 net 합계를 집계하고 영업일 기준 10일 뒤(약 2주)", + "desc": "2주 주기 net 합계 지급 예정. (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", "T_ID1", - "T_NET" + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_BYMERCH", - "desc": "* ST_BYMERCH.pgc - st 모듈 서비스 ST_BYMERCH (one service = one file).", + "desc": "가맹점별 정산 집계 upsert. (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", + "T_AMT1", + "T_AMT2", "T_COUNT", - "T_GROSS" + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_CANCEL", - "desc": "정산 취소: 상태 CANCELLED + 취소 조정 (dbio). */", + "desc": "정산 취소: 상태 CANCELLED + 취소 조정. (심화판).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", "T_PURCHASE_ID", - "T_BIZDATE" + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], - "out": [] + "out": [ + "T_RC", + "T_ARG6", + "T_ARG3", + "T_ARG5", + "T_ARG4" + ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_CAP", - "desc": "수수료 상한 적용: 상한 초과분을 감액 조정 (dbio). */", + "desc": "수수료 상한 적용: 상한 초과분을 감액 조정. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", "T_FEE", - "T_ARG1", - "T_BIZDATE" + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_FEE", - "T_DELTA" + "T_RC", + "T_ARG6", + "T_ADJ", + "T_DELTA", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_CAPCHECK", - "desc": "수수료 상한 도달 점검: 수수료가 상한을 초과하는지 판정만 하고 초과분을", + "desc": "상한 초과 여부 판정 (조정 없음). (심화판).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", "T_FEE", - "T_ARG1" + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_DELTA", - "T_AMT1" + "T_ARG6", + "T_FEE", + "T_AMT1", + "T_AMT2", + "T_COUNT", + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_CARRYOVER", - "desc": "이월 정산: 대사완료(M) 상태이지만 정산 레코드가 없는 매입(NOT EXISTS)을", + "desc": "미정산분 익영업일 이월 예약. (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_COUNT", + "T_ARG6", "T_ID1", - "T_NET" + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_CASHBACK", - "desc": "캐시백 정산: 거래 금액에 캐시백율(T_ARG1 bps)을 적용한 캐시백을 가맹점 지급", + "desc": "캐시백 지급 예정 등록. (심화판).", "in": [ - "T_AMOUNT", - "T_ARG1", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", "T_ID1", - "T_AMOUNT" + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_CHAINSET", - "desc": "체인점 통합 정산: 업종/분류(T_CATEGORY)로 묶인 체인 매입의 정산 net 을 조인 집계해", + "desc": "업종 체인 통합 net 지급. (심화판).", "in": [ "T_MERCHANT", - "T_CATEGORY", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", "T_ID1", - "T_COUNT", - "T_NET" + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_CHARGEBACK", - "desc": "차지백 정산 처리: 차지백 금액(T_AMOUNT)을 음(-)의 정산 조정으로 회수하고 해당", + "desc": "차지백 회수 조정 + 지급 보류. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_AMOUNT", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_ADJ", - "T_DELTA" + "T_ARG6", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_CHNLSTAT", - "desc": "채널별 정산 통계 세분: 정산-매입 조인 커서로 채널(POS/EDC/EDI/FOREIGN)별", + "desc": "채널별 정산 net 분해 통계. (심화판).", "in": [ - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", + "T_AMT1", "T_COUNT", "T_GROSS", - "T_AMT1" + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_COLLATERAL", - "desc": "담보/보증금 잔액: 정산 상세의 DEPOSIT 라인을 합산해 가맹점 누적 보증금(담보)", + "desc": "가맹점 누적 보증금 잔액 조회. (심화판).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_AMOUNT", + "T_RC", + "T_ARG6", + "T_AMT1", + "T_AMT2", "T_COUNT", - "T_RC" + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_CONFIRM", - "desc": "* ST_CONFIRM.pgc - st 모듈 서비스 ST_CONFIRM (one service = one file).", + "desc": "정산 확정 (SETTLED -> CONFIRMED). (심화판).", "in": [ - "T_PURCHASE_ID" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], - "out": [] + "out": [ + "T_RC", + "T_ARG6", + "T_ARG3", + "T_ARG5", + "T_ARG4" + ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_COUPONADJ", - "desc": "쿠폰 조정 정산: 발급 쿠폰 금액(T_ARG1)을 가맹점 부담분으로 양(+)의 정산 조정에", + "desc": "쿠폰 사용액 정산 조정(가산). (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_ARG1", + "T_MERCHANT", "T_BIZDATE", - "T_STR1" + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", "T_ADJ", - "T_DELTA" + "T_DELTA", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_CXLRECALC", - "desc": "정산 취소분 재계산: ac_cancel 누계를 매입 net 에서 차감해 정산 net 을", + "desc": "취소 누계 반영 정산 net 재계산. (심화판).", "in": [ - "T_PURCHASE_ID" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", "T_NET", - "T_DELTA" + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_CYCLED1", - "desc": "D+1 정산주기: 당일 SETTLED 정산 net 합계를 산출해 익영업일(pay_date=D+1)", + "desc": "D+1 주기 지급 예약. (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", "T_ID1", - "T_NET" + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_CYCLED2", - "desc": "D+2 정산주기: 당일 CONFIRMED 확정 정산만 집계해 2영업일 뒤(D+2, 주말 스킵)", + "desc": "D+2 주기 지급 예약 (확정분만). (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_COUNT", + "T_ARG6", "T_ID1", - "T_NET" + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_CYCLED3", - "desc": "D+3 정산 주기: 영업일(T_BIZDATE)로부터 영업일 기준 3일 뒤 지급일을 산출해", + "desc": "D+3 영업일 지급 예정 등록. (심화판).", "in": [ - "T_NET", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_ID1" + "T_RC", + "T_ARG6", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_CYCLEMON", - "desc": "월간 정산주기: 대상 월(YYYY-MM)의 가맹점 정산 net 을 합산해 지급일 기준으로", + "desc": "월간 통합 지급 예약 (직접 INSERT). (심화판).", "in": [ "T_MERCHANT", "T_BIZDATE", - "T_KEY1" + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_COUNT", + "T_ARG6", "T_ID1", - "T_GROSS" + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_CYCLEQTR", - "desc": "분기 정산 주기: 기준일(T_BIZDATE) 이하의 미지급 정산 net 을 누적 합계로 집계해", + "desc": "기준일까지 누적 net 분기 지급. (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", "T_ID1", - "T_COUNT", - "T_NET" + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_CYCLESET", - "desc": "정산 주기 설정 조회: 가맹점 한도(daily_limit)를 기준으로 적용 정산 주기 코드", + "desc": "가맹점 한도 기반 정산주기 코드 파생. (심화판).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_AMOUNT" + "T_ARG6", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_CYCLEWEEK", - "desc": "주간 정산주기: 주 종료일 기준 직전 7일(취소 제외) 정산 net 을 합산해", + "desc": "주간(7일 윈도) 통합 지급 예약. (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", "T_ID1", - "T_COUNT", - "T_GROSS" + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_DAILYCLOSE", - "desc": "정산 일마감: 당일 정산·지급·조정 건수를 각각 검증 집계하고 정산 net 총액으로", + "desc": "정산 일마감 선언 (3원 집계 + 마감 원장). (심화판).", "in": [ - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", - "T_ARG1", - "T_ARG2", - "T_GROSS" + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_DEBTCOLLECT", - "desc": "채권 회수 정산: 회수 입금액을 양수 조정(COLLECT)으로 기록하고 원장에 COLLECT", + "desc": "채권 회수 입금 정산. (심화판).", "in": [ - "T_AMOUNT", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_ADJ", - "T_DELTA" + "T_RC", + "T_ARG6", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_DEBTNET", - "desc": "채권 상계: 가맹점의 지급 예정(SCHEDULED) 채무와 보류(HELD) 미수 채권을 각각", + "desc": "지급 채무 대 미수 채권 상계. (심화판).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_AMT1", - "T_AMT2", - "T_NET", - "T_RC" + "T_RC", + "T_ARG6", + "T_ADJ", + "T_DELTA", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_DELAYINT", - "desc": "* ST_DELAYINT.pgc - st 모듈 서비스 ST_DELAYINT (one service = one file).", + "desc": "지연이자(net * bps/10000 * 일수) 정산 상세 기록. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_NET", - "T_ARG1", - "T_ARG2", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL", + "T_ARG1" ], "out": [ - "T_AMOUNT" + "T_RC", + "T_ARG6", + "T_FEE", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_DEPOSITCUT", - "desc": "보증금 차감: 보증금 납부 예정액과 SCHEDULED 지급 가용액 중 작은 금액을", + "desc": "지급액 보증금 원천 차감. (심화판).", "in": [ - "T_AMT1", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", + "T_ADJ", "T_DELTA", - "T_ADJ" + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_DEPOSITSET", - "desc": "보증금 설정/차감: 가맹점 보증금 예치액(T_ARG1)을 정산 상세에 DEPOSIT 라인으로", + "desc": "보증금 예치 기록 + net 차감. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_NET", - "T_ARG1", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_AMOUNT", - "T_NET" + "T_ARG6", + "T_ADJ", + "T_DELTA", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_DIFFCHK", - "desc": "* ST_DIFFCHK.pgc - st 모듈 서비스 ST_DIFFCHK (one service = one file).", + "desc": "정산 net vs 매입 net 차이 점검. (심화판).", "in": [ - "T_PURCHASE_ID" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", "T_DELTA", - "T_RC" + "T_NET", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_DISPUTE", - "desc": "정산 이의신청 처리: 대상 매입의 가맹점을 역추적해 예정 지급을 보류(HELD)로", + "desc": "정산 이의신청 접수 (지급 보류 + 조정). (심화판).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", "T_DELTA", "T_PURCHASE_ID", - "T_BIZDATE", - "T_REASON" + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_ADJ" + "T_RC", + "T_ARG6", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_DISPUTERSV", - "desc": "이의신청 결과 반영: 판정(T_STATUS: A=인용/R=기각)에 따라 인용 시 청구액을 양(+)의", + "desc": "이의신청 인용/기각 후속 처리. (심화판).", "in": [ - "T_PURCHASE_ID", - "T_DELTA", "T_MERCHANT", - "T_STATUS", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_ADJ" + "T_RC", + "T_ARG6", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_DTLSUM", - "desc": "정산 상세 합계: 가맹점+수수료종류+영업일 단위로 상세 라인의 건수·합계·", + "desc": "정산 상세 다중 집계 (건수/합/min/max/평균). (심화판).", "in": [ "T_MERCHANT", + "T_BIZDATE", + "T_REASON", "T_STR1", - "T_BIZDATE" + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_COUNT", - "T_GROSS", + "T_RC", + "T_ARG6", "T_AMT1", "T_AMT2", - "T_AMT3" + "T_AMT3", + "T_COUNT", + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_EDIRECON", - "desc": "EDI 정산 대사: 가맹점 당일 EDI 문서(ac_edi) 청구 합계와 정산 집계", + "desc": "EDI 청구 vs 정산 집계 대사. (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", + "T_AMT1", + "T_AMT2", "T_COUNT", "T_GROSS", - "T_NET", - "T_DELTA", - "T_RC" + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_EVENTFEE", - "desc": "이벤트 수수료 감면: 이벤트 기간 거래에 대해 정상 요율의 절반을 적용한 감면", + "desc": "이벤트 반값 수수료 감면 기록. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_AMOUNT", - "T_ARG1", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", "T_FEE", - "T_RC" + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_FEEPREVIEW", - "desc": "수수료 사전 미리보기: 가맹점 등록 요율(mdr_bps)로 수수료·부가세·원천세와", + "desc": "정산 실수령 예상 견적. (심화판).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", "T_AMOUNT", - "T_MERCHANT" + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", "T_FEE", - "T_TAX", "T_AMT1", - "T_NET", - "T_RC" + "T_AMT2", + "T_COUNT", + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_FEERATIO", - "desc": "수수료율 실효 검증: 매입-가맹점 조인 한 번으로 실제 부과 수수료의 실효", + "desc": "실효 수수료율 vs 계약 요율 편차 검증. (심화판).", "in": [ - "T_PURCHASE_ID" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_ARG1", - "T_ARG2", - "T_DELTA", - "T_RC" + "T_RC", + "T_ARG6", + "T_FEE", + "T_AMT1", + "T_AMT2", + "T_COUNT", + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_FLATFEE", - "desc": "정액 수수료 적용: 거래 건마다 고정 금액 수수료를 정산 상세(st_settle_dtl)에", + "desc": "정액 수수료 적용 (고정액 정산 상세 기록). (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_ARG1", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", "T_FEE", - "T_ID1" + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_FLATVSPCT", - "desc": "정액 대 정률 비교: 정액(T_ARG1)과 정률(금액 x T_ARG2 bps)을 각각 계산해 더 저렴한", + "desc": "정액/정률 중 저가 방식 선택. (심화판).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", "T_AMOUNT", - "T_ARG1", - "T_ARG2" + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_AMT1", - "T_AMT2", + "T_RC", + "T_ARG6", "T_FEE", - "T_RC" + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_FXCONV", - "desc": "외화 환산: fx * rate/100 = KRW. */", + "desc": "외화 환산: fx * rate/100 = KRW. (심화판).", "in": [ - "T_FX_AMT", - "T_ARG1" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL", + "T_FX_AMT" ], "out": [ - "T_AMOUNT" + "T_RC", + "T_ARG6", + "T_FEE", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_FXSETTLE", - "desc": "해외 매입 정산: 매입에 걸린 외화 내역(ac_fx)의 원화 환산액을 합산해 FX", + "desc": "외화 환산액 정산 상세 기록. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL", + "T_FX_AMT" ], "out": [ "T_RC", - "T_AMOUNT", - "T_COUNT" + "T_ARG6", + "T_FEE", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_GRADESTAT", - "desc": "가맹점 등급별 통계: 가맹점 요율(mdr_bps) 그룹별 가맹점 수를 커서로 집계해 등급", - "in": [], - "out": [ - "T_COUNT", - "T_ARG1", + "desc": "mdr_bps 등급별 가맹점 분포. (심화판).", + "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", "T_ARG2", - "T_RC" + "T_CHANNEL" + ], + "out": [ + "T_RC", + "T_ARG6", + "T_AMT1", + "T_COUNT", + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_GROWTHINC", - "desc": "성장 인센티브: 당일 매출과 비교일(T_STR1) 매출을 비교해 증가분(성장분)에 대해서만", + "desc": "전기 대비 매출 성장분 인센티브. (심화판).", "in": [ - "T_ARG1", "T_MERCHANT", "T_BIZDATE", - "T_STR1" + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_DELTA", + "T_ARG6", "T_ADJ", - "T_AMT1" + "T_DELTA", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_GRPALLOC", - "desc": "그룹 정산 배분: 그룹 통합 지급총액(T_AMOUNT)을 구성원의 net 비중에 따라 안분해", + "desc": "net 비중 안분 후 구성원별 지급 등록. (심화판).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", "T_AMOUNT", - "T_KEY1", - "T_BIZDATE" + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_COUNT", - "T_AMOUNT" + "T_ARG6", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_GRPROLLUP", - "desc": "가맹점 그룹 집계 rollup: prefix(T_KEY1) 로 묶인 그룹 구성원의 당일 매출/수수료/net", + "desc": "그룹 구성원 매출/수수료/net rollup. (심화판).", "in": [ - "T_KEY1", - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", + "T_AMT1", "T_COUNT", "T_GROSS", - "T_FEE", - "T_NET" + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_GRPSETTLE", - "desc": "가맹점그룹 통합정산: 그룹 prefix(LIKE) 소속 가맹점별 정산 net 을 커서로", + "desc": "그룹 prefix 통합정산 지급. (심화판).", "in": [ - "T_KEY1", - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", "T_ID1", - "T_COUNT", - "T_GROSS" + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_HOLD", - "desc": "* ST_HOLD.pgc - st 모듈 서비스 ST_HOLD (one service = one file).", + "desc": "가맹점 지급 보류 (SCHEDULED -> HELD). (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], - "out": [] + "out": [ + "T_RC", + "T_ARG6", + "T_GROSS", + "T_ARG5", + "T_ARG4" + ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_HOLDAMT", - "desc": "금액 조건부 보류: 임계액(threshold) 이상의 SCHEDULED 지급을 집합 UPDATE 로", + "desc": "임계액 이상 지급 일괄 보류. (심화판).", "in": [ - "T_AMT1", - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG6", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_INSTANTPAY", - "desc": "즉시 정산 지급: 정산 net 을 당일 지급으로 즉시 확정(status=PAID)해 지급", + "desc": "즉시 지급(PAID) 등록. (심화판).", "in": [ - "T_NET", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_ID1" + "T_ARG6", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_ISSUERSTAT", - "desc": "발급사별 수수료 통계 세분: 당일 정산완료(S) 매입을 발급사(issuer) 단위로", + "desc": "발급사별 수수료 분해 통계. (심화판).", "in": [ - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", + "T_AMT1", + "T_AMT2", + "T_AMT3", "T_COUNT", - "T_FEE" + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_LEDGERLINK", - "desc": "* ST_LEDGERLINK.pgc - st 모듈 서비스 ST_LEDGERLINK (one service = one file).", + "desc": "정산 연계 원장 기록. (심화판).", "in": [ - "T_PURCHASE_ID", + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", "T_NET", - "T_BIZDATE" + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], - "out": [] + "out": [ + "T_RC", + "T_ARG6", + "T_COUNT", + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" + ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_LEDGERSNAP", - "desc": "정산 스냅샷: 당일 정산 건수·net 총액을 집계해 원장에 STSNAP 스냅샷 엔트리로", + "desc": "당일 정산 총액 원장 스냅샷. (심화판).", "in": [ - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_ID1", + "T_RC", + "T_ARG6", "T_COUNT", - "T_GROSS" + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_LIMITCHK", - "desc": "가맹점 한도 점검: 가맹점 일 한도(daily_limit)와 당일 매입 누계(취소 제외)를", + "desc": "일 매입 한도 잔여 점검. (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", "T_AMT1", + "T_AMT2", + "T_COUNT", "T_GROSS", - "T_DELTA", - "T_RC" + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_LOCKBATCH", - "desc": "정산 배치 잠금: 당일 확정된 정산(status=SETTLED)을 일괄 LOCKED 로 전환해 이후", + "desc": "당일 정산 일괄 잠금(SETTLED->LOCKED). (심화판).", "in": [ - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG6", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_LOYALTY", - "desc": "로열티 인센티브: 가맹점의 전체 기간 누적 매출(st_merch_settle 전 일자 합계)에", + "desc": "누적 매출 기반 로열티 보상. (심화판).", "in": [ - "T_ARG1", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", "T_ADJ", - "T_GROSS", - "T_COUNT", - "T_DELTA" + "T_DELTA", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { @@ -17775,1051 +24909,1993 @@ const SVC_SCREENS = [ "mod": "st", "modnm": "정산/수수료", "svc": "ST_MERCHRATE", - "desc": "가맹점 개별 요율 적용: 구간요율표 대신 가맹점 마스터의 계약 요율(mdr_bps)로", + "desc": "가맹점 계약 요율 수수료 산출/기록. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_AMOUNT", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", "T_FEE", - "T_RC" + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_MINFEE", - "desc": "최저 수수료 보정: 산출 수수료가 최저 수수료에 미달하면 부족분을 ADJ 정산", + "desc": "최저 수수료 미달분 보정. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_FEE", - "T_ARG1", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", + "T_AMT3", "T_FEE", - "T_DELTA" + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_MKTGSHARE", - "desc": "마케팅 분담금 정산: 가맹점 일별 집계(st_merch_settle)의 총매출에 분담율", + "desc": "매출 기반 마케팅 분담금 부과. (심화판).", "in": [ - "T_ARG1", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", + "T_FEE", + "T_ID1", "T_GROSS", - "T_FEE" + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_MONTHLY", - "desc": "* ST_MONTHLY.pgc - st 모듈 서비스 ST_MONTHLY (one service = one file).", + "desc": "월별 정산 net 합계 (biz_date LIKE 'YYYY-MM%'). (심화판).", "in": [ "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL", "T_KEY1" ], "out": [ - "T_GROSS" + "T_RC", + "T_ARG6", + "T_COUNT", + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_MONTHSTAT", - "desc": "월별 정산 통계: 대상 월(T_STR1, YYYY-MM)에 속하는 정산의 건수/net 합계/최대 net 을", + "desc": "월(YYYY-MM) 정산 집계. (심화판).", "in": [ - "T_STR1" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL", + "T_KEY1" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_GROSS", - "T_AMT1" + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_NEGSWEEP", - "desc": "음수 정산 정리: 당일 net 이 음수인 SETTLED 정산의 합계를 먼저 파악한 뒤", + "desc": "음수 net 정산 일괄 취소. (심화판).", "in": [ - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_COUNT", - "T_GROSS", - "T_RC" + "T_RC", + "T_ARG6", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_NETTING", - "desc": "* ST_NETTING.pgc - st 모듈 서비스 ST_NETTING (one service = one file).", + "desc": "당일 net 합계 조회. (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_GROSS" + "T_RC", + "T_ARG6", + "T_COUNT", + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_NEXTPAYDT", - "desc": "다음 지급 영업일 계산: 입력 영업일의 다음 영업일(주말 스킵)을 공통 유틸", + "desc": "다음 영업일(지급일) 계산. (심화판).", "in": [ - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_RC" + "T_RC", + "T_ARG6", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_OFFSET", - "desc": "상계: 음수 조정으로 채권 상계 기록 (dbio). */", + "desc": "상계: 음수 조정으로 채권 상계 기록. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", "T_AMOUNT", - "T_BIZDATE" + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_ADJ" + "T_RC", + "T_ARG6", + "T_ADJ", + "T_DELTA", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_PARTIAL", - "desc": "부분정산: 매입 net 에서 기정산 합계를 뺀 잔여 한도 내에서 요청 금액만큼", + "desc": "부분정산 (잔여 한도 검증 후 PARTIAL 정산 생성). (심화판).", "in": [ - "T_PURCHASE_ID", - "T_AMOUNT", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", "T_NET", - "T_SETTLE_ID" + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_PARTNERSET", - "desc": "제휴사 정산 배분: 정산 net 금액에서 제휴 지분율(T_ARG1 bps)만큼을 제휴사", + "desc": "제휴사 지분 지급 예정 등록. (심화판).", "in": [ - "T_NET", - "T_ARG1", + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", "T_STR1", - "T_BIZDATE" + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", "T_ID1", - "T_AMOUNT" + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_PARTPAY", - "desc": "부분 지급 처리: 정산 net 중 일부(T_ARG1)만 당일 지급(PAID)으로 확정하고", + "desc": "net 일부 부분 지급(PAID). (심화판).", "in": [ - "T_NET", - "T_ARG1", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", "T_ID1", - "T_AMOUNT", - "T_NET" + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_PAYDATE", - "desc": "* ST_PAYDATE.pgc - st 모듈 서비스 ST_PAYDATE (one service = one file).", + "desc": "지급일 산정(익영업일) 후 지급일 갱신. (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], - "out": [] + "out": [ + "T_RC", + "T_ARG6", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" + ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_PAYRETRY", - "desc": "지급 재시도: 이체 실패 등으로 CANCELLED 된 지급을 익영업일 지급일로 갱신해", + "desc": "취소 지급 익영업일 재예약. (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_COUNT" + "T_ARG6", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_PAYSTATUS", - "desc": "지급 상태 조회: 가맹점의 가장 최근 지급 레코드에서 상태·금액·지급 예정일을", + "desc": "가맹점 최신 지급 상태 조회. (심화판).", "in": [ - "T_MERCHANT" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL", + "T_STR2" ], "out": [ "T_RC", - "T_NET" + "T_ARG6", + "T_COUNT", + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_PCTFEE", - "desc": "정률 수수료 계산: 거래 금액에 입력 요율(bps, T_ARG1)을 곱해(acq_fee) 수수료를", + "desc": "정률 수수료 계산 (금액 x bps). (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_AMOUNT", - "T_ARG1", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_FEE", "T_RC", - "T_ID1" + "T_ARG6", + "T_FEE", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_PRECONFIRM", - "desc": "정산 확정 전 검증: 당일 지급 중 보류(HELD) 건이 남아있는지 확인해 확정 가능", + "desc": "보류 잔여 여부로 확정 가능 판정. (심화판).", "in": [ - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_AMT1", - "T_AMT2", - "T_RC" + "T_RC", + "T_ARG6", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_PROMOAPPLY", - "desc": "프로모션 할인 적용: 산정된 수수료(T_FEE)에 프로모션 할인율(T_ARG1 bps)을 적용해", + "desc": "수수료 프로모션 할인(음수 조정). (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", "T_FEE", - "T_ARG1", - "T_BIZDATE" + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", "T_ADJ", "T_DELTA", - "T_NET" + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_PROMOCALC", - "desc": "프로모션/이벤트 정산: 이벤트 코드와 할인율(%)을 받아 수수료 할인액을 계산하고", + "desc": "프로모션/이벤트 수수료 할인 정산. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_FEE", - "T_ARG1", + "T_MERCHANT", "T_BIZDATE", - "T_STR1" + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_DELTA", - "T_ADJ", - "T_FEE" + "T_ARG6", + "T_FEE", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_RATEAPPLY", - "desc": "* ST_RATEAPPLY.pgc - st 모듈 서비스 ST_RATEAPPLY (one service = one file).", + "desc": "구간요율 적용 수수료 계산 + 정산 상세 기록. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_AMOUNT", "T_MERCHANT", "T_BIZDATE", - "T_STR1" + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", "T_FEE", - "T_RC" + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_RATECARD", - "desc": "요율 카드 조회: 특정 수수료 유형의 등록 구간 개수와 최소/최대 요율을 집계해", + "desc": "요율표 요약(구간수/최소/최대 bps). (심화판).", "in": [ - "T_STR1" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_COUNT", + "T_ARG6", + "T_FEE", "T_AMT1", - "T_AMT2" + "T_AMT2", + "T_COUNT", + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_RATECMP", - "desc": "요율 유형 비교: 두 수수료 유형(T_STR1/T_STR2)의 입력 금액 적용 요율을", + "desc": "두 요율 유형 적용 bps 비교. (심화판).", "in": [ - "T_AMOUNT", + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", "T_STR1", - "T_STR2" + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_ARG1", - "T_ARG2", - "T_DELTA", - "T_FEE" + "T_RC", + "T_ARG6", + "T_FEE", + "T_AMT1", + "T_AMT2", + "T_COUNT", + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_RATEFLOOR", - "desc": "요율 하한 적용: 정률 수수료(금액 x bps)가 하한액(T_ARG2)에 미달하면 하한으로", + "desc": "정률 수수료 하한 보정 후 기록. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_AMOUNT", - "T_ARG1", - "T_ARG2", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", "T_FEE", - "T_RC" + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_RATEMAX", - "desc": "요율 상한 적용: 정률 수수료(금액 x bps)를 계산하되 상한액(T_ARG2)을 초과하면", + "desc": "정률 수수료 상한 절삭 후 기록. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_AMOUNT", - "T_ARG1", - "T_ARG2", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", "T_FEE", - "T_RC" + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_RATESIM", - "desc": "요율 시뮬레이션: 요율표(st_fee_rate)의 전 구간을 커서로 순회하며 입력 금액에", + "desc": "요율 시뮬레이션 (구간 판정 + 예상 수수료). (심화판).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", "T_AMOUNT", - "T_STR1" + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", + "T_FEE", + "T_AMT1", + "T_AMT2", "T_COUNT", - "T_FEE" + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_REBATE", - "desc": "리베이트 정산: 부과 수수료(T_FEE)의 일정 비율(T_ARG1 bps)을 가맹점에 돌려주는", + "desc": "수수료 기반 리베이트 환급 조정. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", "T_FEE", - "T_ARG1", - "T_BIZDATE" + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", "T_ADJ", - "T_DELTA" + "T_DELTA", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_RECALC", - "desc": "* ST_RECALC.pgc - st 모듈 서비스 ST_RECALC (one service = one file).", + "desc": "매입 수수료 기준 정산 net 재계산 + 갱신. (심화판).", "in": [ - "T_PURCHASE_ID" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_NET" + "T_RC", + "T_ARG6", + "T_NET", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_RECVAGE", - "desc": "미수금 연령 분석: 기준일(T_BIZDATE) 이전 지급일의 보류(HELD) 지급 건수와 금액을", + "desc": "기준일 이전 보류 미수금 집계. (심화판).", "in": [ - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL", + "T_STR2" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_GROSS", - "T_RC" + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_RECVOFFSET", - "desc": "미수금 상계: 매입 조인으로 가맹점의 누적 미수금(음수 조정 절대값)을 산출하고", + "desc": "미수금-지급 상계 처리. (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_AMOUNT", - "T_AMT1" + "T_ARG6", + "T_ADJ", + "T_DELTA", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_RECVSTAT", - "desc": "미수금 통계: 지급(st_payment) 상태별 건수와 금액을 커서로 순회 집계해 보류/예정/", + "desc": "지급 상태별 분포 통계. (심화판).", "in": [ - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL", + "T_STR2" ], "out": [ + "T_RC", + "T_ARG6", "T_COUNT", "T_GROSS", - "T_AMT1" + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_REFERRAL", - "desc": "추천 인센티브: 추천으로 유치된 가맹점 수(prefix T_KEY1 매칭 건수)에 건당 보상액", + "desc": "추천 유치 건당 보상 지급. (심화판).", "in": [ - "T_ARG1", "T_MERCHANT", - "T_KEY1", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_ID1", - "T_COUNT", - "T_AMOUNT" + "T_ARG6", + "T_ADJ", + "T_DELTA", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_REFUNDCALC", - "desc": "환불 재계산: 환불액(T_ARG1)에 비례한 수수료 환급분을 계산해 정산 상세에", + "desc": "환불 비례 수수료 환급 기록. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_ARG1", - "T_ARG2", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_FEE", - "T_ID1" + "T_ARG6", + "T_ADJ", + "T_DELTA", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_RELAYFEE", - "desc": "중계 수수료: 구간요율 조회 후 정산 상세 기록 (dbio). */", + "desc": "중계 수수료: 구간요율 조회 후 정산 상세 기록. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_AMOUNT", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_FEE" + "T_RC", + "T_ARG6", + "T_FEE", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_RELAYSPLIT", - "desc": "중계수수료 분배: 중계 수수료(T_FEE)를 지분율(T_ARG1 bps)에 따라 중계사 몫과", + "desc": "중계수수료 2자 분배 기록. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_FEE", - "T_ARG1", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_AMT1", - "T_AMT2" + "T_RC", + "T_ARG6", + "T_FEE", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_RELEASE", - "desc": "* ST_RELEASE.pgc - st 모듈 서비스 ST_RELEASE (one service = one file).", + "desc": "지급 보류 해제 (HELD -> SCHEDULED). (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], - "out": [] + "out": [ + "T_RC", + "T_ARG6", + "T_GROSS", + "T_ARG5", + "T_ARG4" + ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_REOPEN", - "desc": "정산 마감 해제: 당일 마감 원장(STCLOSE)을 삭제해 마감을 철회한다.", + "desc": "정산 마감 철회 (STCLOSE 삭제). (심화판).", "in": [ - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG6", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_REPARTIAL", - "desc": "부분 재정산: 부분취소액(T_DELTA)만큼 줄어든 잔여 거래금액으로 수수료를 재계산해", + "desc": "부분취소 후 수수료 재계산 조정. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", "T_AMOUNT", + "T_NET", + "T_FEE", "T_DELTA", - "T_ARG1", - "T_BIZDATE" + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", "T_ADJ", - "T_FEE", - "T_DELTA" + "T_DELTA", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_REPORT", - "desc": "* ST_REPORT.pgc - st 모듈 서비스 ST_REPORT (one service = one file).", + "desc": "당일 정산 건수/합계 리포트. (심화판).", "in": [ - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", + "T_AMT1", + "T_AMT2", "T_COUNT", - "T_GROSS" + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_RERUN", - "desc": "정산 재실행: 해당 매입의 정산 상세를 전량 삭제하고 매입 원본(fee)을 기준으로", + "desc": "정산 재실행 (상세 삭제 + 원본 기준 재생성). (심화판).", "in": [ - "T_PURCHASE_ID" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", + "T_AMT1", + "T_AMT2", "T_COUNT", - "T_FEE" + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_RETRORATE", - "desc": "요율 소급적용: 신규 요율(bps)로 매입 금액의 수수료를 재계산하고, 기계상된", + "desc": "요율 소급적용 (차액 조정 기록). (심화판).", "in": [ - "T_SETTLE_ID", - "T_ARG1", - "T_PURCHASE_ID", + "T_MERCHANT", "T_BIZDATE", - "T_STR1" + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_DELTA", + "T_ARG6", "T_ADJ", - "T_FEE" + "T_DELTA", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_ROUNDING", - "desc": "원단위 절사: 최신 정산 net 의 10원 미만 끝자리를 절사해 갱신하고 절사분을", + "desc": "정산 net 원단위 절사. (심화판).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", "T_PURCHASE_ID", - "T_BIZDATE" + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_DELTA", - "T_ADJ", - "T_NET" + "T_ARG6", + "T_NET", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_SLABFEE", - "desc": "슬라브(누진 구간) 요율 계산: 요율표의 각 구간에 대해 금액이 걸치는 부분에만", + "desc": "슬라브 누진 요율 수수료 합산. (심화판).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", "T_AMOUNT", - "T_STR1" + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", "T_FEE", - "T_COUNT" + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_SPLIT", - "desc": "분할 지급: net 을 n 회로 나눠 지급 레코드 생성 (dbio 루프). */", + "desc": "분할 지급: net 을 n 회로 나눠 지급 레코드 생성 (dbio 루프). (심화판).", "in": [ - "T_NET", - "T_INSTALL_N", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_COUNT" + "T_RC", + "T_ARG6", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_SPLITEVEN", - "desc": "분할 지급 잔여 보정: net 을 n 회로 균등 분할하되 나눗셈 끝수를 마지막", + "desc": "끝수 보정 균등 분할 지급. (심화판).", "in": [ - "T_NET", - "T_INSTALL_N", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_COUNT", - "T_AMT1", - "T_AMT2" + "T_RC", + "T_ARG6", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_STATDETAIL", - "desc": "정산 통계 세분: 당일 정산 상세를 수수료 종류(fee_type)별로 분해 집계하는", + "desc": "수수료 종류별 세분 통계. (심화판).", "in": [ - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", + "T_AMT1", + "T_AMT2", + "T_AMT3", "T_COUNT", "T_GROSS", - "T_AMT1" + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_STATS", - "desc": "* ST_STATS.pgc - st 모듈 서비스 ST_STATS (one service = one file).", + "desc": "정산 통계(건수/net 합계) 조회. (심화판).", "in": [ - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", + "T_AMT1", + "T_AMT2", "T_COUNT", "T_GROSS", - "T_AMT1" + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_STATUS", - "desc": "* ST_STATUS.pgc - st 모듈 서비스 ST_STATUS (one service = one file).", + "desc": "정산 상태 조회. (심화판).", "in": [ - "T_PURCHASE_ID" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], - "out": [] + "out": [ + "T_RC", + "T_ARG6", + "T_ARG3", + "T_ARG5", + "T_ARG4" + ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_SUPPLYCALC", - "desc": "공급가액 역산: 부가세 포함 총액(T_AMOUNT)에서 공급가액과 부가세를 분리", + "desc": "VAT 포함 총액에서 공급가액 역산. (심화판).", "in": [ - "T_AMOUNT" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", + "T_AMT2", + "T_TAX", + "T_ID1", + "T_FEE", "T_GROSS", - "T_NET", - "T_TAX" + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_TAXINVCHK", - "desc": "세금계산서 대사: 가맹점 당일 세금계산서(ac_tax_invoice) 공급가·세액 합계와", + "desc": "세금계산서-정산 VAT 대사. (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_GROSS", + "T_RC", + "T_ARG6", "T_AMT1", - "T_TAX", - "T_DELTA", - "T_RC" + "T_AMT2", + "T_COUNT", + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_TAXSUMMARY", - "desc": "세금 요약: 입력 수수료(T_FEE)를 공급가액으로 부가세(acq_vat)와 원천세", + "desc": "부가세+원천세 통합 요약. (심화판).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", "T_FEE", - "T_ARG1" + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", "T_AMT1", "T_AMT2", - "T_TAX", - "T_NET" + "T_AMT3", + "T_COUNT", + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_TIERFEE", - "desc": "구간별 정률 적용: 입력 금액에 해당하는 최상위 구간(band_min <= amount)의 요율을", + "desc": "해당 구간 요율로 정률 수수료 산출. (심화판).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", "T_AMOUNT", - "T_STR1" + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_FEE" + "T_ARG6", + "T_FEE", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_TIERINCENT", - "desc": "구간별 인센티브: 가맹점 일 거래대금(gross) 구간에 따라 인센티브 요율을", + "desc": "거래대금 구간별 인센티브 정산. (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_AMOUNT", - "T_ADJ" + "T_ARG6", + "T_FEE", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_TOPMERCH", - "desc": "상위 가맹점 조회: 당일 정산 net 합계 1위 가맹점을 GROUP BY + ORDER BY", + "desc": "당일 정산 1위 가맹점 조회. (심화판).", "in": [ - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_GROSS" + "T_ARG6", + "T_AMT1", + "T_COUNT", + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_UNLOCK", - "desc": "정산 잠금 해제: 잠금된 정산(status=LOCKED)을 다시 SETTLED 로 되돌려 조정/재계산을", + "desc": "정산 잠금 해제(LOCKED->SETTLED). (심화판).", "in": [ - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG6", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_UNPAIDSUM", - "desc": "미지급 집계: 기준일 이전이 지급 예정일인데 아직 완료(PAID)되지 않은", + "desc": "예정일 경과 미지급 집계. (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL", + "T_STR2" ], "out": [ "T_RC", + "T_ARG6", "T_COUNT", - "T_GROSS" + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_VANDETAIL", - "desc": "VAN 수수료 세분 명세: 당일 정산 상세 중 VAN 라인의 건수/합계/평균을 집계해", + "desc": "VAN 수수료 건수/합계/평균 명세. (심화판).", "in": [ - "T_BIZDATE" + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", + "T_AMT1", + "T_AMT2", + "T_AMT3", "T_COUNT", - "T_FEE", - "T_AMT1" + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_VANFEE", - "desc": "VAN 수수료: 구간요율 조회 후 정산 상세 기록 (dbio). */", + "desc": "VAN 수수료: 구간요율 조회 후 정산 상세 기록. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_AMOUNT", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_FEE" + "T_RC", + "T_ARG6", + "T_FEE", + "T_ID1", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_VAT", - "desc": "* ST_VAT.pgc - st 모듈 서비스 ST_VAT (one service = one file).", + "desc": "정산 부가세 원장 기록. (심화판).", "in": [ - "T_PURCHASE_ID", + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", "T_FEE", - "T_BIZDATE" + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_TAX" + "T_RC", + "T_ARG6", + "T_TAX", + "T_ID1", + "T_FEE", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_VATADJUST", - "desc": "부가세 조정: 조정된 공급가액(T_ARG1)에 대한 재산정 부가세(acq_vat)를 정산 상세에", + "desc": "공급가액 조정 재부가세 기록. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_ARG1", - "T_TAX", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", "T_TAX", - "T_DELTA", - "T_ID1" + "T_ID1", + "T_FEE", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_VATDETAIL", - "desc": "부가세 명세: 가맹점 당일 수수료 상세(MDR/VAN/RELAY 합계)를 공급가액으로 보고", + "desc": "수수료 합계 기준 부가세 명세. (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", + "T_AMT1", + "T_AMT2", + "T_AMT3", + "T_COUNT", "T_GROSS", - "T_TAX", - "T_COUNT" + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_VATREPORT", - "desc": "부가세 신고자료: 대상 월의 정산 상세 VAT 합계와 세금계산서(ac_tax_invoice)", + "desc": "월별 부가세 신고자료 집계. (심화판).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL", "T_KEY1" ], "out": [ - "T_TAX", + "T_RC", + "T_ARG6", "T_COUNT", "T_GROSS", - "T_AMT1", - "T_DELTA" + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_VOIDRECALC", - "desc": "void(전체 취소) 재계산: 매입 정산 net 전액을 역분개(음수 조정)하고 해당", + "desc": "전체 취소 역분개 + 지급 취소. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_NET", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_ADJ", - "T_DELTA" + "T_RC", + "T_ARG6", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_VOLREBATE", - "desc": "거래량 리베이트: 가맹점 일별 집계의 거래 건수 구간에 따라 리베이트율을 차등", + "desc": "거래건수 구간별 리베이트. (심화판).", "in": [ "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", + "T_ARG6", "T_ADJ", - "T_COUNT", - "T_DELTA" + "T_DELTA", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_WHT", - "desc": "* ST_WHT.pgc - st 모듈 서비스 ST_WHT (one service = one file).", + "desc": "원천징수(수수료의 22%) 정산 상세 기록. (심화판).", "in": [ - "T_SETTLE_ID", - "T_PURCHASE_ID", - "T_FEE", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ - "T_TAX" + "T_RC", + "T_ARG6", + "T_TAX", + "T_ID1", + "T_FEE", + "T_GROSS", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_WHTDETAIL", - "desc": "원천세 명세: 가맹점 당일 수수료 합계에 원천징수율(T_ARG1 bps)을 적용해", + "desc": "수수료 합계 기준 원천세 명세. (심화판).", "in": [ - "T_ARG1", "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ + "T_RC", + "T_ARG6", + "T_AMT1", + "T_AMT2", + "T_AMT3", + "T_COUNT", "T_GROSS", - "T_TAX", - "T_RC" + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_WHTREPORT", - "desc": "원천세 명세: 가맹점의 대상 월 정산 상세(WHT) 합계와 원천 대장(ac_wht) 합계를", + "desc": "가맹점 월별 원천세 명세 대사. (심화판).", "in": [ "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL", "T_KEY1" ], "out": [ - "T_TAX", - "T_AMT1", - "T_DELTA", - "T_RC" + "T_RC", + "T_ARG6", + "T_COUNT", + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_WORKFLOW", - "desc": "정산 워크플로 상태 전이: 요청 전이(from=T_STR1 -> to=T_STR2)가 허용 전이표", + "desc": "허용 전이 검증 후 정산 상태 갱신. (심화판).", "in": [ + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", "T_STR1", - "T_STR2", - "T_BIZDATE" + "T_AMOUNT", + "T_NET", + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], "out": [ "T_RC", - "T_COUNT" + "T_ARG6", + "T_ARG3", + "T_ARG5", + "T_ARG4" ] }, { "mod": "st", "modnm": "정산/수수료", "svc": "ST_XFERLINK", - "desc": "* ST_XFERLINK.pgc - st 모듈 서비스 ST_XFERLINK (one service = one file).", + "desc": "이체 연계 원장 기록. (심화판).", "in": [ - "T_PURCHASE_ID", + "T_MERCHANT", + "T_BIZDATE", + "T_REASON", + "T_STR1", + "T_AMOUNT", "T_NET", - "T_BIZDATE" + "T_FEE", + "T_DELTA", + "T_PURCHASE_ID", + "T_SETTLE_ID", + "T_ARG2", + "T_CHANNEL" ], - "out": [] + "out": [ + "T_RC", + "T_ARG6", + "T_COUNT", + "T_GROSS", + "T_ARG3", + "T_ARG5", + "T_ARG4" + ] }, { "mod": "vl", @@ -18831,9 +26907,12 @@ const SVC_SCREENS = [ "T_ARG2" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_COUNT", - "T_ARG3", - "T_RC" + "T_ARG3" ] }, { @@ -18842,12 +26921,17 @@ const SVC_SCREENS = [ "svc": "VL_AGEDOPEN", "desc": "기능: 장기미결 의심거래 검출 (OPEN 상태로 N일 초과 경과, RC 1=존재).", "in": [ + "T_ID1", "T_ARG1" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", "T_COUNT", - "T_ID1", - "T_RC" + "T_ID1" ] }, { @@ -18860,6 +26944,10 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", "T_ARG1" ] }, @@ -18869,11 +26957,19 @@ const SVC_SCREENS = [ "svc": "VL_ANOMALY", "desc": "기능: 이상거래 탐지 후 의심거래 적재 (dbio).", "in": [ + "T_BIZDATE", "T_PURCHASE_ID", - "T_BIZDATE" + "T_ID1" ], "out": [ "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ID2", + "T_ARG2", + "T_ARG1", "T_ID1" ] }, @@ -18886,10 +26982,15 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_GROSS", "T_AMT1", - "T_DELTA", - "T_RC" + "T_DELTA" ] }, { @@ -18898,13 +26999,19 @@ const SVC_SCREENS = [ "svc": "VL_APPRAMT", "desc": "기능: 건별 매입-승인 금액/존재 정합 검증 (RC 1=승인없음, 2=거절승인만 존재, 3=매입없음).", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_AMOUNT" ], "out": [ "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_AMOUNT", - "T_COUNT", - "T_ARG1" + "T_COUNT" ] }, { @@ -18917,8 +27024,12 @@ const SVC_SCREENS = [ "T_ARG1" ], "out": [ - "T_ARG2", - "T_RC" + "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2" ] }, { @@ -18926,11 +27037,18 @@ const SVC_SCREENS = [ "modnm": "정합성검증", "svc": "VL_APPRNOPUR", "desc": "기능: 미매입 승인 검출 (매입 원거래 없는 고아 승인 전수, RC 1=존재; 최소 승인ID 반환).", - "in": [], + "in": [ + "T_ID1" + ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT", - "T_ID1", - "T_RC" + "T_ID1" ] }, { @@ -18942,8 +27060,14 @@ const SVC_SCREENS = [ "T_PURCHASE_ID" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1", + "T_COUNT" ] }, { @@ -18955,10 +27079,15 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_AMT1", "T_AMT2", - "T_AMT3", - "T_RC" + "T_AMT3" ] }, { @@ -18970,10 +27099,15 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_AMT1", "T_AMT2", - "T_DELTA", - "T_RC" + "T_DELTA" ] }, { @@ -18985,11 +27119,16 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_AMT1", "T_AMT2", "T_COUNT", - "T_DELTA", - "T_RC" + "T_DELTA" ] }, { @@ -19001,10 +27140,15 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_AMT1", "T_AMT2", - "T_DELTA", - "T_RC" + "T_DELTA" ] }, { @@ -19013,14 +27157,21 @@ const SVC_SCREENS = [ "svc": "VL_BALANCE", "desc": "기능: 가맹점 순액 대차 검증 (sum(purchase.net) vs sum(settlement.net)).", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_MERCHANT" ], "out": [ + "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_AMT3", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_GROSS", "T_NET", - "T_DELTA", - "T_RC" + "T_DELTA" ] }, { @@ -19032,9 +27183,14 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT", - "T_GROSS", - "T_RC" + "T_GROSS" ] }, { @@ -19046,7 +27202,12 @@ const SVC_SCREENS = [ "T_KEY1" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -19055,10 +27216,17 @@ const SVC_SCREENS = [ "svc": "VL_CANCELMIS", "desc": "기능: 취소 정합 검증 (RC 비트합 1=취소합>매입금액, 2=취소 존재하나 매입상태 미반영, 3=매입없음).", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_AMOUNT" ], "out": [ "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_AMOUNT", "T_AMT1", "T_COUNT" @@ -19074,10 +27242,13 @@ const SVC_SCREENS = [ "T_ARG1" ], "out": [ - "T_COUNT", + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2", - "T_ARG3", - "T_RC" + "T_COUNT", + "T_ARG3" ] }, { @@ -19089,7 +27260,12 @@ const SVC_SCREENS = [ "T_STR1" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -19102,10 +27278,13 @@ const SVC_SCREENS = [ "T_ARG1" ], "out": [ - "T_COUNT", + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2", - "T_ARG3", - "T_RC" + "T_COUNT", + "T_ARG3" ] }, { @@ -19118,10 +27297,13 @@ const SVC_SCREENS = [ "T_ARG1" ], "out": [ - "T_COUNT", + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2", - "T_ARG3", - "T_RC" + "T_COUNT", + "T_ARG3" ] }, { @@ -19134,7 +27316,12 @@ const SVC_SCREENS = [ "T_KEY1" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -19146,7 +27333,12 @@ const SVC_SCREENS = [ "T_ID1" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -19158,7 +27350,13 @@ const SVC_SCREENS = [ "T_PURCHASE_ID" ], "out": [ - "T_RC" + "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -19170,7 +27368,12 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -19178,11 +27381,18 @@ const SVC_SCREENS = [ "modnm": "정합성검증", "svc": "VL_DATEFUT", "desc": "기능: 미래일자 매입 검출 (영업일이 오늘 이후인 매입 수, RC 1=존재).", - "in": [], + "in": [ + "T_ID1" + ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT", - "T_ID1", - "T_RC" + "T_ID1" ] }, { @@ -19192,9 +27402,14 @@ const SVC_SCREENS = [ "desc": "기능: 매입-정산 일자 역전 검출 (정산일<매입일 건수, RC 1=존재; 최소 정산ID 반환).", "in": [], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT", - "T_SETTLE_ID", - "T_RC" + "T_SETTLE_ID" ] }, { @@ -19207,6 +27422,12 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT" ] }, @@ -19216,11 +27437,18 @@ const SVC_SCREENS = [ "svc": "VL_DAYLIMIT", "desc": "기능: 가맹점 일한도 위반 검증 (일자 매입합 > daily_limit → RC 1, 3=가맹점 없음).", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_MERCHANT" ], "out": [ "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_AMT3", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_AMT1", "T_GROSS", "T_DELTA" @@ -19232,13 +27460,20 @@ const SVC_SCREENS = [ "svc": "VL_DUP", "desc": "기능: 중복 매입 검증 (동일 가맹점/금액/영업일 2건 이상 → RC 1).", "in": [ - "T_MERCHANT", "T_BIZDATE", + "T_MERCHANT", "T_AMOUNT" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_AMT3", + "T_ARG6", + "T_ARG2", + "T_ARG1", + "T_COUNT" ] }, { @@ -19247,12 +27482,18 @@ const SVC_SCREENS = [ "svc": "VL_DUPAPPR", "desc": "기능: 중복 승인 검출 건별 (동일 매입 승인가결 2건 이상, RC 1=중복; 검증로그 기록).", "in": [ - "T_PURCHASE_ID", - "T_BIZDATE" + "T_BIZDATE", + "T_PURCHASE_ID" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1", + "T_COUNT" ] }, { @@ -19264,8 +27505,14 @@ const SVC_SCREENS = [ "T_PURCHASE_ID" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1", + "T_COUNT" ] }, { @@ -19277,8 +27524,14 @@ const SVC_SCREENS = [ "T_PURCHASE_ID" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1", + "T_COUNT" ] }, { @@ -19287,13 +27540,19 @@ const SVC_SCREENS = [ "svc": "VL_DUPPAY", "desc": "기능: 건별 중복지급 검출 (ledger XFER 엔트리 2건 이상 → RC 1, 검증로그 기록).", "in": [ - "T_PURCHASE_ID", - "T_BIZDATE" + "T_BIZDATE", + "T_PURCHASE_ID" ], "out": [ + "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT", - "T_GROSS", - "T_RC" + "T_GROSS" ] }, { @@ -19305,9 +27564,13 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_COUNT", + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2", - "T_RC" + "T_ARG1", + "T_COUNT" ] }, { @@ -19319,8 +27582,14 @@ const SVC_SCREENS = [ "T_PURCHASE_ID" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1", + "T_COUNT" ] }, { @@ -19329,14 +27598,21 @@ const SVC_SCREENS = [ "svc": "VL_DUPSETTLE", "desc": "기능: 건별 중복 정산 검출 (동일 매입에 정산 2건 이상 → RC 1, 최초/최종 정산 ID 반환).", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_ID1" ], "out": [ + "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT", "T_NET", "T_ID1", - "T_ID2", - "T_RC" + "T_ID2" ] }, { @@ -19345,12 +27621,19 @@ const SVC_SCREENS = [ "svc": "VL_DUPSETTMR", "desc": "기능: 가맹점 중복 정산 검출 (동일 가맹점+영업일 settlement 2건 이상, RC 1=중복).", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_MERCHANT" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_AMT3", + "T_ARG6", + "T_ARG2", + "T_ARG1", + "T_COUNT" ] }, { @@ -19359,11 +27642,16 @@ const SVC_SCREENS = [ "svc": "VL_EXPIRY", "desc": "기능: 카드 유효기간(YYYYMM) vs 영업일 검증 (RC 1=만료).", "in": [ - "T_STR2", - "T_BIZDATE" + "T_BIZDATE", + "T_STR2" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -19376,6 +27664,12 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_FEE", "T_DELTA" ] @@ -19390,6 +27684,12 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_FEE", "T_DELTA", "T_COUNT" @@ -19406,8 +27706,12 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", - "T_FEE", - "T_ARG2" + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_FEE" ] }, { @@ -19421,6 +27725,11 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", "T_FEE", "T_AMT1", "T_DELTA" @@ -19435,8 +27744,15 @@ const SVC_SCREENS = [ "T_MERCHANT" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_AMT3", + "T_ARG6", + "T_ARG2", + "T_ARG1", + "T_COUNT" ] }, { @@ -19450,6 +27766,11 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", "T_FX_AMT", "T_AMT1", "T_DELTA" @@ -19466,6 +27787,11 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", "T_AMT1", "T_INSTALL_N", "T_COUNT", @@ -19483,6 +27809,9 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2", "T_ARG3" ] @@ -19496,11 +27825,16 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_AMT1", "T_AMT2", "T_COUNT", - "T_DELTA", - "T_RC" + "T_DELTA" ] }, { @@ -19509,12 +27843,19 @@ const SVC_SCREENS = [ "svc": "VL_LEDGDATE", "desc": "기능: 건별 원장 일자 역전 검출 (ledger.biz_date < purchase.biz_date 엔트리 수, RC 1=역전 존재).", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_ID1" ], "out": [ + "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT", - "T_ID1", - "T_RC" + "T_ID1" ] }, { @@ -19523,10 +27864,17 @@ const SVC_SCREENS = [ "svc": "VL_LEDGENTRY", "desc": "기능: 건별 원장 대사 엔트리 검증 (purchase.amount vs ledger RECON 합, RC 1=금액불일치 2=원장없음 3=매입없음", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_AMOUNT" ], "out": [ "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_AMOUNT", "T_AMT1", "T_COUNT" @@ -19538,11 +27886,18 @@ const SVC_SCREENS = [ "svc": "VL_LEDGER", "desc": "기능: 원장 합계 정합 검증 (sum(ledger) vs purchase.net, RC 1=불일치).", "in": [ - "T_PURCHASE_ID", - "T_BIZDATE" + "T_BIZDATE", + "T_PURCHASE_ID" ], "out": [ "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ID2", + "T_ARG2", + "T_ARG1", "T_GROSS" ] }, @@ -19556,7 +27911,14 @@ const SVC_SCREENS = [ "T_AMOUNT" ], "out": [ - "T_RC" + "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_AMT3", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -19565,15 +27927,20 @@ const SVC_SCREENS = [ "svc": "VL_LIMITNEAR", "desc": "기능: 가맹점 일한도 임박 검출 (사용률>=T_ARG1% → RC 1, 초과 → RC 2, 3=가맹점 없음).", "in": [ - "T_ARG1", + "T_BIZDATE", "T_MERCHANT", - "T_BIZDATE" + "T_ARG1" ], "out": [ "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_AMT3", + "T_ARG6", + "T_ARG2", "T_AMT1", - "T_GROSS", - "T_ARG2" + "T_GROSS" ] }, { @@ -19582,12 +27949,17 @@ const SVC_SCREENS = [ "svc": "VL_LIMITPCT", "desc": "기능: 한도 사용률 검증 (가맹점 일 매입합/한도 %가 T_ARG1 기본90 이상, RC 1=근접 8=가맹점없음).", "in": [ - "T_MERCHANT", "T_BIZDATE", + "T_MERCHANT", "T_ARG1" ], "out": [ "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_AMT3", + "T_ARG6", "T_ARG2", "T_AMT1" ] @@ -19598,13 +27970,17 @@ const SVC_SCREENS = [ "svc": "VL_LONGOPEN", "desc": "기능: 장기 미결 의심거래 검출 (OPEN 상태로 N일 초과 경과, 기준 T_ARG1 기본7, RC 1=존재).", "in": [ + "T_ID1", "T_ARG1" ], "out": [ - "T_COUNT", - "T_ID1", + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2", - "T_RC" + "T_COUNT", + "T_ID1" ] }, { @@ -19614,9 +27990,13 @@ const SVC_SCREENS = [ "desc": "기능: 가맹점 요율 위반 검출 (mdr_bps<0 또는 규칙 MDR_MAX 초과, RC 1=위반 존재).", "in": [], "out": [ - "T_COUNT", + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", "T_ARG1", - "T_RC" + "T_COUNT" ] }, { @@ -19628,7 +28008,14 @@ const SVC_SCREENS = [ "T_MERCHANT" ], "out": [ - "T_RC" + "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_AMT3", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -19640,9 +28027,14 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT", - "T_AMT1", - "T_RC" + "T_AMT1" ] }, { @@ -19651,12 +28043,18 @@ const SVC_SCREENS = [ "svc": "VL_NEGFEE", "desc": "기능: 음수 수수료 매입 검출 (일자별 fee<0 매입 수, RC 1=존재; 최소 매입ID 반환).", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_ID1" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT", - "T_ID1", - "T_RC" + "T_ID1" ] }, { @@ -19668,10 +28066,13 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_COUNT", - "T_ARG1", + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2", - "T_RC" + "T_ARG1", + "T_COUNT" ] }, { @@ -19684,10 +28085,13 @@ const SVC_SCREENS = [ "T_ARG1" ], "out": [ - "T_COUNT", + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2", - "T_ARG3", - "T_RC" + "T_COUNT", + "T_ARG3" ] }, { @@ -19700,8 +28104,12 @@ const SVC_SCREENS = [ "T_ARG1" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_COUNT" ] }, { @@ -19713,8 +28121,13 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", + "T_COUNT" ] }, { @@ -19723,12 +28136,18 @@ const SVC_SCREENS = [ "svc": "VL_PURNOAPPR", "desc": "기능: 무승인 매입 검출 (일자별 승인 전무 매입 수, 취소분 제외, RC 1=존재).", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_ID1" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT", - "T_ID1", - "T_RC" + "T_ID1" ] }, { @@ -19742,8 +28161,11 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", - "T_ARG2", "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", "T_DELTA" ] }, @@ -19756,7 +28178,13 @@ const SVC_SCREENS = [ "T_PURCHASE_ID" ], "out": [ - "T_RC" + "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -19765,10 +28193,16 @@ const SVC_SCREENS = [ "svc": "VL_REPORT", "desc": "기능: 심각도별 의심거래 건수 리포트.", "in": [ - "T_STR4", - "T_BIZDATE" + "T_BIZDATE", + "T_STR4" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT" ] }, @@ -19782,7 +28216,14 @@ const SVC_SCREENS = [ "T_AMOUNT" ], "out": [ - "T_RC" + "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_AMT3", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -19792,8 +28233,13 @@ const SVC_SCREENS = [ "desc": "기능: 참조무결성 승인 (취소/무효 매입에 승인가결 존재 건수, RC 1=위반).", "in": [], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", + "T_COUNT" ] }, { @@ -19805,9 +28251,14 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT", - "T_CANCEL_ID", - "T_RC" + "T_CANCEL_ID" ] }, { @@ -19817,9 +28268,14 @@ const SVC_SCREENS = [ "desc": "기능: 참조무결성 해외 (매입 원거래 없는 해외 내역 수+환산합, RC 1=위반).", "in": [], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT", - "T_AMT1", - "T_RC" + "T_AMT1" ] }, { @@ -19829,8 +28285,13 @@ const SVC_SCREENS = [ "desc": "기능: 참조무결성 할부 (매입 원거래 없는 할부 내역 수, NOT IN, RC 1=위반).", "in": [], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", + "T_COUNT" ] }, { @@ -19842,11 +28303,14 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_ARG1", + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2", + "T_ARG1", "T_ARG3", - "T_COUNT", - "T_RC" + "T_COUNT" ] }, { @@ -19855,12 +28319,18 @@ const SVC_SCREENS = [ "svc": "VL_RIMERCH", "desc": "기능: 참조무결성 세분 검증 - 매입→가맹점 (일자별 고아 매입 수, RC 1=고아 존재).", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_ID1" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT", - "T_ID1", - "T_RC" + "T_ID1" ] }, { @@ -19868,11 +28338,18 @@ const SVC_SCREENS = [ "modnm": "정합성검증", "svc": "VL_RIPAY", "desc": "기능: 참조무결성 지급 (지급 XFER 원장의 매입 원거래 부재 건수, RC 1=위반).", - "in": [], + "in": [ + "T_ID1" + ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT", - "T_ID1", - "T_RC" + "T_ID1" ] }, { @@ -19881,13 +28358,19 @@ const SVC_SCREENS = [ "svc": "VL_RIPURCH", "desc": "기능: 참조무결성 세분 검증 - 정산→매입 (일자별 고아 정산 수/금액, RC 1=고아 존재).", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_ID1" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT", "T_ID1", - "T_NET", - "T_RC" + "T_NET" ] }, { @@ -19896,12 +28379,19 @@ const SVC_SCREENS = [ "svc": "VL_RISETTLE", "desc": "기능: 참조무결성 세분 검증 - 정산 가맹점 불일치 (settlement.merchant_id <> purchase.merchant_id, RC", "in": [ - "T_BIZDATE" + "T_BIZDATE", + "T_PURCHASE_ID" ], "out": [ + "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT", - "T_PURCHASE_ID", - "T_RC" + "T_PURCHASE_ID" ] }, { @@ -19911,8 +28401,13 @@ const SVC_SCREENS = [ "desc": "기능: 참조무결성 세금계산서 (매입 원거래 없는 세금계산서 수, LEFT JOIN, RC 1=위반).", "in": [], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", + "T_COUNT" ] }, { @@ -19925,10 +28420,13 @@ const SVC_SCREENS = [ "T_ARG1" ], "out": [ - "T_COUNT", + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2", - "T_ARG3", - "T_RC" + "T_COUNT", + "T_ARG3" ] }, { @@ -19942,20 +28440,31 @@ const SVC_SCREENS = [ "T_STR4", "T_ARG1" ], - "out": [] + "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2" + ] }, { "mod": "vl", "modnm": "정합성검증", "svc": "VL_RULECOUNT", "desc": "기능: 검증규칙 심각도 분포 (INFO/WARN/ERROR/CRIT 건수, RC 1=CRIT 규칙 존재).", - "in": [], + "in": [ + "T_STR3" + ], "out": [ - "T_ARG1", + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2", + "T_ARG1", "T_ARG3", - "T_ARG4", - "T_RC" + "T_ARG4" ] }, { @@ -19968,6 +28477,10 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", "T_ARG1" ] }, @@ -19977,12 +28490,18 @@ const SVC_SCREENS = [ "svc": "VL_RULEHIST", "desc": "기능: 검증 규칙 변경이력 조회 (vl_check_log의 VL_RULEMOD 이력 건수/최종 로그 ID, RC 1=이력 없음).", "in": [ - "T_STR3" + "T_STR3", + "T_ID1" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT", - "T_ID1", - "T_RC" + "T_ID1" ] }, { @@ -19991,13 +28510,16 @@ const SVC_SCREENS = [ "svc": "VL_RULEMOD", "desc": "기능: 검증 규칙 변경 + 변경이력 기록 (임계값/심각도 UPDATE 후 vl_check_log에 이력 적재, RC 3=규칙 없음).", "in": [ + "T_BIZDATE", "T_STR3", "T_ARG1", - "T_STR4", - "T_BIZDATE" + "T_STR4" ], "out": [ "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2" ] }, @@ -20011,7 +28533,12 @@ const SVC_SCREENS = [ "T_STR5" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -20023,7 +28550,12 @@ const SVC_SCREENS = [ "T_STR3" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -20038,7 +28570,11 @@ const SVC_SCREENS = [ "T_ARG1" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2" ] }, { @@ -20051,7 +28587,12 @@ const SVC_SCREENS = [ "T_STR1" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -20064,7 +28605,12 @@ const SVC_SCREENS = [ "T_STR4" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -20072,12 +28618,17 @@ const SVC_SCREENS = [ "modnm": "정합성검증", "svc": "VL_RULESTAT", "desc": "기능: 검증 규칙 현황 집계 (전체/ACTIVE/CRIT 건수, RC 1=활성 규칙 전무).", - "in": [], + "in": [ + "T_STR3" + ], "out": [ - "T_COUNT", - "T_ARG1", + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2", - "T_RC" + "T_ARG1", + "T_COUNT" ] }, { @@ -20091,6 +28642,9 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2" ] }, @@ -20100,11 +28654,17 @@ const SVC_SCREENS = [ "svc": "VL_SALES", "desc": "기능: 매입 원거래 존재 검증 (RC 1=미존재).", "in": [ - "T_PURCHASE_ID", - "T_BIZDATE" + "T_BIZDATE", + "T_PURCHASE_ID" ], "out": [ - "T_RC" + "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -20117,6 +28677,12 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_NET" ] }, @@ -20129,9 +28695,14 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT", - "T_AMT1", - "T_RC" + "T_AMT1" ] }, { @@ -20144,10 +28715,14 @@ const SVC_SCREENS = [ "T_ARG1" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", "T_AMT1", "T_AMT2", - "T_COUNT", - "T_RC" + "T_COUNT" ] }, { @@ -20160,10 +28735,13 @@ const SVC_SCREENS = [ "T_ARG1" ], "out": [ - "T_COUNT", + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2", - "T_ARG3", - "T_RC" + "T_COUNT", + "T_ARG3" ] }, { @@ -20173,8 +28751,13 @@ const SVC_SCREENS = [ "desc": "기능: 서비스 상태 (전체 OPEN 의심거래 수, RC=0).", "in": [], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", + "T_COUNT" ] }, { @@ -20186,7 +28769,13 @@ const SVC_SCREENS = [ "T_PURCHASE_ID" ], "out": [ - "T_RC" + "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -20195,11 +28784,18 @@ const SVC_SCREENS = [ "svc": "VL_SUMFEE", "desc": "기능: 통계-원천 수수료/순액 대사 (merchant_summary fee/net vs purchase 재집계, RC 비트합 1=수수료 2=순액", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_MERCHANT" ], "out": [ "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_AMT3", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_FEE", "T_AMT1", "T_NET", @@ -20216,8 +28812,13 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", + "T_COUNT" ] }, { @@ -20226,11 +28827,18 @@ const SVC_SCREENS = [ "svc": "VL_SUMGROSS", "desc": "기능: 통계-원천 총액 대사 (merchant_summary 총액 vs 원천 매입금액 합, RC 1=불일치 8=통계없음).", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_MERCHANT" ], "out": [ "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_AMT3", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_GROSS", "T_AMT1", "T_DELTA" @@ -20245,8 +28853,13 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", + "T_COUNT" ] }, { @@ -20255,11 +28868,18 @@ const SVC_SCREENS = [ "svc": "VL_SUMNETCHK", "desc": "기능: 통계-원천 순액 대사 (merchant_summary 순액 vs 원천 매입순액 합, RC 1=불일치 8=통계없음).", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_MERCHANT" ], "out": [ "T_RC", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_AMT3", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_NET", "T_AMT1" ] @@ -20270,13 +28890,19 @@ const SVC_SCREENS = [ "svc": "VL_SUMRECON", "desc": "기능: 통계-원천 대사 검증 (merchant_summary 건수/총액 vs purchase 재집계, RC 비트합 1=건수 2=총액, 8=통계 ", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_MERCHANT" ], "out": [ "T_RC", - "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_AMT3", + "T_ARG6", + "T_ARG2", "T_ARG1", + "T_COUNT", "T_GROSS", "T_AMT1" ] @@ -20287,13 +28913,19 @@ const SVC_SCREENS = [ "svc": "VL_SUMTXNCNT", "desc": "기능: 통계-원천 건수 대사 (merchant_summary 건수 vs 원천 매입 건수, RC 1=불일치 8=통계없음).", "in": [ - "T_MERCHANT", - "T_BIZDATE" + "T_BIZDATE", + "T_MERCHANT" ], "out": [ "T_RC", - "T_COUNT", + "T_ARG7", + "T_ARG8", + "T_ARG5", + "T_AMT3", + "T_ARG6", + "T_ARG2", "T_ARG1", + "T_COUNT", "T_DELTA" ] }, @@ -20307,8 +28939,12 @@ const SVC_SCREENS = [ "T_STR1" ], "out": [ - "T_ARG1", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -20317,12 +28953,19 @@ const SVC_SCREENS = [ "svc": "VL_SUSPACT", "desc": "기능: 의심거래 워크플로 2단계 조치 등록 (조치내용 detail 부기 + 검증로그 적재, RC 2=이미 종결 3=없음 4=조치내용 누락).", "in": [ + "T_BIZDATE", + "T_PURCHASE_ID", "T_ID1", - "T_REASON", - "T_BIZDATE" + "T_REASON" ], "out": [ "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_PURCHASE_ID" ] }, @@ -20335,9 +28978,12 @@ const SVC_SCREENS = [ "T_ARG1" ], "out": [ - "T_COUNT", + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2", - "T_RC" + "T_COUNT" ] }, { @@ -20350,8 +28996,12 @@ const SVC_SCREENS = [ "T_STR1" ], "out": [ - "T_ARG1", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -20363,8 +29013,13 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", + "T_COUNT" ] }, { @@ -20376,7 +29031,12 @@ const SVC_SCREENS = [ "T_ID1" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -20389,6 +29049,10 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", "T_ARG1" ] }, @@ -20402,7 +29066,12 @@ const SVC_SCREENS = [ "T_MSG" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -20415,8 +29084,12 @@ const SVC_SCREENS = [ "T_STR1" ], "out": [ - "T_ARG1", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -20429,7 +29102,12 @@ const SVC_SCREENS = [ "T_REASON" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -20441,11 +29119,14 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_COUNT", - "T_ARG1", + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2", - "T_ARG3", - "T_RC" + "T_ARG1", + "T_COUNT", + "T_ARG3" ] }, { @@ -20459,6 +29140,10 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", "T_ARG1" ] }, @@ -20467,11 +29152,16 @@ const SVC_SCREENS = [ "modnm": "정합성검증", "svc": "VL_THRAUDIT", "desc": "기능: 임계값 설정 감사 (RC 비트합 1=ACTIVE ERROR/CRIT 규칙의 임계값 미설정, 2=CRIT 규칙 비활성 방치).", - "in": [], + "in": [ + "T_STR3" + ], "out": [ - "T_ARG1", + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2", - "T_RC" + "T_ARG1" ] }, { @@ -20485,7 +29175,11 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2", + "T_ARG1", "T_ARG3" ] }, @@ -20500,6 +29194,9 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2" ] }, @@ -20509,11 +29206,15 @@ const SVC_SCREENS = [ "svc": "VL_THRESHOLD", "desc": "기능: 규칙 임계치 검사 (dbio get_rule; amount>threshold → RC 1).", "in": [ - "T_STR3", - "T_AMOUNT" + "T_AMOUNT", + "T_STR3" ], "out": [ "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", "T_ARG1" ] }, @@ -20527,6 +29228,10 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", "T_ARG1" ] }, @@ -20539,7 +29244,12 @@ const SVC_SCREENS = [ "T_STR3" ], "out": [ - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1" ] }, { @@ -20553,6 +29263,9 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2", "T_ARG3" ] @@ -20568,6 +29281,9 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2" ] }, @@ -20582,6 +29298,11 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", "T_DELTA" ] }, @@ -20594,11 +29315,16 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_AMT1", "T_AMT2", "T_AMT3", - "T_DELTA", - "T_RC" + "T_DELTA" ] }, { @@ -20611,6 +29337,12 @@ const SVC_SCREENS = [ ], "out": [ "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_NET", "T_AMT1", "T_AMT2" @@ -20622,10 +29354,17 @@ const SVC_SCREENS = [ "svc": "VL_TXNREV", "desc": "기능: 역전거래 검출 건별 (RC 비트 1=음수금액 2=음수순액 4=순액>금액, 8=매입없음).", "in": [ - "T_PURCHASE_ID" + "T_PURCHASE_ID", + "T_AMOUNT" ], "out": [ "T_RC", + "T_ARG3", + "T_ARG8", + "T_ARG4", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_AMOUNT", "T_NET" ] @@ -20635,11 +29374,18 @@ const SVC_SCREENS = [ "modnm": "정합성검증", "svc": "VL_UNACQ", "desc": "기능: 미매입 승인 검출 (매입 원장에 없는 승인 레코드 수, RC 1=고아 승인 존재).", - "in": [], + "in": [ + "T_ID1" + ], "out": [ + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", "T_COUNT", - "T_ID1", - "T_RC" + "T_ID1" ] }, { @@ -20648,13 +29394,17 @@ const SVC_SCREENS = [ "svc": "VL_UNACQAGE", "desc": "기능: 장기 미확정 매입 검출 (접수 A/보류 H 상태로 N일 초과 경과, RC 1=존재).", "in": [ + "T_ID1", "T_ARG1" ], "out": [ - "T_COUNT", - "T_ID1", + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", "T_ARG2", - "T_RC" + "T_COUNT", + "T_ID1" ] }, { @@ -20667,8 +29417,12 @@ const SVC_SCREENS = [ "T_ARG1" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_COUNT" ] }, { @@ -20680,8 +29434,13 @@ const SVC_SCREENS = [ "T_BIZDATE" ], "out": [ - "T_COUNT", - "T_RC" + "T_RC", + "T_ARG8", + "T_ARG5", + "T_ARG6", + "T_ARG2", + "T_ARG1", + "T_COUNT" ] } ];